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:
@ -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 标记单条消息为已读
|
||||
|
||||
Reference in New Issue
Block a user