package model import ( "context" "errors" "time" "gorm.io/gorm" "spark-server/internal/module/friend/entity" "spark-server/internal/pkg/errs" ) // FriendRelation maps table friend_relations (one directed edge per row). type FriendRelation struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增"` UserID int64 `gorm:"column:user_id;comment:关系所属用户"` FriendID int64 `gorm:"column:friend_id;comment:对方 user_id"` Status int `gorm:"column:status;comment:1=好友 0=非好友(仅用于单向拉黑占位)"` Remark string `gorm:"column:remark;comment:备注,仅自己可见"` Blocked bool `gorm:"column:is_blocked;comment:是否拉黑对方"` Pinned bool `gorm:"column:is_pinned;comment:会话消息置顶"` Source int `gorm:"column:source;comment:添加来源:0=未知 1=二维码 2=邮箱 3=spark_id 4=互关"` CreatedAt time.Time `gorm:"column:created_at;comment:成为好友或建边时间"` UpdatedAt time.Time `gorm:"column:updated_at;comment:最近更新时间"` } func (FriendRelation) TableName() string { return "t_friend_relations" } func (r *FriendRelation) entity() *entity.Relation { if r == nil { return nil } return &entity.Relation{ ID: r.ID, UserID: r.UserID, FriendID: r.FriendID, Status: entity.EdgeStatus(r.Status), Remark: r.Remark, Blocked: r.Blocked, Pinned: r.Pinned, Source: entity.Source(r.Source), CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, } } func rowFromEdge(e *entity.Relation) FriendRelation { return FriendRelation{ UserID: e.UserID, FriendID: e.FriendID, Status: int(e.Status), Remark: e.Remark, Blocked: e.Blocked, Pinned: e.Pinned, Source: int(e.Source), CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, } } func (m *Repo) GetRelation(ctx context.Context, userID, friendID int64) (*entity.Relation, error) { db, err := m.session(ctx) if err != nil { return nil, err } var row FriendRelation if err := db.Where("user_id = ? AND friend_id = ?", userID, friendID).Take(&row).Error; err != nil { return nil, mapDBErr(err) } return row.entity(), nil } func (m *Repo) ListFriendIDs(ctx context.Context, userID int64) ([]int64, error) { db, err := m.session(ctx) if err != nil { return nil, err } var ids []int64 err = db.Model(&FriendRelation{}). Where("user_id = ? AND status = ?", userID, entity.StatusFriend). Limit(entity.FriendCap).Pluck("friend_id", &ids).Error if err != nil { return nil, mapDBErr(err) } return ids, nil } func (m *Repo) ListBlockedPeerIDs(ctx context.Context, userID int64) ([]int64, error) { db, err := m.session(ctx) if err != nil { return nil, err } var mine, theirs []int64 q := db.Model(&FriendRelation{}) if err := q.Where("user_id = ? AND is_blocked = ?", userID, true).Limit(entity.FriendCap).Pluck("friend_id", &mine).Error; err != nil { return nil, mapDBErr(err) } if err := db.Model(&FriendRelation{}).Where("friend_id = ? AND is_blocked = ?", userID, true).Limit(entity.FriendCap).Pluck("user_id", &theirs).Error; err != nil { return nil, mapDBErr(err) } seen := make(map[int64]struct{}, len(mine)+len(theirs)) out := make([]int64, 0, len(mine)+len(theirs)) for _, id := range append(mine, theirs...) { if id == 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} out = append(out, id) } return out, nil } func (m *Repo) ListFriends(ctx context.Context, userID int64, limit int) ([]*entity.Relation, error) { db, err := m.session(ctx) if err != nil { return nil, err } limit = entity.ClampLimit(limit) var rows []FriendRelation err = db.Where("user_id = ? AND status = ?", userID, entity.StatusFriend). Order("is_pinned DESC, created_at DESC").Limit(limit).Find(&rows).Error if err != nil { return nil, mapDBErr(err) } out := make([]*entity.Relation, 0, len(rows)) for i := range rows { out = append(out, rows[i].entity()) } return out, nil } func (m *Repo) SetRemark(ctx context.Context, userID, friendID int64, remark string) error { return m.updateEdge(ctx, userID, friendID, map[string]any{"remark": remark, "updated_at": time.Now()}) } func (m *Repo) SetPinned(ctx context.Context, userID, friendID int64, pinned bool) error { return m.updateEdge(ctx, userID, friendID, map[string]any{"is_pinned": pinned, "updated_at": time.Now()}) } func (m *Repo) updateEdge(ctx context.Context, userID, friendID int64, values map[string]any) error { if userID <= 0 || friendID <= 0 { return errs.ErrInvalidArgument } db, err := m.session(ctx) if err != nil { return err } res := db.Model(&FriendRelation{}). Where("user_id = ? AND friend_id = ? AND status = ?", userID, friendID, entity.StatusFriend). Updates(values) if res.Error != nil { return mapDBErr(res.Error) } if res.RowsAffected == 0 { return errs.ErrNotFound } return nil } func (m *Repo) IsBlocked(ctx context.Context, userID, targetID int64) (bool, error) { rel, err := m.GetRelation(ctx, userID, targetID) if err != nil { if errors.Is(err, errs.ErrNotFound) { return false, nil } return false, err } return rel.Blocked, nil } func (m *Repo) BlockedEither(ctx context.Context, a, b int64) (bool, error) { me, err := m.IsBlocked(ctx, a, b) if err != nil || me { return me, err } return m.IsBlocked(ctx, b, a) } func (m *Repo) SetBlocked(ctx context.Context, userID, friendID int64, blocked bool) error { if userID <= 0 || friendID <= 0 { return errs.ErrInvalidArgument } db, err := m.session(ctx) if err != nil { return err } now := time.Now() return db.Transaction(func(tx *gorm.DB) error { var row FriendRelation err := tx.Where("user_id = ? AND friend_id = ?", userID, friendID).Take(&row).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return mapDBErr(err) } if errors.Is(err, gorm.ErrRecordNotFound) { if !blocked { return nil } row = FriendRelation{ UserID: userID, FriendID: friendID, Status: int(entity.StatusNone), Blocked: true, CreatedAt: now, UpdatedAt: now, } if err := tx.Create(&row).Error; err != nil { return mapDBErr(err) } } else { if err := tx.Model(&row).Updates(map[string]any{"is_blocked": blocked, "updated_at": now}).Error; err != nil { return mapDBErr(err) } } if blocked { return rejectPairRequests(tx, userID, friendID, now) } return nil }) }