spark-server/internal/module/friend/model/friend_relation.go
DESKTOP-L8OUL9F\gfl0034 ce9c7a13aa feat: 互相关注后自动成为好友
一方关注只记关注。双方互关时写入已有双向好友边;取消关注若不再互关只删好友边,不删消息。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 21:51:18 +08:00

207 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
})
}