feat: 互相关注后自动成为好友
一方关注只记关注。双方互关时写入已有双向好友边;取消关注若不再互关只删好友边,不删消息。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0471749141
commit
ce9c7a13aa
@ -20,13 +20,14 @@ const (
|
||||
SourceQRCode Source = 1
|
||||
SourceEmail Source = 2
|
||||
SourceSparkID Source = 3
|
||||
SourceFollow Source = 4
|
||||
)
|
||||
|
||||
// Source is friend add origin on the request and the directed edge.
|
||||
type Source int
|
||||
|
||||
func (s Source) Valid() bool {
|
||||
return s == SourceUnknown || s == SourceQRCode || s == SourceEmail || s == SourceSparkID
|
||||
return s == SourceUnknown || s == SourceQRCode || s == SourceEmail || s == SourceSparkID || s == SourceFollow
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
150
internal/module/friend/logic/follow.go
Normal file
150
internal/module/friend/logic/follow.go
Normal file
@ -0,0 +1,150 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"spark-server/internal/module/friend/entity"
|
||||
"spark-server/internal/pkg/errs"
|
||||
"spark-server/internal/pkg/i18n"
|
||||
"spark-server/internal/pkg/reqctx"
|
||||
)
|
||||
|
||||
// FollowView is the viewer's follow state toward one user.
|
||||
type FollowView struct {
|
||||
UID int64 `json:"uid"`
|
||||
Following bool `json:"following"`
|
||||
FollowedBy bool `json:"followed_by"`
|
||||
Mutual bool `json:"mutual"`
|
||||
Friend bool `json:"friend"`
|
||||
}
|
||||
|
||||
func (s *Service) Follow(ctx context.Context, userID, targetID int64) (*FollowView, error) {
|
||||
if err := s.followReady(ctx, userID, targetID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.activeUser(ctx, targetID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.followBlocks(ctx, userID, targetID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.repo.PutFollow(ctx, userID, targetID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.friendIfMutual(ctx, userID, targetID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.followView(ctx, userID, targetID)
|
||||
}
|
||||
|
||||
func (s *Service) Unfollow(ctx context.Context, userID, targetID int64) (*FollowView, error) {
|
||||
if err := s.followReady(ctx, userID, targetID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mutual, err := s.mutualFollow(ctx, userID, targetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.repo.DeleteFollow(ctx, userID, targetID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mutual {
|
||||
if err := s.repo.DeleteFriend(ctx, userID, targetID); err != nil && !errors.Is(err, errs.ErrNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s.followView(ctx, userID, targetID)
|
||||
}
|
||||
|
||||
func (s *Service) GetFollow(ctx context.Context, userID, targetID int64) (*FollowView, error) {
|
||||
if err := s.followReady(ctx, userID, targetID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.activeUser(ctx, targetID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.followView(ctx, userID, targetID)
|
||||
}
|
||||
|
||||
func (s *Service) followReady(ctx context.Context, userID, targetID int64) error {
|
||||
if !s.ready() || userID <= 0 {
|
||||
return errs.ErrUnauthorized
|
||||
}
|
||||
if targetID <= 0 {
|
||||
return errs.ErrInvalidArgument
|
||||
}
|
||||
if entity.SameUser(userID, targetID) {
|
||||
return errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyFriendSelf, nil))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) followBlocks(ctx context.Context, from, to int64) error {
|
||||
lang := reqctx.Lang(ctx)
|
||||
blockedMe, err := s.repo.IsBlocked(ctx, from, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if blockedMe {
|
||||
return errs.ErrForbidden.WithMsg(i18n.T(lang, i18n.KeyFriendBlocked, nil))
|
||||
}
|
||||
blockedBy, err := s.repo.IsBlocked(ctx, to, from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if blockedBy {
|
||||
return errs.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) friendIfMutual(ctx context.Context, userID, targetID int64) error {
|
||||
mutual, err := s.mutualFollow(ctx, userID, targetID)
|
||||
if err != nil || !mutual {
|
||||
return err
|
||||
}
|
||||
ok, err := s.AreFriends(ctx, userID, targetID)
|
||||
if err != nil || ok {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
if err := s.repo.AcceptFriend(ctx, entity.AcceptParams{Edges: []*entity.Relation{
|
||||
entity.NewFriendEdge(userID, targetID, "", entity.SourceFollow, now),
|
||||
entity.NewFriendEdge(targetID, userID, "", entity.SourceFollow, now),
|
||||
}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.greeter != nil {
|
||||
_ = s.greeter.BecameFriends(ctx, userID, targetID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) mutualFollow(ctx context.Context, userID, targetID int64) (bool, error) {
|
||||
following, err := s.repo.HasFollow(ctx, userID, targetID)
|
||||
if err != nil || !following {
|
||||
return false, err
|
||||
}
|
||||
return s.repo.HasFollow(ctx, targetID, userID)
|
||||
}
|
||||
|
||||
func (s *Service) followView(ctx context.Context, userID, targetID int64) (*FollowView, error) {
|
||||
following, err := s.repo.HasFollow(ctx, userID, targetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
followedBy, err := s.repo.HasFollow(ctx, targetID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
friend, err := s.AreFriends(ctx, userID, targetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FollowView{
|
||||
UID: targetID, Following: following, FollowedBy: followedBy,
|
||||
Mutual: following && followedBy, Friend: friend,
|
||||
}, nil
|
||||
}
|
||||
145
internal/module/friend/logic/follow_test.go
Normal file
145
internal/module/friend/logic/follow_test.go
Normal file
@ -0,0 +1,145 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"spark-server/internal/infrastructure/id"
|
||||
convlogic "spark-server/internal/module/conversation/logic"
|
||||
convmodel "spark-server/internal/module/conversation/model"
|
||||
"spark-server/internal/module/friend/entity"
|
||||
friendmodel "spark-server/internal/module/friend/model"
|
||||
msgmodel "spark-server/internal/module/message/model"
|
||||
userentity "spark-server/internal/module/user/entity"
|
||||
"spark-server/internal/module/user/logictest"
|
||||
"spark-server/internal/pkg/errs"
|
||||
)
|
||||
|
||||
func TestOneWayFollowDoesNotFriend(t *testing.T) {
|
||||
svc, users, g := testFriends(t)
|
||||
ctx := t.Context()
|
||||
a, b := twoUsers(t, users)
|
||||
view, err := svc.Follow(ctx, a.ID, b.ID)
|
||||
if err != nil || !view.Following || view.FollowedBy || view.Mutual || view.Friend {
|
||||
t.Fatalf("%+v %v", view, err)
|
||||
}
|
||||
if g.n != 0 {
|
||||
t.Fatalf("greeting %d", g.n)
|
||||
}
|
||||
ok, err := svc.AreFriends(ctx, a.ID, b.ID)
|
||||
if err != nil || ok {
|
||||
t.Fatalf("friends %v %v", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMutualFollowCreatesFriendsOnce(t *testing.T) {
|
||||
svc, users, g := testFriends(t)
|
||||
ctx := t.Context()
|
||||
a, b := twoUsers(t, users)
|
||||
if _, err := svc.Follow(ctx, a.ID, b.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
view, err := svc.Follow(ctx, b.ID, a.ID)
|
||||
if err != nil || !view.Mutual || !view.Friend || !view.Following || !view.FollowedBy {
|
||||
t.Fatalf("%+v %v", view, err)
|
||||
}
|
||||
if g.n != 1 {
|
||||
t.Fatalf("greeting %d", g.n)
|
||||
}
|
||||
again, err := svc.Follow(ctx, b.ID, a.ID)
|
||||
if err != nil || !again.Friend || g.n != 1 {
|
||||
t.Fatalf("repeat %+v greet %d %v", again, g.n, err)
|
||||
}
|
||||
rel, err := svc.Relation(ctx, a.ID, b.ID)
|
||||
if err != nil || rel.Source != entity.SourceFollow {
|
||||
t.Fatalf("source %+v %v", rel, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnfollowMutualDropsFriendsKeepsMessage(t *testing.T) {
|
||||
ids, err := id.New(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users := logictest.Users(ids, nil)
|
||||
ctx := t.Context()
|
||||
a, err := users.AuthenticateEmail(ctx, "test01@spark.com", "1", &userentity.LoginDevice{DeviceID: 1, DeviceType: userentity.DeviceIOS})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := users.AuthenticateEmail(ctx, "test02@spark.com", "1", &userentity.LoginDevice{DeviceID: 2, DeviceType: userentity.DeviceWeb})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
chats := convmodel.NewMemoryP2P()
|
||||
msgs := msgmodel.NewMemoryRepo()
|
||||
p2p := convlogic.NewP2PService(chats, ids)
|
||||
svc := NewService(friendmodel.NewMemoryRepo(), users, ids, NewGreeter(p2p, msgs, ids, nil))
|
||||
if _, err := svc.Follow(ctx, a.ID, b.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.Follow(ctx, b.ID, a.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conv, err := chats.GetP2P(ctx, a.ID, b.ID)
|
||||
if err != nil || conv == nil || conv.ID <= 0 {
|
||||
t.Fatalf("conversation %+v %v", conv, err)
|
||||
}
|
||||
before, err := msgs.ListByConversation(ctx, conv.ID, a.ID, 0, 20)
|
||||
if err != nil || len(before) != 1 {
|
||||
t.Fatalf("before %d %v", len(before), err)
|
||||
}
|
||||
view, err := svc.Unfollow(ctx, a.ID, b.ID)
|
||||
if err != nil || view.Following || view.Mutual || view.Friend || !view.FollowedBy {
|
||||
t.Fatalf("%+v %v", view, err)
|
||||
}
|
||||
after, err := msgs.ListByConversation(ctx, conv.ID, a.ID, 0, 20)
|
||||
if err != nil || len(after) != 1 || after[0].ID != before[0].ID {
|
||||
t.Fatalf("after %d %v", len(after), err)
|
||||
}
|
||||
ok, err := svc.AreFriends(ctx, a.ID, b.ID)
|
||||
if err != nil || ok {
|
||||
t.Fatalf("still friends %v %v", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnfollowOneWayKeepsAcceptedFriend(t *testing.T) {
|
||||
svc, users, g := testFriends(t)
|
||||
ctx := t.Context()
|
||||
a, b := twoUsers(t, users)
|
||||
applied, err := svc.Apply(ctx, a.ID, &ApplyInput{UID: b.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.Accept(ctx, b.ID, AcceptInput{RequestID: applied.RequestID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.Follow(ctx, a.ID, b.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
view, err := svc.Unfollow(ctx, a.ID, b.ID)
|
||||
if err != nil || view.Following || !view.Friend || view.Mutual {
|
||||
t.Fatalf("%+v %v", view, err)
|
||||
}
|
||||
if g.n != 1 {
|
||||
t.Fatalf("greeting %d", g.n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFollowRejectsSelfAndBlock(t *testing.T) {
|
||||
svc, users, _ := testFriends(t)
|
||||
ctx := t.Context()
|
||||
a, b := twoUsers(t, users)
|
||||
if _, err := svc.Follow(ctx, a.ID, a.ID); !errors.Is(err, errs.ErrInvalidArgument) {
|
||||
t.Fatalf("self %v", err)
|
||||
}
|
||||
if err := svc.Block(ctx, a.ID, b.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.Follow(ctx, a.ID, b.ID); !errors.Is(err, errs.ErrForbidden) {
|
||||
t.Fatalf("blocked by me %v", err)
|
||||
}
|
||||
if _, err := svc.Follow(ctx, b.ID, a.ID); !errors.Is(err, errs.ErrNotFound) {
|
||||
t.Fatalf("blocked by peer %v", err)
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,9 @@ type Repository interface {
|
||||
CreateReport(ctx context.Context, r *entity.Report) error
|
||||
AcceptFriend(ctx context.Context, in entity.AcceptParams) error
|
||||
DeleteFriend(ctx context.Context, a, b int64) error
|
||||
PutFollow(ctx context.Context, userID, targetID int64) error
|
||||
DeleteFollow(ctx context.Context, userID, targetID int64) error
|
||||
HasFollow(ctx context.Context, userID, targetID int64) (bool, error)
|
||||
PurgeUser(ctx context.Context, userID int64) error
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ type FriendRelation struct {
|
||||
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"`
|
||||
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:最近更新时间"`
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ type MemoryRepo struct {
|
||||
requests map[int64]*entity.Request
|
||||
byPair map[requestKey]int64
|
||||
reports []*entity.Report
|
||||
follows map[followKey]struct{}
|
||||
}
|
||||
|
||||
func NewMemoryRepo() *MemoryRepo {
|
||||
@ -34,6 +35,7 @@ func NewMemoryRepo() *MemoryRepo {
|
||||
relations: make(map[edgeKey]*entity.Relation),
|
||||
requests: make(map[int64]*entity.Request),
|
||||
byPair: make(map[requestKey]int64),
|
||||
follows: make(map[followKey]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,9 @@ func (m *Repo) PurgeUser(ctx context.Context, userID int64) error {
|
||||
if err := db.Where("user_id = ? OR target_id = ?", userID, userID).Delete(&FriendReport{}).Error; err != nil {
|
||||
return mapDBErr(err)
|
||||
}
|
||||
if err := db.Where("user_id = ? OR target_id = ?", userID, userID).Delete(&UserFollow{}).Error; err != nil {
|
||||
return mapDBErr(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -51,5 +54,10 @@ func (m *MemoryRepo) PurgeUser(_ context.Context, userID int64) error {
|
||||
}
|
||||
}
|
||||
m.reports = kept
|
||||
for key := range m.follows {
|
||||
if key.UserID == userID || key.TargetID == userID {
|
||||
delete(m.follows, key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
98
internal/module/friend/model/user_follow.go
Normal file
98
internal/module/friend/model/user_follow.go
Normal file
@ -0,0 +1,98 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"spark-server/internal/pkg/errs"
|
||||
)
|
||||
|
||||
type followKey struct {
|
||||
UserID int64
|
||||
TargetID int64
|
||||
}
|
||||
|
||||
// UserFollow maps table t_user_follows (one directed follow per row).
|
||||
type UserFollow struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增"`
|
||||
UserID int64 `gorm:"column:user_id;comment:关注人 user_id"`
|
||||
TargetID int64 `gorm:"column:target_id;comment:被关注人 user_id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;comment:关注时间"`
|
||||
}
|
||||
|
||||
func (UserFollow) TableName() string { return "t_user_follows" }
|
||||
|
||||
func (m *Repo) PutFollow(ctx context.Context, userID, targetID int64) error {
|
||||
if userID <= 0 || targetID <= 0 || userID == targetID {
|
||||
return errs.ErrInvalidArgument
|
||||
}
|
||||
db, err := m.session(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row := UserFollow{UserID: userID, TargetID: targetID, CreatedAt: time.Now()}
|
||||
err = db.Where("user_id = ? AND target_id = ?", userID, targetID).
|
||||
Attrs(row).FirstOrCreate(&row).Error
|
||||
return mapDBErr(err)
|
||||
}
|
||||
|
||||
func (m *Repo) DeleteFollow(ctx context.Context, userID, targetID int64) error {
|
||||
if userID <= 0 || targetID <= 0 {
|
||||
return errs.ErrInvalidArgument
|
||||
}
|
||||
db, err := m.session(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.Where("user_id = ? AND target_id = ?", userID, targetID).Delete(&UserFollow{}).Error
|
||||
return mapDBErr(err)
|
||||
}
|
||||
|
||||
func (m *Repo) HasFollow(ctx context.Context, userID, targetID int64) (bool, error) {
|
||||
if userID <= 0 || targetID <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
db, err := m.session(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var n int64
|
||||
err = db.Model(&UserFollow{}).Where("user_id = ? AND target_id = ?", userID, targetID).Count(&n).Error
|
||||
if err != nil {
|
||||
return false, mapDBErr(err)
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
func (m *MemoryRepo) PutFollow(_ context.Context, userID, targetID int64) error {
|
||||
if userID <= 0 || targetID <= 0 || userID == targetID {
|
||||
return errs.ErrInvalidArgument
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.follows == nil {
|
||||
m.follows = make(map[followKey]struct{})
|
||||
}
|
||||
m.follows[followKey{UserID: userID, TargetID: targetID}] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MemoryRepo) DeleteFollow(_ context.Context, userID, targetID int64) error {
|
||||
if userID <= 0 || targetID <= 0 {
|
||||
return errs.ErrInvalidArgument
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.follows, followKey{UserID: userID, TargetID: targetID})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MemoryRepo) HasFollow(_ context.Context, userID, targetID int64) (bool, error) {
|
||||
if userID <= 0 || targetID <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
_, ok := m.follows[followKey{UserID: userID, TargetID: targetID}]
|
||||
return ok, nil
|
||||
}
|
||||
@ -28,6 +28,7 @@ func friendSource(lang i18n.Lang) []entity.Item {
|
||||
item(lang, "friend_source", "qrcode", int(friendentity.SourceQRCode)),
|
||||
item(lang, "friend_source", "email", int(friendentity.SourceEmail)),
|
||||
item(lang, "friend_source", "spark_id", int(friendentity.SourceSparkID)),
|
||||
item(lang, "friend_source", "follow", int(friendentity.SourceFollow)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ func init() {
|
||||
"enum.friend_source.qrcode": {ZhCN: "二维码", En: "QR code", ZhTW: "二維碼"},
|
||||
"enum.friend_source.email": {ZhCN: "邮箱", En: "Email", ZhTW: "電子郵件"},
|
||||
"enum.friend_source.spark_id": {ZhCN: "Spark账号", En: "Spark ID", ZhTW: "Spark帳號"},
|
||||
"enum.friend_source.follow": {ZhCN: "互关", En: "Mutual follow", ZhTW: "互關"},
|
||||
"enum.report_reason.spam": {ZhCN: "垃圾信息", En: "Spam", ZhTW: "垃圾訊息"},
|
||||
"enum.report_reason.harassment": {ZhCN: "骚扰", En: "Harassment", ZhTW: "騷擾"},
|
||||
"enum.report_reason.fake": {ZhCN: "不实身份", En: "Fake identity", ZhTW: "不實身分"},
|
||||
|
||||
61
internal/transport/http/handler/friend/follow.go
Normal file
61
internal/transport/http/handler/friend/follow.go
Normal file
@ -0,0 +1,61 @@
|
||||
package friendhandler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"spark-server/internal/transport/http/respond"
|
||||
)
|
||||
|
||||
func (h *Handler) GetFollow(c *gin.Context) {
|
||||
uid, ok := currentUID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
targetID, err := pathUID(c)
|
||||
if err != nil {
|
||||
respond.JSONError(c, err)
|
||||
return
|
||||
}
|
||||
out, err := h.friends.GetFollow(c.Request.Context(), uid, targetID)
|
||||
if err != nil {
|
||||
respond.JSONError(c, err)
|
||||
return
|
||||
}
|
||||
respond.JSON(c, out)
|
||||
}
|
||||
|
||||
func (h *Handler) Follow(c *gin.Context) {
|
||||
uid, ok := currentUID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
targetID, err := pathUID(c)
|
||||
if err != nil {
|
||||
respond.JSONError(c, err)
|
||||
return
|
||||
}
|
||||
out, err := h.friends.Follow(c.Request.Context(), uid, targetID)
|
||||
if err != nil {
|
||||
respond.JSONError(c, err)
|
||||
return
|
||||
}
|
||||
respond.JSON(c, out)
|
||||
}
|
||||
|
||||
func (h *Handler) Unfollow(c *gin.Context) {
|
||||
uid, ok := currentUID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
targetID, err := pathUID(c)
|
||||
if err != nil {
|
||||
respond.JSONError(c, err)
|
||||
return
|
||||
}
|
||||
out, err := h.friends.Unfollow(c.Request.Context(), uid, targetID)
|
||||
if err != nil {
|
||||
respond.JSONError(c, err)
|
||||
return
|
||||
}
|
||||
respond.JSON(c, out)
|
||||
}
|
||||
@ -155,6 +155,10 @@ func registerFriendRoutes(v1 *gin.RouterGroup, deps *EngineDeps) {
|
||||
g.PUT("/:uid/block", h.Block)
|
||||
g.DELETE("/:uid/block", h.Unblock)
|
||||
g.POST("/:uid/reports", h.Report)
|
||||
f := v1.Group("/follows")
|
||||
f.GET("/:uid", h.GetFollow)
|
||||
f.PUT("/:uid", h.Follow)
|
||||
f.DELETE("/:uid", h.Unfollow)
|
||||
}
|
||||
|
||||
func registerGroupRoutes(v1 *gin.RouterGroup, deps *EngineDeps) {
|
||||
|
||||
11
migrations/0033_user_follows.sql
Normal file
11
migrations/0033_user_follows.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- One directed follow per pair. Mutual follows become friend edges in logic; unfollowing does not delete messages.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS t_user_follows (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增',
|
||||
user_id BIGINT NOT NULL COMMENT '关注人 user_id',
|
||||
target_id BIGINT NOT NULL COMMENT '被关注人 user_id',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '关注时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_user_target (user_id, target_id),
|
||||
KEY idx_target (target_id)
|
||||
) COMMENT='单向关注。双方互相关注时写入已有双向好友边;取消关注只删关注,不再互关时删好友边,不删消息';
|
||||
Loading…
Reference in New Issue
Block a user