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

@ -0,0 +1,157 @@
package repository
import (
"metazone.cc/metalab/internal/model"
"metazone.cc/metalab/internal/service"
"gorm.io/gorm"
)
// FavoriteRepo 收藏夹数据访问
type FavoriteRepo struct {
db *gorm.DB
}
// NewFavoriteRepo 构造函数
func NewFavoriteRepo(db *gorm.DB) *FavoriteRepo {
return &FavoriteRepo{db: db}
}
// WithTx 基于事务连接创建 FavoriteRepo
func (r *FavoriteRepo) WithTx(tx *gorm.DB) service.FavoriteStore {
return &FavoriteRepo{db: tx}
}
// ======================== Folder CRUD ========================
// CreateFolder 创建收藏夹
func (r *FavoriteRepo) CreateFolder(folder *model.Folder) error {
return r.db.Create(folder).Error
}
// FindFolderByID 按 ID 查找收藏夹
func (r *FavoriteRepo) FindFolderByID(id uint) (*model.Folder, error) {
var f model.Folder
err := r.db.First(&f, id).Error
if err != nil {
return nil, err
}
return &f, nil
}
// ListFoldersByUser 列用户的收藏夹(附项目数)
func (r *FavoriteRepo) ListFoldersByUser(userID uint) ([]model.Folder, error) {
var folders []model.Folder
err := r.db.Table("folders").
Select("folders.*, COUNT(folder_items.id) AS item_count").
Joins("LEFT JOIN folder_items ON folder_items.folder_id = folders.id").
Where("folders.user_id = ?", userID).
Group("folders.id").
Order("folders.is_default DESC, folders.created_at ASC").
Scan(&folders).Error
return folders, err
}
// UpdateFolder 更新收藏夹
func (r *FavoriteRepo) UpdateFolder(folder *model.Folder) error {
return r.db.Save(folder).Error
}
// DeleteFolder 删除收藏夹
func (r *FavoriteRepo) DeleteFolder(id uint) error {
return r.db.Where("id = ?", id).Delete(&model.Folder{}).Error
}
// DeleteItemsByFolder 删除收藏夹内所有记录
func (r *FavoriteRepo) DeleteItemsByFolder(folderID uint) error {
return r.db.Where("folder_id = ?", folderID).Delete(&model.FolderItem{}).Error
}
// CountByNameAndUser 检查同用户下名称是否重复(排除自身)
func (r *FavoriteRepo) CountByNameAndUser(userID uint, name string, excludeID uint) (int64, error) {
var count int64
q := r.db.Model(&model.Folder{}).Where("user_id = ? AND name = ?", userID, name)
if excludeID > 0 {
q = q.Where("id != ?", excludeID)
}
err := q.Count(&count).Error
return count, err
}
// HasDefaultFolder 检查用户是否有默认收藏夹
func (r *FavoriteRepo) HasDefaultFolder(userID uint) (bool, error) {
var count int64
err := r.db.Model(&model.Folder{}).Where("user_id = ? AND is_default = ?", userID, true).Count(&count).Error
return count > 0, err
}
// ======================== FolderItem CRUD ========================
// CreateItem 添加收藏
func (r *FavoriteRepo) CreateItem(item *model.FolderItem) error {
return r.db.Create(item).Error
}
// DeleteItem 取消收藏
func (r *FavoriteRepo) DeleteItem(folderID, postID uint) error {
return r.db.Where("folder_id = ? AND post_id = ?", folderID, postID).Delete(&model.FolderItem{}).Error
}
// FindItemByUserAndPost 查找用户对某文章的收藏记录
func (r *FavoriteRepo) FindItemByUserAndPost(userID, postID uint) (*model.FolderItem, error) {
var item model.FolderItem
err := r.db.Where("user_id = ? AND post_id = ?", userID, postID).First(&item).Error
if err != nil {
return nil, err
}
return &item, nil
}
// ListItemsByFolder 列收藏夹内文章分页JOIN posts 获取标题)
func (r *FavoriteRepo) ListItemsByFolder(folderID uint, offset, limit int) ([]model.FolderItem, int64, error) {
var total int64
if err := r.db.Model(&model.FolderItem{}).Where("folder_id = ?", folderID).Count(&total).Error; err != nil {
return nil, 0, err
}
var items []model.FolderItem
err := r.db.Table("folder_items").
Select("folder_items.*, posts.title AS post_title").
Joins("LEFT JOIN posts ON posts.id = folder_items.post_id").
Where("folder_items.folder_id = ?", folderID).
Order("folder_items.created_at DESC").
Offset(offset).Limit(limit).
Scan(&items).Error
return items, total, err
}
// CountItemsByFolder 统计收藏夹内文章数
func (r *FavoriteRepo) CountItemsByFolder(folderID uint) (int64, error) {
var count int64
err := r.db.Model(&model.FolderItem{}).Where("folder_id = ?", folderID).Count(&count).Error
return count, err
}
// ======================== Post 计数器 ========================
// IncrPostFavoritesCount 原子更新文章收藏数
func (r *FavoriteRepo) IncrPostFavoritesCount(postID uint, delta int) error {
return r.db.Model(&model.Post{}).
Where("id = ?", postID).
UpdateColumn("favorites_count", gorm.Expr("favorites_count + ?", delta)).Error
}
// ======================== 趋势聚合 ========================
// AggregateFavoritesByAuthor 按日期聚合收藏量(对于某用户的文章)
func (r *FavoriteRepo) AggregateFavoritesByAuthor(userID uint, since string) ([]service.TrendPoint, error) {
var results []service.TrendPoint
err := r.db.Table("folder_items").
Select("TO_CHAR(folder_items.created_at, 'YYYY-MM-DD') AS date, COUNT(*) AS value").
Joins("JOIN posts ON posts.id = folder_items.post_id").
Where("posts.user_id = ? AND folder_items.created_at >= ?", userID, since).
Group("TO_CHAR(folder_items.created_at, 'YYYY-MM-DD')").
Order("date ASC").
Scan(&results).Error
return results, err
}