Files
mce/internal/service/notification_service.go
Victor_Jay 4d212a8f8a feat: 收藏夹系统 + Studio 趋势图 + 通知偏好设置
- 新增收藏夹系统 (Folder/FolderItem 模型、CRUD API、前端管理页)
- 新增文章详情页收藏按钮 (书签图标、ToggleFavorite 一键收藏)
- 新增 Studio 趋势图 (赋能值/评论/点赞/收藏 4 线图, 7d/30d/90d)
- 新增通知偏好设置页 (评论/回复/点赞/关注 4 类开关)
- 后端通知列表支持按偏好过滤 (排除已关闭的通知类型)
- 下载 Chart.js 到本地静态资源 (static/js/chart.umd.min.js)
- 补充收藏夹卡片、开关滑块、趋势图网格等 CSS 样式
2026-06-01 17:33:59 +08:00

332 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"encoding/json"
"fmt"
"sort"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/model"
)
// ParseNotifyPrefs 解析用户通知偏好 JSON 字符串
func ParseNotifyPrefs(raw string) map[string]bool {
prefs := map[string]bool{
model.NotifyComment: true,
model.NotifyCommentReply: true,
model.NotifyLikeAggregated: true,
model.NotifyFollow: true,
}
if raw == "" {
return prefs
}
var parsed map[string]bool
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
return prefs
}
for k, v := range parsed {
prefs[k] = v
}
return prefs
}
// 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)
ListByUserExcludeTypes(userID uint, excludeTypes []string, offset, limit int) ([]model.Notification, error)
CountByUser(userID uint) (int64, error)
CountByUserExcludeTypes(userID uint, excludeTypes []string) (int64, error)
CountByUserAndType(userID uint, notifyType string) (int64, error)
CountUnread(userID uint) (int64, error)
CountUnreadExcludeTypes(userID uint, excludeTypes []string) (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)
GetNotifyPrefs(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()
excludeTypes := s.getDisabledTypes(userID)
total, err := s.repo.CountByUserExcludeTypes(userID, excludeTypes)
if err != nil {
return nil, err
}
items, err := s.repo.ListByUserExcludeTypes(userID, excludeTypes, 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) {
excludeTypes := s.getDisabledTypes(userID)
return s.repo.CountUnreadExcludeTypes(userID, excludeTypes)
}
// getDisabledTypes 获取用户禁用的通知类型列表
func (s *NotificationService) getDisabledTypes(userID uint) []string {
raw, err := s.repo.GetNotifyPrefs(userID)
if err != nil || raw == "" {
return nil
}
prefs := ParseNotifyPrefs(raw)
var disabled []string
for _, t := range []string{
model.NotifyComment,
model.NotifyCommentReply,
model.NotifyLikeAggregated,
model.NotifyFollow,
} {
if !prefs[t] {
disabled = append(disabled, t)
}
}
return disabled
}
// 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
}