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

@ -1,6 +1,9 @@
package service
import (
"fmt"
"sort"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/model"
)
@ -9,10 +12,16 @@ import (
type notifRepo interface {
Create(n *model.Notification) error
ListByUser(userID uint, offset, limit int) ([]model.Notification, error)
ListByUserAndType(userID uint, notifyType string, offset, limit int) ([]model.Notification, error)
CountByUser(userID uint) (int64, error)
CountByUserAndType(userID uint, notifyType string) (int64, error)
CountUnread(userID uint) (int64, error)
CountUnreadByType(userID uint, notifyType string) (int64, error)
MarkRead(id, userID uint) error
MarkAllRead(userID uint) error
GetUnnotifiedDailyLikeSummaries(userID uint) ([]model.DailyLikeSummary, error)
MarkDailyLikeSummaryNotified(id uint) error
GetUsernameByID(userID uint) (string, error)
}
// NotificationService 消息/通知业务逻辑
@ -39,8 +48,11 @@ func (s *NotificationService) Create(userID uint, notifyType, title, content str
return s.repo.Create(n)
}
// List 分页查询用户消息列表
// List 分页查询用户消息列表(触发聚合通知检查)
func (s *NotificationService) List(userID uint, page, pageSize int) (*model.NotificationListResult, error) {
// 自动触发点赞聚合通知生成
s.checkAndNotifyAggregation(userID)
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
@ -69,6 +81,123 @@ func (s *NotificationService) List(userID uint, page, pageSize int) (*model.Noti
}, nil
}
// ListByCategory 按通知分类分页查询(供消息页分类 TAB 使用)
func (s *NotificationService) ListByCategory(userID uint, category string, page, pageSize int) (*model.NotificationListResult, error) {
s.checkAndNotifyAggregation(userID)
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
// 分类→通知类型映射
notifyTypes := categoryTypes(category)
var combinedItems []model.Notification
var total int64
for _, nt := range notifyTypes {
c, _ := s.repo.CountByUserAndType(userID, nt)
total += c
}
allItems := make([]model.Notification, 0)
for _, nt := range notifyTypes {
items, err := s.repo.ListByUserAndType(userID, nt, 0, 10000) // 大 limit在内存里分页
if err != nil {
continue
}
allItems = append(allItems, items...)
}
// 按 created_at 降序排序
sortNotifications(allItems)
// 分页
start := p.Offset()
end := start + p.PageSize
if start > len(allItems) {
start = len(allItems)
}
if end > len(allItems) {
end = len(allItems)
}
if start < end {
combinedItems = allItems[start:end]
}
if combinedItems == nil {
combinedItems = []model.Notification{}
}
totalPages := int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
unread, _ := s.CountUnread(userID)
return &model.NotificationListResult{
Items: combinedItems,
Total: total,
Page: p.Page,
TotalPages: totalPages,
Unread: unread,
}, nil
}
// checkAndNotifyAggregation 检查并生成点赞聚合通知
func (s *NotificationService) checkAndNotifyAggregation(userID uint) {
summaries, err := s.repo.GetUnnotifiedDailyLikeSummaries(userID)
if err != nil || len(summaries) == 0 {
return
}
for _, sm := range summaries {
count := countUIDs(sm.LikerUIDs)
var content string
if count <= 3 {
content = fmt.Sprintf("你的文章被 %d 人点赞了", count)
} else {
content = fmt.Sprintf("你的文章被 %d 人点赞了", count)
}
_ = s.Create(userID, model.NotifyLikeAggregated,
"点赞汇总通知",
content,
nil, nil)
_ = s.repo.MarkDailyLikeSummaryNotified(sm.ID)
}
}
func countUIDs(uids string) int {
if uids == "" {
return 0
}
count := 1
for _, c := range uids {
if c == ',' {
count++
}
}
return count
}
// categoryTypes 分类→通知类型列表
func categoryTypes(category string) []string {
switch category {
case "system":
return []string{model.NotifyAuditApproved, model.NotifyAuditRejected, model.NotifyPostApproved, model.NotifyPostRejected, model.NotifyLevelUp}
case "mention":
return []string{model.NotifyComment, model.NotifyCommentReply}
case "like":
return []string{model.NotifyLikeAggregated}
case "follow":
return []string{model.NotifyFollow}
default:
return []string{}
}
}
// sortNotifications 按 CreatedAt 降序排序
func sortNotifications(items []model.Notification) {
sort.Slice(items, func(i, j int) bool {
return items[i].CreatedAt.After(items[j].CreatedAt)
})
}
// CountUnread 统计未读消息数
func (s *NotificationService) CountUnread(userID uint) (int64, error) {
return s.repo.CountUnread(userID)
@ -128,3 +257,25 @@ func (s *NotificationService) NotifyPostRejected(userID uint, postTitle, reason
}
_ = s.Create(userID, model.NotifyPostRejected, title, content, &postID, nil)
}
// NotifyFollow 关注通知followerID 关注了 followeeID
func (s *NotificationService) NotifyFollow(followerID, followeeID uint) {
// 获取关注者用户名
followerName := s.getUsername(followerID)
if followerName == "" {
followerName = "一位用户"
}
title := "关注通知"
content := followerName + " 关注了你"
relID := followerID
_ = s.Create(followeeID, model.NotifyFollow, title, content, &relID, nil)
}
// getUsername 获取用户名(辅助方法)
func (s *NotificationService) getUsername(userID uint) string {
name, err := s.repo.GetUsernameByID(userID)
if err != nil || name == "" {
return "一位用户"
}
return name
}