feat: 收藏夹系统 + Studio 趋势图 + 通知偏好设置

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

View File

@ -1,6 +1,7 @@
package service
import (
"encoding/json"
"fmt"
"sort"
@ -8,20 +9,45 @@ import (
"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 消息/通知业务逻辑
@ -56,12 +82,14 @@ func (s *NotificationService) List(userID uint, page, pageSize int) (*model.Noti
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
total, err := s.repo.CountByUser(userID)
excludeTypes := s.getDisabledTypes(userID)
total, err := s.repo.CountByUserExcludeTypes(userID, excludeTypes)
if err != nil {
return nil, err
}
items, err := s.repo.ListByUser(userID, p.Offset(), p.PageSize)
items, err := s.repo.ListByUserExcludeTypes(userID, excludeTypes, p.Offset(), p.PageSize)
if err != nil {
return nil, err
}
@ -198,9 +226,31 @@ func sortNotifications(items []model.Notification) {
})
}
// CountUnread 统计未读消息数
// CountUnread 统计未读消息数(排除用户禁用的通知类型)
func (s *NotificationService) CountUnread(userID uint) (int64, error) {
return s.repo.CountUnread(userID)
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 标记单条消息为已读