feat: 实现关注系统 + 通知侧边栏分类 TAB + 点赞聚合通知

- 新增关注系统:UserFollow 模型、FollowService(toggle/status/列表隐私控制)
- User 新增 FollowersCount/FollowingCount/FollowListPublic/NotifyPrefs 字段
- Space 页面增加四态关注按钮(关注/已关注/回关/已互粉)+ 粉丝/关注数链接
- 新增 /space/:uid/followers 和 /space/:uid/following 列表页
- 新增 NotifyFollow/NotifyLikeAggregated 通知类型
- ReactionService 点赞时写入 daily_like_summary,访问时生成聚合通知
- FollowService 关注时触发 NotifyFollow 通知
- 消息中心侧边栏升级为分类 TAB(全部/系统通知/@艾特/点赞/关注)
- NotificationService 新增 ListByCategory 按分类分页查询
This commit is contained in:
2026-06-01 16:30:46 +08:00
parent 2b47380ac5
commit 680df7371a
26 changed files with 1251 additions and 23 deletions

View File

@ -0,0 +1,188 @@
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"
)
// FollowController 关注 API 控制器
type FollowController struct {
followSvc followUseCase
}
// NewFollowController 构造函数
func NewFollowController(followSvc followUseCase) *FollowController {
return &FollowController{followSvc: followSvc}
}
// Toggle 切换关注 POST /api/users/:uid/follow
func (fc *FollowController) Toggle(c *gin.Context) {
uid, _, ok := common.GetGinUser(c)
if !ok {
common.Error(c, http.StatusUnauthorized, "请先登录")
return
}
targetUID, err := strconv.ParseUint(c.Param("uid"), 10, 64)
if err != nil {
common.Error(c, http.StatusBadRequest, "无效的用户ID")
return
}
isFollowing, err := fc.followSvc.Toggle(uid, uint(targetUID))
if err != nil {
if err.Error() == "不能关注自己" {
common.Error(c, http.StatusBadRequest, err.Error())
return
}
common.Error(c, http.StatusInternalServerError, "操作失败")
return
}
// 获取最新状态
status, _ := fc.followSvc.GetStatus(uid, uint(targetUID))
common.Ok(c, gin.H{
"is_following": isFollowing,
"status": status,
})
}
// GetStatus 查询关注关系 GET /api/users/:uid/follow-status
func (fc *FollowController) GetStatus(c *gin.Context) {
targetUID, err := strconv.ParseUint(c.Param("uid"), 10, 64)
if err != nil {
common.Error(c, http.StatusBadRequest, "无效的用户ID")
return
}
uid, _, _ := common.GetGinUser(c) // 允许未登录(返回全是 false
status, err := fc.followSvc.GetStatus(uid, uint(targetUID))
if err != nil {
common.Error(c, http.StatusInternalServerError, "查询失败")
return
}
common.Ok(c, status)
}
// Followers 粉丝列表 GET /api/users/:uid/followers
func (fc *FollowController) Followers(c *gin.Context) {
targetUID, err := strconv.ParseUint(c.Param("uid"), 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"))
uid, _, _ := common.GetGinUser(c) // 允许未登录
result, err := fc.followSvc.ListFollowers(uint(targetUID), uid, page, pageSize)
if err != nil {
common.Error(c, http.StatusInternalServerError, "查询失败")
return
}
common.Ok(c, result)
}
// Following 关注列表 GET /api/users/:uid/following
func (fc *FollowController) Following(c *gin.Context) {
targetUID, err := strconv.ParseUint(c.Param("uid"), 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"))
uid, _, _ := common.GetGinUser(c)
result, err := fc.followSvc.ListFollowing(uint(targetUID), uid, page, pageSize)
if err != nil {
common.Error(c, http.StatusInternalServerError, "查询失败")
return
}
common.Ok(c, result)
}
// FollowPageController 关注/粉丝页面控制器
type FollowPageController struct {
followSvc followUseCase
}
// NewFollowPageController 构造函数
func NewFollowPageController(followSvc followUseCase) *FollowPageController {
return &FollowPageController{followSvc: followSvc}
}
// FollowersPage 粉丝列表页面
func (fpc *FollowPageController) FollowersPage(c *gin.Context) {
targetUID, err := strconv.ParseUint(c.Param("uid"), 10, 64)
if err != nil {
common.Error(c, http.StatusBadRequest, "无效的用户ID")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
uid, _, _ := common.GetGinUser(c)
result, err := fpc.followSvc.ListFollowers(uint(targetUID), uid, page, 20)
if err != nil {
result = &service.FollowListResult{Items: []model.UserFollow{}, Total: 0}
}
c.HTML(http.StatusOK, "follow/followers.html", common.BuildPageData(c, gin.H{
"Title": "粉丝列表",
"ExtraCSS": "/static/css/follow.css",
"Result": result,
"TargetUID": targetUID,
"Page": result.Page,
"TotalPages": result.TotalPages,
"HasPrev": result.Page > 1,
"HasNext": result.Page < result.TotalPages,
"PrevPage": result.Page - 1,
"NextPage": result.Page + 1,
}))
}
// FollowingPage 关注列表页面
func (fpc *FollowPageController) FollowingPage(c *gin.Context) {
targetUID, err := strconv.ParseUint(c.Param("uid"), 10, 64)
if err != nil {
common.Error(c, http.StatusBadRequest, "无效的用户ID")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
uid, _, _ := common.GetGinUser(c)
result, err := fpc.followSvc.ListFollowing(uint(targetUID), uid, page, 20)
if err != nil {
result = &service.FollowListResult{Items: []model.UserFollow{}, Total: 0}
}
c.HTML(http.StatusOK, "follow/following.html", common.BuildPageData(c, gin.H{
"Title": "关注列表",
"ExtraCSS": "/static/css/follow.css",
"Result": result,
"TargetUID": targetUID,
"Page": result.Page,
"TotalPages": result.TotalPages,
"HasPrev": result.Page > 1,
"HasNext": result.Page < result.TotalPages,
"PrevPage": result.Page - 1,
"NextPage": result.Page + 1,
}))
}