- 新增关注系统: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 按分类分页查询
282 lines
7.8 KiB
Go
282 lines
7.8 KiB
Go
package service
|
||
|
||
import (
|
||
"fmt"
|
||
"sort"
|
||
|
||
"metazone.cc/metalab/internal/common"
|
||
"metazone.cc/metalab/internal/model"
|
||
)
|
||
|
||
// notifRepo 通知服务所需的最小仓储接口(ISP)
|
||
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 消息/通知业务逻辑
|
||
type NotificationService struct {
|
||
repo notifRepo
|
||
}
|
||
|
||
// NewNotificationService 构造函数
|
||
func NewNotificationService(repo notifRepo) *NotificationService {
|
||
return &NotificationService{repo: repo}
|
||
}
|
||
|
||
// Create 创建消息
|
||
func (s *NotificationService) Create(userID uint, notifyType, title, content string, relatedID, commentID *uint) error {
|
||
n := &model.Notification{
|
||
UserID: userID,
|
||
NotifyType: notifyType,
|
||
Title: title,
|
||
Content: content,
|
||
IsRead: false,
|
||
RelatedID: relatedID,
|
||
CommentID: commentID,
|
||
}
|
||
return s.repo.Create(n)
|
||
}
|
||
|
||
// 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()
|
||
|
||
total, err := s.repo.CountByUser(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
items, err := s.repo.ListByUser(userID, p.Offset(), p.PageSize)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if items == nil {
|
||
items = []model.Notification{}
|
||
}
|
||
|
||
totalPages := int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
|
||
unread, _ := s.CountUnread(userID)
|
||
|
||
return &model.NotificationListResult{
|
||
Items: items,
|
||
Total: total,
|
||
Page: p.Page,
|
||
TotalPages: totalPages,
|
||
Unread: unread,
|
||
}, 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)
|
||
}
|
||
|
||
// MarkRead 标记单条消息为已读
|
||
func (s *NotificationService) MarkRead(id, userID uint) error {
|
||
return s.repo.MarkRead(id, userID)
|
||
}
|
||
|
||
// MarkAllRead 标记所有未读为已读
|
||
func (s *NotificationService) MarkAllRead(userID uint) error {
|
||
return s.repo.MarkAllRead(userID)
|
||
}
|
||
|
||
// NotifyAuditApproved 审核通过通知
|
||
func (s *NotificationService) NotifyAuditApproved(userID uint, auditType string, relatedID uint) {
|
||
typeName := model.AuditTypeNames[auditType]
|
||
if typeName == "" {
|
||
typeName = auditType
|
||
}
|
||
_ = s.Create(userID, model.NotifyAuditApproved,
|
||
"审核已通过",
|
||
"你的"+typeName+"修改已通过审核",
|
||
&relatedID, nil)
|
||
}
|
||
|
||
// NotifyAuditRejected 审核拒绝通知
|
||
func (s *NotificationService) NotifyAuditRejected(userID uint, auditType, reason string, relatedID uint) {
|
||
typeName := model.AuditTypeNames[auditType]
|
||
if typeName == "" {
|
||
typeName = auditType
|
||
}
|
||
content := "你的" + typeName + "修改被拒绝"
|
||
if reason != "" {
|
||
content += ",理由:" + reason
|
||
}
|
||
_ = s.Create(userID, model.NotifyAuditRejected,
|
||
"审核未通过",
|
||
content,
|
||
&relatedID, nil)
|
||
}
|
||
|
||
// NotifyPostApproved 稿件审核通过通知
|
||
func (s *NotificationService) NotifyPostApproved(userID uint, postTitle string, postID uint) {
|
||
title := "稿件审核通过"
|
||
content := "你的稿件《" + postTitle + "》已通过审核,现已公开发布"
|
||
_ = s.Create(userID, model.NotifyPostApproved, title, content, &postID, nil)
|
||
}
|
||
|
||
// NotifyPostRejected 稿件审核拒绝通知
|
||
func (s *NotificationService) NotifyPostRejected(userID uint, postTitle, reason string, postID uint) {
|
||
title := "稿件审核未通过"
|
||
content := "你的稿件《" + postTitle + "》未通过审核"
|
||
if reason != "" {
|
||
content += ",理由:" + 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
|
||
}
|