- 新增关注系统: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 按分类分页查询
214 lines
5.5 KiB
Go
214 lines
5.5 KiB
Go
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
|
||
}
|