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:
188
internal/controller/follow_controller.go
Normal file
188
internal/controller/follow_controller.go
Normal 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,
|
||||
}))
|
||||
}
|
||||
@ -111,3 +111,11 @@ type reactionUseCase interface {
|
||||
GetReaction(userID, postID uint) (service.ReactionType, error)
|
||||
GetPostLikesCount(postID uint) (int, error)
|
||||
}
|
||||
|
||||
// followUseCase FollowController 对 FollowService 的最小依赖(ISP:4 个方法)
|
||||
type followUseCase interface {
|
||||
Toggle(followerID, followeeID uint) (bool, error)
|
||||
GetStatus(currentUserID, targetUserID uint) (*service.FollowStatus, error)
|
||||
ListFollowers(userID, currentUserID uint, page, pageSize int) (*service.FollowListResult, error)
|
||||
ListFollowing(userID, currentUserID uint, page, pageSize int) (*service.FollowListResult, error)
|
||||
}
|
||||
|
||||
@ -10,9 +10,10 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// notifProvider MessageController 所需的通知服务接口(ISP:4 个方法)
|
||||
// notifProvider MessageController 所需的通知服务接口(ISP:5 个方法)
|
||||
type notifProvider interface {
|
||||
List(userID uint, page, pageSize int) (*model.NotificationListResult, error)
|
||||
ListByCategory(userID uint, category string, page, pageSize int) (*model.NotificationListResult, error)
|
||||
CountUnread(userID uint) (int64, error)
|
||||
MarkRead(id, userID uint) error
|
||||
MarkAllRead(userID uint) error
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MessagesPage 消息中心页面(需登录,noindex)
|
||||
// MessagesPage 消息中心页面(需登录,noindex,支持分类 TAB)
|
||||
func (mc *MessageController) MessagesPage(c *gin.Context) {
|
||||
uidVal, exists := c.Get("uid")
|
||||
if !exists {
|
||||
@ -19,6 +19,8 @@ func (mc *MessageController) MessagesPage(c *gin.Context) {
|
||||
}
|
||||
uid := uidVal.(uint)
|
||||
|
||||
tab := c.DefaultQuery("tab", "all")
|
||||
|
||||
// 从 query 取分页参数
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
@ -26,7 +28,15 @@ func (mc *MessageController) MessagesPage(c *gin.Context) {
|
||||
}
|
||||
pageSize := 20
|
||||
|
||||
result, err := mc.notifService.List(uid, page, pageSize)
|
||||
var result *model.NotificationListResult
|
||||
var err error
|
||||
|
||||
if tab == "all" {
|
||||
result, err = mc.notifService.List(uid, page, pageSize)
|
||||
} else {
|
||||
result, err = mc.notifService.ListByCategory(uid, tab, page, pageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result = &model.NotificationListResult{Items: []model.Notification{}, Total: 0, Page: 1}
|
||||
}
|
||||
@ -50,5 +60,6 @@ func (mc *MessageController) MessagesPage(c *gin.Context) {
|
||||
"UnreadCount": result.Unread,
|
||||
"NotifyTypeNames": model.NotifyTypeNames,
|
||||
"AuditTypeNames": model.AuditTypeNames,
|
||||
"CurrentTab": tab,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user