feat: 实现关注系统 + 通知侧边栏分类 TAB + 点赞聚合通知

- 新增关注系统:UserFollow 模型、FollowService(toggle/status/列表隐私控制)
- User 新增 FollowersCount/FollowingCount/FollowListPublic/NotifyPrefs 字段
- Space 页面增加四态关注按钮(关注/已关注/回关/已互粉)+ 粉丝/关注数链接
- 新增 /space/:uid/followers 和 /space/:uid/following 列表页
- 新增 NotifyFollow/NotifyLikeAggregated 通知类型
- ReactionService 点赞时写入 daily_like_summary,访问时生成聚合通知
- FollowService 关注时触发 NotifyFollow 通知
- 消息中心侧边栏升级为分类 TAB(全部/系统通知/@艾特/点赞/关注)
- NotificationService 新增 ListByCategory 按分类分页查询
This commit is contained in:
2026-06-01 16:30:46 +08:00
parent 2b47380ac5
commit 680df7371a
26 changed files with 1251 additions and 23 deletions

View File

@ -0,0 +1,213 @@
package service
import (
"fmt"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/model"
"gorm.io/gorm"
)
// FollowStatus 关注关系状态
type FollowStatus struct {
IFollow bool `json:"i_follow"`
TheyFollow bool `json:"they_follow"`
}
// FollowListResult 关注/粉丝列表结果
type FollowListResult struct {
Items []model.UserFollow `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
TotalPages int `json:"total_pages"`
ListPublic bool `json:"list_public"`
Accessible bool `json:"accessible"`
}
// followNotifier 关注通知接口ISP
type followNotifier interface {
NotifyFollow(followerID, followeeID uint)
}
// FollowService 关注业务逻辑
type FollowService struct {
db *gorm.DB
repo FollowStore
notifier followNotifier
}
// NewFollowService 构造函数
func NewFollowService(db *gorm.DB, repo FollowStore) *FollowService {
return &FollowService{db: db, repo: repo}
}
// SetNotifier 注入通知服务
func (s *FollowService) SetNotifier(n followNotifier) {
s.notifier = n
}
// Toggle 关注/取消关注返回操作后的关注状态true=已关注)
func (s *FollowService) Toggle(followerID, followeeID uint) (bool, error) {
if followerID == followeeID {
return false, fmt.Errorf("不能关注自己")
}
var isFollowing bool
err := s.db.Transaction(func(tx *gorm.DB) error {
txRepo := s.repo.WithTx(tx)
exists, err := txRepo.Exists(followerID, followeeID)
if err != nil {
return fmt.Errorf("查询关注状态失败: %w", err)
}
if exists {
if err := txRepo.Delete(followerID, followeeID); err != nil {
return fmt.Errorf("取消关注失败: %w", err)
}
if err := txRepo.IncrFollowingCount(followerID, -1); err != nil {
return fmt.Errorf("更新关注数失败: %w", err)
}
if err := txRepo.IncrFollowersCount(followeeID, -1); err != nil {
return fmt.Errorf("更新粉丝数失败: %w", err)
}
isFollowing = false
} else {
follow := &model.UserFollow{
FollowerID: followerID,
FolloweeID: followeeID,
}
if err := txRepo.Create(follow); err != nil {
return fmt.Errorf("关注失败: %w", err)
}
if err := txRepo.IncrFollowingCount(followerID, 1); err != nil {
return fmt.Errorf("更新关注数失败: %w", err)
}
if err := txRepo.IncrFollowersCount(followeeID, 1); err != nil {
return fmt.Errorf("更新粉丝数失败: %w", err)
}
isFollowing = true
}
return nil
})
if err == nil && isFollowing {
s.sendFollowNotification(followerID, followeeID)
}
return isFollowing, err
}
// sendFollowNotification 发送关注通知(非关键路径)
func (s *FollowService) sendFollowNotification(followerID, followeeID uint) {
if s.notifier != nil && followerID != followeeID {
s.notifier.NotifyFollow(followerID, followeeID)
}
}
// GetStatus 查询当前用户对目标用户的关注关系
func (s *FollowService) GetStatus(currentUserID, targetUserID uint) (*FollowStatus, error) {
if currentUserID == 0 {
return &FollowStatus{}, nil
}
iFollow, err := s.repo.Exists(currentUserID, targetUserID)
if err != nil {
return nil, fmt.Errorf("查询我是否关注: %w", err)
}
theyFollow, err := s.repo.Exists(targetUserID, currentUserID)
if err != nil {
return nil, fmt.Errorf("查询对方是否关注: %w", err)
}
return &FollowStatus{
IFollow: iFollow,
TheyFollow: theyFollow,
}, nil
}
// ListFollowers 粉丝列表(含隐私控制)
func (s *FollowService) ListFollowers(userID, currentUserID uint, page, pageSize int) (*FollowListResult, error) {
listPublic, err := s.repo.GetFollowListPublic(userID)
if err != nil {
return nil, fmt.Errorf("查询公开设置: %w", err)
}
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
accessible := listPublic || currentUserID == userID
var items []model.UserFollow
var total int64
if accessible {
items, total, err = s.repo.ListFollowers(userID, p.Offset(), p.PageSize)
if err != nil {
return nil, fmt.Errorf("查询粉丝列表: %w", err)
}
} else {
total, err = s.repo.CountFollowers(userID)
if err != nil {
return nil, fmt.Errorf("查询粉丝数: %w", err)
}
items = []model.UserFollow{}
}
if items == nil {
items = []model.UserFollow{}
}
return &FollowListResult{
Items: items,
Total: total,
Page: p.Page,
TotalPages: common.PageCount(total, p.PageSize),
ListPublic: listPublic,
Accessible: accessible,
}, nil
}
// ListFollowing 关注列表(含隐私控制)
func (s *FollowService) ListFollowing(userID, currentUserID uint, page, pageSize int) (*FollowListResult, error) {
listPublic, err := s.repo.GetFollowListPublic(userID)
if err != nil {
return nil, fmt.Errorf("查询公开设置: %w", err)
}
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
accessible := listPublic || currentUserID == userID
var items []model.UserFollow
var total int64
if accessible {
items, total, err = s.repo.ListFollowing(userID, p.Offset(), p.PageSize)
if err != nil {
return nil, fmt.Errorf("查询关注列表: %w", err)
}
} else {
total, err = s.repo.CountFollowing(userID)
if err != nil {
return nil, fmt.Errorf("查询关注数: %w", err)
}
items = []model.UserFollow{}
}
if items == nil {
items = []model.UserFollow{}
}
return &FollowListResult{
Items: items,
Total: total,
Page: p.Page,
TotalPages: common.PageCount(total, p.PageSize),
ListPublic: listPublic,
Accessible: accessible,
}, nil
}