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:
270
internal/controller/favorite_controller.go
Normal file
270
internal/controller/favorite_controller.go
Normal file
@ -0,0 +1,270 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
"metazone.cc/metalab/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// favoriteUseCase FavoriteController 对 FavoriteService 的最小依赖(ISP)
|
||||
type favoriteUseCase interface {
|
||||
ListFolders(userID uint) (*service.FavoriteListResult, error)
|
||||
CreateFolder(userID uint, name, description string, isPublic bool) (*model.Folder, error)
|
||||
UpdateFolder(userID, folderID uint, name, description string, isPublic bool) error
|
||||
DeleteFolder(userID, folderID uint) error
|
||||
ListFolderItems(userID, folderID uint, page, pageSize int) (*service.FolderItemsResult, error)
|
||||
AddFavorite(userID, postID uint, folderID *uint) error
|
||||
RemoveFavorite(userID, postID uint) error
|
||||
ToggleFavorite(userID, postID uint) (bool, error)
|
||||
GetPostFavoriteStatus(userID, postID uint) (*service.FavoriteStatus, error)
|
||||
}
|
||||
|
||||
// FavoriteController 收藏夹 API 控制器
|
||||
type FavoriteController struct {
|
||||
svc favoriteUseCase
|
||||
}
|
||||
|
||||
// NewFavoriteController 构造函数
|
||||
func NewFavoriteController(svc favoriteUseCase) *FavoriteController {
|
||||
return &FavoriteController{svc: svc}
|
||||
}
|
||||
|
||||
// ListFolders 我的收藏夹列表 GET /api/folders
|
||||
func (fc *FavoriteController) ListFolders(c *gin.Context) {
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := fc.svc.ListFolders(uid)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "获取收藏夹失败")
|
||||
return
|
||||
}
|
||||
|
||||
common.Ok(c, result)
|
||||
}
|
||||
|
||||
// CreateFolder 创建收藏夹 POST /api/folders
|
||||
func (fc *FavoriteController) CreateFolder(c *gin.Context) {
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
f, err := fc.svc.CreateFolder(uid, req.Name, req.Description, req.IsPublic)
|
||||
if err != nil {
|
||||
if err.Error() == "收藏夹名称已存在" || err.Error() == "收藏夹名称不能为空" || err.Error() == "收藏夹名称不能超过 50 个字符" {
|
||||
common.Error(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
common.Error(c, http.StatusInternalServerError, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
common.OkWithMessage(c, f, "收藏夹创建成功")
|
||||
}
|
||||
|
||||
// UpdateFolder 修改收藏夹 PUT /api/folders/:id
|
||||
func (fc *FavoriteController) UpdateFolder(c *gin.Context) {
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
folderID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的收藏夹 ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := fc.svc.UpdateFolder(uid, uint(folderID), req.Name, req.Description, req.IsPublic); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.OkMessage(c, "收藏夹已更新")
|
||||
}
|
||||
|
||||
// DeleteFolder 删除收藏夹 DELETE /api/folders/:id
|
||||
func (fc *FavoriteController) DeleteFolder(c *gin.Context) {
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
folderID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的收藏夹 ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := fc.svc.DeleteFolder(uid, uint(folderID)); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.OkMessage(c, "收藏夹已删除")
|
||||
}
|
||||
|
||||
// ListFolderItems 获取收藏夹内文章列表 GET /api/folders/:id/posts
|
||||
func (fc *FavoriteController) ListFolderItems(c *gin.Context) {
|
||||
uid, _, _ := common.GetGinUser(c) // 允许未登录查看公开收藏夹
|
||||
|
||||
folderID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的收藏夹 ID")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
result, err := fc.svc.ListFolderItems(uid, uint(folderID), page, pageSize)
|
||||
if err != nil {
|
||||
if err == common.ErrPermissionDenied {
|
||||
common.Error(c, http.StatusForbidden, "该收藏夹未公开")
|
||||
return
|
||||
}
|
||||
common.Error(c, http.StatusInternalServerError, "获取失败")
|
||||
return
|
||||
}
|
||||
|
||||
common.Ok(c, result)
|
||||
}
|
||||
|
||||
// AddToFolder 收藏文章 POST /api/folders/:id/posts
|
||||
func (fc *FavoriteController) AddToFolder(c *gin.Context) {
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
folderID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的收藏夹 ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
PostID uint `json:"post_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.PostID == 0 {
|
||||
common.Error(c, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := fc.svc.AddFavorite(uid, req.PostID, func() *uint { v := uint(folderID); return &v }()); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.OkMessage(c, "收藏成功")
|
||||
}
|
||||
|
||||
// RemoveFromFolder 取消收藏 DELETE /api/folders/:id/posts/:postId
|
||||
func (fc *FavoriteController) RemoveFromFolder(c *gin.Context) {
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
folderID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的收藏夹 ID")
|
||||
return
|
||||
}
|
||||
|
||||
postID, err := strconv.ParseUint(c.Param("postId"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的文章 ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := fc.svc.RemoveFavorite(uid, uint(postID)); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_ = folderID // 移除不依赖具体收藏夹
|
||||
common.OkMessage(c, "已取消收藏")
|
||||
}
|
||||
|
||||
// GetPostStatus 查询文章收藏状态 GET /api/posts/:id/folder-status
|
||||
func (fc *FavoriteController) GetPostStatus(c *gin.Context) {
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Ok(c, gin.H{"favorited": false})
|
||||
return
|
||||
}
|
||||
|
||||
postID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的文章 ID")
|
||||
return
|
||||
}
|
||||
|
||||
status, err := fc.svc.GetPostFavoriteStatus(uid, uint(postID))
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
common.Ok(c, status)
|
||||
}
|
||||
|
||||
// ToggleFavorite 切换收藏 POST /api/posts/:id/favorite
|
||||
func (fc *FavoriteController) ToggleFavorite(c *gin.Context) {
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
postID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的文章 ID")
|
||||
return
|
||||
}
|
||||
|
||||
isFavored, err := fc.svc.ToggleFavorite(uid, uint(postID))
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "操作失败")
|
||||
return
|
||||
}
|
||||
|
||||
common.Ok(c, gin.H{
|
||||
"favorited": isFavored,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user