Files
mce/internal/service/favorite_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

364 lines
9.9 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 (
"fmt"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/model"
"gorm.io/gorm"
)
// FavoriteStore FavoriteService 所需的最小仓储接口ISP
type FavoriteStore interface {
// Folder
CreateFolder(folder *model.Folder) error
FindFolderByID(id uint) (*model.Folder, error)
ListFoldersByUser(userID uint) ([]model.Folder, error)
UpdateFolder(folder *model.Folder) error
DeleteFolder(id uint) error
DeleteItemsByFolder(folderID uint) error
CountByNameAndUser(userID uint, name string, excludeID uint) (int64, error)
HasDefaultFolder(userID uint) (bool, error)
// FolderItem
CreateItem(item *model.FolderItem) error
DeleteItem(folderID, postID uint) error
FindItemByUserAndPost(userID, postID uint) (*model.FolderItem, error)
ListItemsByFolder(folderID uint, offset, limit int) ([]model.FolderItem, int64, error)
CountItemsByFolder(folderID uint) (int64, error)
// Post counter
IncrPostFavoritesCount(postID uint, delta int) error
// Trend
AggregateFavoritesByAuthor(userID uint, since string) ([]TrendPoint, error)
// Tx
WithTx(tx *gorm.DB) FavoriteStore
}
// FavoriteListResult 收藏夹列表 + 收藏内容
type FavoriteListResult struct {
Folders []model.Folder `json:"folders"`
}
// FolderItemsResult 收藏夹内容
type FolderItemsResult struct {
Items []model.FolderItem `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
TotalPages int `json:"total_pages"`
}
// FavoriteStatus 某文章的收藏状态
type FavoriteStatus struct {
Favorited bool `json:"favorited"`
FolderID *uint `json:"folder_id,omitempty"`
FolderName *string `json:"folder_name,omitempty"`
}
// FavoriteService 收藏夹业务逻辑
type FavoriteService struct {
db *gorm.DB
repo FavoriteStore
}
// NewFavoriteService 构造函数
func NewFavoriteService(db *gorm.DB, repo FavoriteStore) *FavoriteService {
return &FavoriteService{db: db, repo: repo}
}
// ensureDefaultFolder 确保用户有默认收藏夹(不存在则创建)
func (s *FavoriteService) ensureDefaultFolder(userID uint) (*model.Folder, error) {
has, err := s.repo.HasDefaultFolder(userID)
if err != nil {
return nil, fmt.Errorf("查询默认收藏夹: %w", err)
}
if has {
folders, err := s.repo.ListFoldersByUser(userID)
if err != nil {
return nil, fmt.Errorf("查询收藏夹列表: %w", err)
}
for _, f := range folders {
if f.IsDefault {
return &f, nil
}
}
}
// 创建默认收藏夹
f := &model.Folder{
UserID: userID,
Name: "默认",
Description: "自动创建的默认收藏夹",
IsPublic: false,
IsDefault: true,
}
if err := s.repo.CreateFolder(f); err != nil {
return nil, fmt.Errorf("创建默认收藏夹: %w", err)
}
return f, nil
}
// ListFolders 获取用户的所有收藏夹
func (s *FavoriteService) ListFolders(userID uint) (*FavoriteListResult, error) {
folders, err := s.repo.ListFoldersByUser(userID)
if err != nil {
return nil, fmt.Errorf("查询收藏夹: %w", err)
}
if folders == nil {
folders = []model.Folder{}
}
return &FavoriteListResult{Folders: folders}, nil
}
// CreateFolder 创建收藏夹
func (s *FavoriteService) CreateFolder(userID uint, name, description string, isPublic bool) (*model.Folder, error) {
if name == "" {
return nil, fmt.Errorf("收藏夹名称不能为空")
}
if len(name) > 50 {
return nil, fmt.Errorf("收藏夹名称不能超过 50 个字符")
}
// 检查名称是否重复
count, err := s.repo.CountByNameAndUser(userID, name, 0)
if err != nil {
return nil, fmt.Errorf("检查名称: %w", err)
}
if count > 0 {
return nil, fmt.Errorf("收藏夹名称已存在")
}
f := &model.Folder{
UserID: userID,
Name: name,
Description: description,
IsPublic: isPublic,
}
if err := s.repo.CreateFolder(f); err != nil {
return nil, fmt.Errorf("创建收藏夹: %w", err)
}
return f, nil
}
// UpdateFolder 修改收藏夹(默认收藏夹不可改名/改描述)
func (s *FavoriteService) UpdateFolder(userID, folderID uint, name, description string, isPublic bool) error {
f, err := s.repo.FindFolderByID(folderID)
if err != nil {
return common.ErrPostNotFound // 复用错误哨兵
}
if f.UserID != userID {
return common.ErrPermissionDenied
}
if f.IsDefault {
// 默认收藏夹只允许修改公开性
f.IsPublic = isPublic
return s.repo.UpdateFolder(f)
}
if name == "" {
return fmt.Errorf("收藏夹名称不能为空")
}
// 检查名称重复
count, err := s.repo.CountByNameAndUser(userID, name, folderID)
if err != nil {
return fmt.Errorf("检查名称: %w", err)
}
if count > 0 {
return fmt.Errorf("收藏夹名称已存在")
}
f.Name = name
f.Description = description
f.IsPublic = isPublic
return s.repo.UpdateFolder(f)
}
// DeleteFolder 删除收藏夹(默认不可删)
func (s *FavoriteService) DeleteFolder(userID, folderID uint) error {
f, err := s.repo.FindFolderByID(folderID)
if err != nil {
return common.ErrPostNotFound
}
if f.UserID != userID {
return common.ErrPermissionDenied
}
if f.IsDefault {
return fmt.Errorf("默认收藏夹不可删除")
}
return s.db.Transaction(func(tx *gorm.DB) error {
txRepo := s.repo.WithTx(tx)
// 先清理 items 的 fans 计数
items, _, _ := txRepo.ListItemsByFolder(folderID, 0, 10000)
for _, item := range items {
if err := txRepo.IncrPostFavoritesCount(item.PostID, -1); err != nil {
return fmt.Errorf("更新文章收藏数: %w", err)
}
}
if err := txRepo.DeleteItemsByFolder(folderID); err != nil {
return fmt.Errorf("删除收藏内容: %w", err)
}
return txRepo.DeleteFolder(folderID)
})
}
// AddToFolder 将文章加入收藏夹(会先移除该文章在其他收藏夹的收藏)
func (s *FavoriteService) AddToFolder(userID, folderID, postID uint) error {
// 检查收藏夹归属
f, err := s.repo.FindFolderByID(folderID)
if err != nil {
return common.ErrPostNotFound
}
if f.UserID != userID {
return common.ErrPermissionDenied
}
return s.db.Transaction(func(tx *gorm.DB) error {
txRepo := s.repo.WithTx(tx)
// 如果该文章已在其他收藏夹中,先移除
existing, err := txRepo.FindItemByUserAndPost(userID, postID)
if err == nil && existing.FolderID != folderID {
if err := txRepo.DeleteItem(existing.FolderID, postID); err != nil {
return fmt.Errorf("移除旧收藏: %w", err)
}
// 旧收藏夹没有增加计数,所以不需要减
}
// 检查是否已在目标收藏夹中
if err == nil && existing.FolderID == folderID {
return fmt.Errorf("文章已在此收藏夹中")
}
item := &model.FolderItem{
FolderID: folderID,
PostID: postID,
UserID: userID,
}
if err := txRepo.CreateItem(item); err != nil {
return fmt.Errorf("添加收藏: %w", err)
}
return txRepo.IncrPostFavoritesCount(postID, 1)
})
}
// RemoveFromFolder 从收藏夹移除文章
func (s *FavoriteService) RemoveFromFolder(userID, folderID, postID uint) error {
f, err := s.repo.FindFolderByID(folderID)
if err != nil {
return common.ErrPostNotFound
}
if f.UserID != userID {
return common.ErrPermissionDenied
}
return s.db.Transaction(func(tx *gorm.DB) error {
txRepo := s.repo.WithTx(tx)
if err := txRepo.DeleteItem(folderID, postID); err != nil {
return fmt.Errorf("移除收藏: %w", err)
}
return txRepo.IncrPostFavoritesCount(postID, -1)
})
}
// AddFavorite 收藏文章(自动选择或创建默认收藏夹)
func (s *FavoriteService) AddFavorite(userID, postID uint, folderID *uint) error {
var targetFolderID uint
if folderID != nil && *folderID > 0 {
targetFolderID = *folderID
} else {
// 使用或创建默认收藏夹
f, err := s.ensureDefaultFolder(userID)
if err != nil {
return err
}
targetFolderID = f.ID
}
return s.AddToFolder(userID, targetFolderID, postID)
}
// RemoveFavorite 取消收藏文章
func (s *FavoriteService) RemoveFavorite(userID, postID uint) error {
existing, err := s.repo.FindItemByUserAndPost(userID, postID)
if err != nil {
return fmt.Errorf("未收藏该文章")
}
return s.RemoveFromFolder(userID, existing.FolderID, postID)
}
// ListFolderItems 列收藏夹内的文章
func (s *FavoriteService) ListFolderItems(userID, folderID uint, page, pageSize int) (*FolderItemsResult, error) {
f, err := s.repo.FindFolderByID(folderID)
if err != nil {
return nil, common.ErrPostNotFound
}
// 权限检查:非本人 + 非公开 → 拒绝
if f.UserID != userID && !f.IsPublic {
return nil, common.ErrPermissionDenied
}
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
items, total, err := s.repo.ListItemsByFolder(folderID, p.Offset(), p.PageSize)
if err != nil {
return nil, fmt.Errorf("查询收藏内容: %w", err)
}
if items == nil {
items = []model.FolderItem{}
}
return &FolderItemsResult{
Items: items,
Total: total,
Page: p.Page,
TotalPages: common.PageCount(total, p.PageSize),
}, nil
}
// GetPostFavoriteStatus 查询文章收藏状态
func (s *FavoriteService) GetPostFavoriteStatus(userID, postID uint) (*FavoriteStatus, error) {
item, err := s.repo.FindItemByUserAndPost(userID, postID)
if err != nil {
return &FavoriteStatus{Favorited: false}, nil
}
f, err := s.repo.FindFolderByID(item.FolderID)
var folderName *string
if err == nil {
folderName = &f.Name
}
return &FavoriteStatus{
Favorited: true,
FolderID: &item.FolderID,
FolderName: folderName,
}, nil
}
// ToggleFavorite 切换收藏(有则取消,无则加入默认收藏夹)
func (s *FavoriteService) ToggleFavorite(userID, postID uint) (bool, error) {
existing, err := s.repo.FindItemByUserAndPost(userID, postID)
if err == nil {
// 已收藏,取消
if err := s.RemoveFromFolder(userID, existing.FolderID, postID); err != nil {
return false, err
}
return false, nil
}
// 未收藏,加入默认
if err := s.AddFavorite(userID, postID, nil); err != nil {
return false, err
}
return true, nil
}