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,
|
||||
}))
|
||||
}
|
||||
|
||||
14
internal/model/daily_like_summary.go
Normal file
14
internal/model/daily_like_summary.go
Normal file
@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// DailyLikeSummary 每日点赞汇聚(用于聚合点赞通知)
|
||||
type DailyLikeSummary struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
AuthorUID uint `gorm:"uniqueIndex:idx_author_date;not null" json:"author_uid"` // 被点赞文章的作者
|
||||
Date string `gorm:"type:varchar(10);uniqueIndex:idx_author_date;not null" json:"date"` // 日期 YYYY-MM-DD(Asia/Shanghai)
|
||||
LikerUIDs string `gorm:"type:text" json:"liker_uids"` // 点赞者 UID 列表(追加式,逗号分隔)
|
||||
Notified bool `gorm:"default:false" json:"notified"` // 是否已发送聚合通知
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@ -2,13 +2,15 @@ package model
|
||||
|
||||
// 消息/通知类型常量
|
||||
const (
|
||||
NotifyAuditApproved = "audit_approved"
|
||||
NotifyAuditRejected = "audit_rejected"
|
||||
NotifyPostApproved = "post_approved"
|
||||
NotifyPostRejected = "post_rejected"
|
||||
NotifyLevelUp = "level_up"
|
||||
NotifyComment = "comment"
|
||||
NotifyCommentReply = "comment_reply"
|
||||
NotifyAuditApproved = "audit_approved"
|
||||
NotifyAuditRejected = "audit_rejected"
|
||||
NotifyPostApproved = "post_approved"
|
||||
NotifyPostRejected = "post_rejected"
|
||||
NotifyLevelUp = "level_up"
|
||||
NotifyComment = "comment"
|
||||
NotifyCommentReply = "comment_reply"
|
||||
NotifyLikeAggregated = "like_aggregated" // 每日聚合点赞通知
|
||||
NotifyFollow = "follow" // 关注通知
|
||||
)
|
||||
|
||||
// Notification 消息/通知模型
|
||||
@ -26,13 +28,28 @@ type Notification struct {
|
||||
|
||||
// NotifyTypeNames 通知类型 → 中文名
|
||||
var NotifyTypeNames = map[string]string{
|
||||
NotifyAuditApproved: "资料审核",
|
||||
NotifyAuditRejected: "资料审核",
|
||||
NotifyPostApproved: "稿件审核",
|
||||
NotifyPostRejected: "稿件审核",
|
||||
NotifyLevelUp: "等级提升",
|
||||
NotifyComment: "评论",
|
||||
NotifyCommentReply: "回复",
|
||||
NotifyAuditApproved: "资料审核",
|
||||
NotifyAuditRejected: "资料审核",
|
||||
NotifyPostApproved: "稿件审核",
|
||||
NotifyPostRejected: "稿件审核",
|
||||
NotifyLevelUp: "等级提升",
|
||||
NotifyComment: "评论",
|
||||
NotifyCommentReply: "回复",
|
||||
NotifyLikeAggregated: "点赞",
|
||||
NotifyFollow: "关注",
|
||||
}
|
||||
|
||||
// NotifyCategory 通知类型所属分类 TAB
|
||||
var NotifyCategory = map[string]string{
|
||||
NotifyAuditApproved: "system",
|
||||
NotifyAuditRejected: "system",
|
||||
NotifyPostApproved: "system",
|
||||
NotifyPostRejected: "system",
|
||||
NotifyLevelUp: "system",
|
||||
NotifyComment: "mention",
|
||||
NotifyCommentReply: "mention",
|
||||
NotifyLikeAggregated: "like",
|
||||
NotifyFollow: "follow",
|
||||
}
|
||||
|
||||
// NotificationListResult 消息列表查询结果
|
||||
|
||||
@ -19,6 +19,14 @@ type User struct {
|
||||
Exp int `gorm:"default:0" json:"exp"` // 经验值(只增不减)
|
||||
Energy int `gorm:"default:0" json:"energy"` // 域能余额(可为负数,乘10存储)
|
||||
|
||||
// 关注系统
|
||||
FollowersCount int `gorm:"default:0" json:"followers_count"` // 粉丝数(冗余计数器)
|
||||
FollowingCount int `gorm:"default:0" json:"following_count"` // 关注数(冗余计数器)
|
||||
FollowListPublic bool `gorm:"default:true" json:"follow_list_public"` // 关注/粉丝列表是否公开
|
||||
|
||||
// 通知偏好 (JSON string)
|
||||
NotifyPrefs string `gorm:"type:text;default:'{\"comment\":true,\"comment_reply\":true,\"like_aggregated\":true,\"follow\":true}'" json:"-"`
|
||||
|
||||
// 安全与审计
|
||||
RegIP string `gorm:"type:varchar(45);default:''" json:"-"` // 注册 IP
|
||||
LastLoginIP string `gorm:"type:varchar(45);default:''" json:"-"` // 最后登录 IP
|
||||
|
||||
14
internal/model/user_follow.go
Normal file
14
internal/model/user_follow.go
Normal file
@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// UserFollow 关注关系
|
||||
type UserFollow struct {
|
||||
FollowerID uint `gorm:"primaryKey;not null" json:"follower_id"`
|
||||
FolloweeID uint `gorm:"primaryKey;not null" json:"followee_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// 联表查询填充(非数据库字段)
|
||||
FollowerUsername string `gorm:"-:migration;<-:false;column:follower_username" json:"follower_username,omitempty"`
|
||||
FolloweeUsername string `gorm:"-:migration;<-:false;column:followee_username" json:"followee_username,omitempty"`
|
||||
}
|
||||
112
internal/repository/follow_repo.go
Normal file
112
internal/repository/follow_repo.go
Normal file
@ -0,0 +1,112 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"metazone.cc/metalab/internal/model"
|
||||
"metazone.cc/metalab/internal/service"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// FollowRepo 关注数据访问
|
||||
type FollowRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewFollowRepo 构造函数
|
||||
func NewFollowRepo(db *gorm.DB) *FollowRepo {
|
||||
return &FollowRepo{db: db}
|
||||
}
|
||||
|
||||
// WithTx 基于事务连接创建 FollowRepo
|
||||
func (r *FollowRepo) WithTx(tx *gorm.DB) service.FollowStore {
|
||||
return &FollowRepo{db: tx}
|
||||
}
|
||||
|
||||
// Create 创建关注
|
||||
func (r *FollowRepo) Create(follow *model.UserFollow) error {
|
||||
return r.db.Create(follow).Error
|
||||
}
|
||||
|
||||
// Delete 取消关注
|
||||
func (r *FollowRepo) Delete(followerID, followeeID uint) error {
|
||||
return r.db.Where("follower_id = ? AND followee_id = ?", followerID, followeeID).
|
||||
Delete(&model.UserFollow{}).Error
|
||||
}
|
||||
|
||||
// Exists 检查是否已关注
|
||||
func (r *FollowRepo) Exists(followerID, followeeID uint) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.UserFollow{}).
|
||||
Where("follower_id = ? AND followee_id = ?", followerID, followeeID).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// CountFollowers 粉丝数
|
||||
func (r *FollowRepo) CountFollowers(userID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.UserFollow{}).Where("followee_id = ?", userID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountFollowing 关注数
|
||||
func (r *FollowRepo) CountFollowing(userID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.UserFollow{}).Where("follower_id = ?", userID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// ListFollowers 粉丝列表(分页,JOIN users 获取用户名)
|
||||
func (r *FollowRepo) ListFollowers(userID uint, offset, limit int) ([]model.UserFollow, int64, error) {
|
||||
var total int64
|
||||
if err := r.db.Model(&model.UserFollow{}).Where("followee_id = ?", userID).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var items []model.UserFollow
|
||||
err := r.db.Table("user_follows").
|
||||
Select("user_follows.*, u.username AS follower_username").
|
||||
Joins("INNER JOIN users u ON u.id = user_follows.follower_id").
|
||||
Where("user_follows.followee_id = ?", userID).
|
||||
Order("user_follows.created_at DESC").
|
||||
Offset(offset).Limit(limit).
|
||||
Scan(&items).Error
|
||||
return items, total, err
|
||||
}
|
||||
|
||||
// ListFollowing 关注列表(分页,JOIN users 获取用户名)
|
||||
func (r *FollowRepo) ListFollowing(userID uint, offset, limit int) ([]model.UserFollow, int64, error) {
|
||||
var total int64
|
||||
if err := r.db.Model(&model.UserFollow{}).Where("follower_id = ?", userID).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var items []model.UserFollow
|
||||
err := r.db.Table("user_follows").
|
||||
Select("user_follows.*, u.username AS followee_username").
|
||||
Joins("INNER JOIN users u ON u.id = user_follows.followee_id").
|
||||
Where("user_follows.follower_id = ?", userID).
|
||||
Order("user_follows.created_at DESC").
|
||||
Offset(offset).Limit(limit).
|
||||
Scan(&items).Error
|
||||
return items, total, err
|
||||
}
|
||||
|
||||
// IncrFollowersCount 原子增减粉丝数
|
||||
func (r *FollowRepo) IncrFollowersCount(userID uint, delta int) error {
|
||||
return r.db.Model(&model.User{}).
|
||||
Where("id = ?", userID).
|
||||
UpdateColumn("followers_count", gorm.Expr("followers_count + ?", delta)).Error
|
||||
}
|
||||
|
||||
// IncrFollowingCount 原子增减关注数
|
||||
func (r *FollowRepo) IncrFollowingCount(userID uint, delta int) error {
|
||||
return r.db.Model(&model.User{}).
|
||||
Where("id = ?", userID).
|
||||
UpdateColumn("following_count", gorm.Expr("following_count + ?", delta)).Error
|
||||
}
|
||||
|
||||
// GetFollowListPublic 查询用户关注列表公开性
|
||||
func (r *FollowRepo) GetFollowListPublic(userID uint) (bool, error) {
|
||||
var user model.User
|
||||
err := r.db.Select("follow_list_public").First(&user, userID).Error
|
||||
return user.FollowListPublic, err
|
||||
}
|
||||
@ -60,3 +60,48 @@ func (r *NotificationRepo) MarkAllRead(userID uint) error {
|
||||
Where("user_id = ? AND is_read = false", userID).
|
||||
Update("is_read", true).Error
|
||||
}
|
||||
|
||||
// ListByUserAndType 按类型分页查询用户消息
|
||||
func (r *NotificationRepo) ListByUserAndType(userID uint, notifyType string, offset, limit int) ([]model.Notification, error) {
|
||||
var list []model.Notification
|
||||
err := r.db.Where("user_id = ? AND notify_type = ?", userID, notifyType).
|
||||
Order("created_at DESC").
|
||||
Offset(offset).Limit(limit).
|
||||
Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
// CountByUserAndType 按类型统计用户消息数
|
||||
func (r *NotificationRepo) CountByUserAndType(userID uint, notifyType string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.Notification{}).Where("user_id = ? AND notify_type = ?", userID, notifyType).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountUnreadByType 按类型统计用户未读消息数
|
||||
func (r *NotificationRepo) CountUnreadByType(userID uint, notifyType string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.Notification{}).
|
||||
Where("user_id = ? AND is_read = false AND notify_type = ?", userID, notifyType).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetUnnotifiedDailyLikeSummaries 获取未通知的每日点赞汇总
|
||||
func (r *NotificationRepo) GetUnnotifiedDailyLikeSummaries(userID uint) ([]model.DailyLikeSummary, error) {
|
||||
var summaries []model.DailyLikeSummary
|
||||
err := r.db.Where("author_uid = ? AND notified = ?", userID, false).Find(&summaries).Error
|
||||
return summaries, err
|
||||
}
|
||||
|
||||
// MarkDailyLikeSummaryNotified 标记汇总已通知
|
||||
func (r *NotificationRepo) MarkDailyLikeSummaryNotified(id uint) error {
|
||||
return r.db.Model(&model.DailyLikeSummary{}).Where("id = ?", id).Update("notified", true).Error
|
||||
}
|
||||
|
||||
// GetUsernameByID 根据用户ID获取用户名
|
||||
func (r *NotificationRepo) GetUsernameByID(userID uint) (string, error) {
|
||||
var user model.User
|
||||
err := r.db.Select("username").First(&user, userID).Error
|
||||
return user.Username, err
|
||||
}
|
||||
|
||||
@ -75,3 +75,47 @@ func (r *ReactionRepo) IncrPostDislikes(postID uint, delta int) error {
|
||||
Where("id = ?", postID).
|
||||
UpdateColumn("dislikes_count", gorm.Expr("dislikes_count + ?", delta)).Error
|
||||
}
|
||||
|
||||
// GetPostAuthorID 查询文章作者ID
|
||||
func (r *ReactionRepo) GetPostAuthorID(postID uint) (uint, error) {
|
||||
var post model.Post
|
||||
err := r.db.Select("user_id").First(&post, postID).Error
|
||||
return post.UserID, err
|
||||
}
|
||||
|
||||
// UpsertDailyLikeSummary 追加或创建每日点赞汇总
|
||||
func (r *ReactionRepo) UpsertDailyLikeSummary(authorUID uint, date string, likerUID string) error {
|
||||
var existing model.DailyLikeSummary
|
||||
err := r.db.Where("author_uid = ? AND date = ?", authorUID, date).First(&existing).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return r.db.Create(&model.DailyLikeSummary{
|
||||
AuthorUID: authorUID,
|
||||
Date: date,
|
||||
LikerUIDs: likerUID,
|
||||
Notified: false,
|
||||
}).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 追加 likerUID
|
||||
newUids := existing.LikerUIDs
|
||||
if newUids == "" {
|
||||
newUids = likerUID
|
||||
} else {
|
||||
newUids = newUids + "," + likerUID
|
||||
}
|
||||
return r.db.Model(&existing).Update("liker_uids", newUids).Error
|
||||
}
|
||||
|
||||
// GetUnnotifiedSummaries 查询未通知的每日点赞汇总
|
||||
func (r *ReactionRepo) GetUnnotifiedSummaries(userID uint) ([]model.DailyLikeSummary, error) {
|
||||
var summaries []model.DailyLikeSummary
|
||||
err := r.db.Where("author_uid = ? AND notified = ?", userID, false).Find(&summaries).Error
|
||||
return summaries, err
|
||||
}
|
||||
|
||||
// MarkSummaryNotified 标记汇总已通知
|
||||
func (r *ReactionRepo) MarkSummaryNotified(id uint) error {
|
||||
return r.db.Model(&model.DailyLikeSummary{}).Where("id = ?", id).Update("notified", true).Error
|
||||
}
|
||||
|
||||
@ -131,4 +131,18 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
reactionAPI.POST("/like", d.reactionCtrl.ToggleLike)
|
||||
reactionAPI.POST("/dislike", d.reactionCtrl.ToggleDislike)
|
||||
}
|
||||
|
||||
// --- 关注 API ---
|
||||
// 公开(未登录返回 false)
|
||||
r.GET("/api/users/:uid/follow-status", d.followCtrl.GetStatus)
|
||||
r.GET("/api/users/:uid/followers", d.followCtrl.Followers)
|
||||
r.GET("/api/users/:uid/following", d.followCtrl.Following)
|
||||
// 需登录:切换关注
|
||||
followAPI := r.Group("/api/users/:uid")
|
||||
followAPI.Use(d.authMdw.Required())
|
||||
followAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
followAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
followAPI.POST("/follow", d.followCtrl.Toggle)
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,8 @@ type dependencies struct {
|
||||
commentService *service.CommentService
|
||||
adminCommentCtrl *adminCtrl.AdminCommentController
|
||||
reactionCtrl *controller.ReactionController
|
||||
followCtrl *controller.FollowController
|
||||
followPageCtrl *controller.FollowPageController
|
||||
}
|
||||
|
||||
func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings, redisClient *redis.Client) (
|
||||
|
||||
@ -74,6 +74,13 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
|
||||
reactionService := service.NewReactionService(db, reactionRepo)
|
||||
reactionCtrl := controller.NewReactionController(reactionService)
|
||||
|
||||
// 关注系统
|
||||
followRepo := repository.NewFollowRepo(db)
|
||||
followService := service.NewFollowService(db, followRepo)
|
||||
followService.SetNotifier(notificationService) // 关注通知
|
||||
followCtrl := controller.NewFollowController(followService)
|
||||
followPageCtrl := controller.NewFollowPageController(followService)
|
||||
|
||||
// 用户空间
|
||||
spaceService := service.NewSpaceService(userRepo, postRepo)
|
||||
spaceCtrl := controller.NewSpaceController(spaceService)
|
||||
@ -96,5 +103,7 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
|
||||
commentService: commentService,
|
||||
adminCommentCtrl: adminCommentCtrl,
|
||||
reactionCtrl: reactionCtrl,
|
||||
followCtrl: followCtrl,
|
||||
followPageCtrl: followPageCtrl,
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,10 @@ func setupFrontendRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
pages.GET("/space", d.spaceCtrl.MySpace)
|
||||
pages.GET("/space/:uid", d.spaceCtrl.ShowSpace)
|
||||
|
||||
// 关注/粉丝列表页
|
||||
pages.GET("/space/:uid/followers", d.followPageCtrl.FollowersPage)
|
||||
pages.GET("/space/:uid/following", d.followPageCtrl.FollowingPage)
|
||||
|
||||
// 帖子页面
|
||||
pages.GET("/posts", d.postCtrl.ListPage)
|
||||
pages.GET("/posts/:id", d.postCtrl.ShowPage)
|
||||
|
||||
213
internal/service/follow_service.go
Normal file
213
internal/service/follow_service.go
Normal file
@ -0,0 +1,213 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// FollowStatus 关注关系状态
|
||||
type FollowStatus struct {
|
||||
IFollow bool `json:"i_follow"`
|
||||
TheyFollow bool `json:"they_follow"`
|
||||
}
|
||||
|
||||
// FollowListResult 关注/粉丝列表结果
|
||||
type FollowListResult struct {
|
||||
Items []model.UserFollow `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
ListPublic bool `json:"list_public"`
|
||||
Accessible bool `json:"accessible"`
|
||||
}
|
||||
|
||||
// followNotifier 关注通知接口(ISP)
|
||||
type followNotifier interface {
|
||||
NotifyFollow(followerID, followeeID uint)
|
||||
}
|
||||
|
||||
// FollowService 关注业务逻辑
|
||||
type FollowService struct {
|
||||
db *gorm.DB
|
||||
repo FollowStore
|
||||
notifier followNotifier
|
||||
}
|
||||
|
||||
// NewFollowService 构造函数
|
||||
func NewFollowService(db *gorm.DB, repo FollowStore) *FollowService {
|
||||
return &FollowService{db: db, repo: repo}
|
||||
}
|
||||
|
||||
// SetNotifier 注入通知服务
|
||||
func (s *FollowService) SetNotifier(n followNotifier) {
|
||||
s.notifier = n
|
||||
}
|
||||
|
||||
// Toggle 关注/取消关注,返回操作后的关注状态(true=已关注)
|
||||
func (s *FollowService) Toggle(followerID, followeeID uint) (bool, error) {
|
||||
if followerID == followeeID {
|
||||
return false, fmt.Errorf("不能关注自己")
|
||||
}
|
||||
|
||||
var isFollowing bool
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
txRepo := s.repo.WithTx(tx)
|
||||
|
||||
exists, err := txRepo.Exists(followerID, followeeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询关注状态失败: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
if err := txRepo.Delete(followerID, followeeID); err != nil {
|
||||
return fmt.Errorf("取消关注失败: %w", err)
|
||||
}
|
||||
if err := txRepo.IncrFollowingCount(followerID, -1); err != nil {
|
||||
return fmt.Errorf("更新关注数失败: %w", err)
|
||||
}
|
||||
if err := txRepo.IncrFollowersCount(followeeID, -1); err != nil {
|
||||
return fmt.Errorf("更新粉丝数失败: %w", err)
|
||||
}
|
||||
isFollowing = false
|
||||
} else {
|
||||
follow := &model.UserFollow{
|
||||
FollowerID: followerID,
|
||||
FolloweeID: followeeID,
|
||||
}
|
||||
if err := txRepo.Create(follow); err != nil {
|
||||
return fmt.Errorf("关注失败: %w", err)
|
||||
}
|
||||
if err := txRepo.IncrFollowingCount(followerID, 1); err != nil {
|
||||
return fmt.Errorf("更新关注数失败: %w", err)
|
||||
}
|
||||
if err := txRepo.IncrFollowersCount(followeeID, 1); err != nil {
|
||||
return fmt.Errorf("更新粉丝数失败: %w", err)
|
||||
}
|
||||
isFollowing = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil && isFollowing {
|
||||
s.sendFollowNotification(followerID, followeeID)
|
||||
}
|
||||
|
||||
return isFollowing, err
|
||||
}
|
||||
|
||||
// sendFollowNotification 发送关注通知(非关键路径)
|
||||
func (s *FollowService) sendFollowNotification(followerID, followeeID uint) {
|
||||
if s.notifier != nil && followerID != followeeID {
|
||||
s.notifier.NotifyFollow(followerID, followeeID)
|
||||
}
|
||||
}
|
||||
|
||||
// GetStatus 查询当前用户对目标用户的关注关系
|
||||
func (s *FollowService) GetStatus(currentUserID, targetUserID uint) (*FollowStatus, error) {
|
||||
if currentUserID == 0 {
|
||||
return &FollowStatus{}, nil
|
||||
}
|
||||
|
||||
iFollow, err := s.repo.Exists(currentUserID, targetUserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询我是否关注: %w", err)
|
||||
}
|
||||
|
||||
theyFollow, err := s.repo.Exists(targetUserID, currentUserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询对方是否关注: %w", err)
|
||||
}
|
||||
|
||||
return &FollowStatus{
|
||||
IFollow: iFollow,
|
||||
TheyFollow: theyFollow,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListFollowers 粉丝列表(含隐私控制)
|
||||
func (s *FollowService) ListFollowers(userID, currentUserID uint, page, pageSize int) (*FollowListResult, error) {
|
||||
listPublic, err := s.repo.GetFollowListPublic(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询公开设置: %w", err)
|
||||
}
|
||||
|
||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||
p.DefaultPagination()
|
||||
|
||||
accessible := listPublic || currentUserID == userID
|
||||
|
||||
var items []model.UserFollow
|
||||
var total int64
|
||||
|
||||
if accessible {
|
||||
items, total, err = s.repo.ListFollowers(userID, p.Offset(), p.PageSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询粉丝列表: %w", err)
|
||||
}
|
||||
} else {
|
||||
total, err = s.repo.CountFollowers(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询粉丝数: %w", err)
|
||||
}
|
||||
items = []model.UserFollow{}
|
||||
}
|
||||
|
||||
if items == nil {
|
||||
items = []model.UserFollow{}
|
||||
}
|
||||
|
||||
return &FollowListResult{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: p.Page,
|
||||
TotalPages: common.PageCount(total, p.PageSize),
|
||||
ListPublic: listPublic,
|
||||
Accessible: accessible,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListFollowing 关注列表(含隐私控制)
|
||||
func (s *FollowService) ListFollowing(userID, currentUserID uint, page, pageSize int) (*FollowListResult, error) {
|
||||
listPublic, err := s.repo.GetFollowListPublic(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询公开设置: %w", err)
|
||||
}
|
||||
|
||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||
p.DefaultPagination()
|
||||
|
||||
accessible := listPublic || currentUserID == userID
|
||||
|
||||
var items []model.UserFollow
|
||||
var total int64
|
||||
|
||||
if accessible {
|
||||
items, total, err = s.repo.ListFollowing(userID, p.Offset(), p.PageSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询关注列表: %w", err)
|
||||
}
|
||||
} else {
|
||||
total, err = s.repo.CountFollowing(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询关注数: %w", err)
|
||||
}
|
||||
items = []model.UserFollow{}
|
||||
}
|
||||
|
||||
if items == nil {
|
||||
items = []model.UserFollow{}
|
||||
}
|
||||
|
||||
return &FollowListResult{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: p.Page,
|
||||
TotalPages: common.PageCount(total, p.PageSize),
|
||||
ListPublic: listPublic,
|
||||
Accessible: accessible,
|
||||
}, nil
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
)
|
||||
@ -9,10 +12,16 @@ import (
|
||||
type notifRepo interface {
|
||||
Create(n *model.Notification) error
|
||||
ListByUser(userID uint, offset, limit int) ([]model.Notification, error)
|
||||
ListByUserAndType(userID uint, notifyType string, offset, limit int) ([]model.Notification, error)
|
||||
CountByUser(userID uint) (int64, error)
|
||||
CountByUserAndType(userID uint, notifyType string) (int64, error)
|
||||
CountUnread(userID uint) (int64, error)
|
||||
CountUnreadByType(userID uint, notifyType string) (int64, error)
|
||||
MarkRead(id, userID uint) error
|
||||
MarkAllRead(userID uint) error
|
||||
GetUnnotifiedDailyLikeSummaries(userID uint) ([]model.DailyLikeSummary, error)
|
||||
MarkDailyLikeSummaryNotified(id uint) error
|
||||
GetUsernameByID(userID uint) (string, error)
|
||||
}
|
||||
|
||||
// NotificationService 消息/通知业务逻辑
|
||||
@ -39,8 +48,11 @@ func (s *NotificationService) Create(userID uint, notifyType, title, content str
|
||||
return s.repo.Create(n)
|
||||
}
|
||||
|
||||
// List 分页查询用户消息列表
|
||||
// List 分页查询用户消息列表(触发聚合通知检查)
|
||||
func (s *NotificationService) List(userID uint, page, pageSize int) (*model.NotificationListResult, error) {
|
||||
// 自动触发点赞聚合通知生成
|
||||
s.checkAndNotifyAggregation(userID)
|
||||
|
||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||
p.DefaultPagination()
|
||||
|
||||
@ -69,6 +81,123 @@ func (s *NotificationService) List(userID uint, page, pageSize int) (*model.Noti
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListByCategory 按通知分类分页查询(供消息页分类 TAB 使用)
|
||||
func (s *NotificationService) ListByCategory(userID uint, category string, page, pageSize int) (*model.NotificationListResult, error) {
|
||||
s.checkAndNotifyAggregation(userID)
|
||||
|
||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||
p.DefaultPagination()
|
||||
|
||||
// 分类→通知类型映射
|
||||
notifyTypes := categoryTypes(category)
|
||||
var combinedItems []model.Notification
|
||||
var total int64
|
||||
|
||||
for _, nt := range notifyTypes {
|
||||
c, _ := s.repo.CountByUserAndType(userID, nt)
|
||||
total += c
|
||||
}
|
||||
|
||||
allItems := make([]model.Notification, 0)
|
||||
for _, nt := range notifyTypes {
|
||||
items, err := s.repo.ListByUserAndType(userID, nt, 0, 10000) // 大 limit,在内存里分页
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
allItems = append(allItems, items...)
|
||||
}
|
||||
|
||||
// 按 created_at 降序排序
|
||||
sortNotifications(allItems)
|
||||
|
||||
// 分页
|
||||
start := p.Offset()
|
||||
end := start + p.PageSize
|
||||
if start > len(allItems) {
|
||||
start = len(allItems)
|
||||
}
|
||||
if end > len(allItems) {
|
||||
end = len(allItems)
|
||||
}
|
||||
if start < end {
|
||||
combinedItems = allItems[start:end]
|
||||
}
|
||||
|
||||
if combinedItems == nil {
|
||||
combinedItems = []model.Notification{}
|
||||
}
|
||||
|
||||
totalPages := int((total + int64(p.PageSize) - 1) / int64(p.PageSize))
|
||||
unread, _ := s.CountUnread(userID)
|
||||
|
||||
return &model.NotificationListResult{
|
||||
Items: combinedItems,
|
||||
Total: total,
|
||||
Page: p.Page,
|
||||
TotalPages: totalPages,
|
||||
Unread: unread,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// checkAndNotifyAggregation 检查并生成点赞聚合通知
|
||||
func (s *NotificationService) checkAndNotifyAggregation(userID uint) {
|
||||
summaries, err := s.repo.GetUnnotifiedDailyLikeSummaries(userID)
|
||||
if err != nil || len(summaries) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, sm := range summaries {
|
||||
count := countUIDs(sm.LikerUIDs)
|
||||
var content string
|
||||
if count <= 3 {
|
||||
content = fmt.Sprintf("你的文章被 %d 人点赞了", count)
|
||||
} else {
|
||||
content = fmt.Sprintf("你的文章被 %d 人点赞了", count)
|
||||
}
|
||||
_ = s.Create(userID, model.NotifyLikeAggregated,
|
||||
"点赞汇总通知",
|
||||
content,
|
||||
nil, nil)
|
||||
_ = s.repo.MarkDailyLikeSummaryNotified(sm.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func countUIDs(uids string) int {
|
||||
if uids == "" {
|
||||
return 0
|
||||
}
|
||||
count := 1
|
||||
for _, c := range uids {
|
||||
if c == ',' {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// categoryTypes 分类→通知类型列表
|
||||
func categoryTypes(category string) []string {
|
||||
switch category {
|
||||
case "system":
|
||||
return []string{model.NotifyAuditApproved, model.NotifyAuditRejected, model.NotifyPostApproved, model.NotifyPostRejected, model.NotifyLevelUp}
|
||||
case "mention":
|
||||
return []string{model.NotifyComment, model.NotifyCommentReply}
|
||||
case "like":
|
||||
return []string{model.NotifyLikeAggregated}
|
||||
case "follow":
|
||||
return []string{model.NotifyFollow}
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// sortNotifications 按 CreatedAt 降序排序
|
||||
func sortNotifications(items []model.Notification) {
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].CreatedAt.After(items[j].CreatedAt)
|
||||
})
|
||||
}
|
||||
|
||||
// CountUnread 统计未读消息数
|
||||
func (s *NotificationService) CountUnread(userID uint) (int64, error) {
|
||||
return s.repo.CountUnread(userID)
|
||||
@ -128,3 +257,25 @@ func (s *NotificationService) NotifyPostRejected(userID uint, postTitle, reason
|
||||
}
|
||||
_ = s.Create(userID, model.NotifyPostRejected, title, content, &postID, nil)
|
||||
}
|
||||
|
||||
// NotifyFollow 关注通知(followerID 关注了 followeeID)
|
||||
func (s *NotificationService) NotifyFollow(followerID, followeeID uint) {
|
||||
// 获取关注者用户名
|
||||
followerName := s.getUsername(followerID)
|
||||
if followerName == "" {
|
||||
followerName = "一位用户"
|
||||
}
|
||||
title := "关注通知"
|
||||
content := followerName + " 关注了你"
|
||||
relID := followerID
|
||||
_ = s.Create(followeeID, model.NotifyFollow, title, content, &relID, nil)
|
||||
}
|
||||
|
||||
// getUsername 获取用户名(辅助方法)
|
||||
func (s *NotificationService) getUsername(userID uint) string {
|
||||
name, err := s.repo.GetUsernameByID(userID)
|
||||
if err != nil || name == "" {
|
||||
return "一位用户"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
@ -75,9 +76,29 @@ func (s *ReactionService) ToggleLike(userID, postID uint) (bool, error) {
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil && isLiked {
|
||||
// 新赞 → 记录到每日点赞汇总(非关键路径)
|
||||
s.recordLikeForAggregation(postID, userID)
|
||||
}
|
||||
|
||||
return isLiked, err
|
||||
}
|
||||
|
||||
// recordLikeForAggregation 记录点赞到每日汇总(非关键路径,失败不阻塞)
|
||||
func (s *ReactionService) recordLikeForAggregation(postID uint, likerID uint) {
|
||||
authorID, err := s.repo.GetPostAuthorID(postID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 不记录自己赞自己
|
||||
if authorID == likerID {
|
||||
return
|
||||
}
|
||||
date := time.Now().Format("2006-01-02")
|
||||
likerUID := fmt.Sprintf("%d", likerID)
|
||||
_ = s.repo.UpsertDailyLikeSummary(authorID, date, likerUID)
|
||||
}
|
||||
|
||||
// ToggleDislike 切换踩(含赞互斥逻辑)
|
||||
// 返回:当前是否已踩
|
||||
func (s *ReactionService) ToggleDislike(userID, postID uint) (bool, error) {
|
||||
|
||||
@ -164,5 +164,24 @@ type ReactionStore interface {
|
||||
DeleteDislike(userID, postID uint) error
|
||||
IncrPostLikes(postID uint, delta int) error
|
||||
IncrPostDislikes(postID uint, delta int) error
|
||||
GetPostAuthorID(postID uint) (uint, error)
|
||||
UpsertDailyLikeSummary(authorUID uint, date string, likerUID string) error
|
||||
GetUnnotifiedSummaries(userID uint) ([]model.DailyLikeSummary, error)
|
||||
MarkSummaryNotified(id uint) error
|
||||
WithTx(tx *gorm.DB) ReactionStore
|
||||
}
|
||||
|
||||
// FollowStore FollowService 所需的最小仓储接口(ISP)
|
||||
type FollowStore interface {
|
||||
Create(follow *model.UserFollow) error
|
||||
Delete(followerID, followeeID uint) error
|
||||
Exists(followerID, followeeID uint) (bool, error)
|
||||
CountFollowers(userID uint) (int64, error)
|
||||
CountFollowing(userID uint) (int64, error)
|
||||
ListFollowers(userID uint, offset, limit int) ([]model.UserFollow, int64, error)
|
||||
ListFollowing(userID uint, offset, limit int) ([]model.UserFollow, int64, error)
|
||||
IncrFollowersCount(userID uint, delta int) error
|
||||
IncrFollowingCount(userID uint, delta int) error
|
||||
GetFollowListPublic(userID uint) (bool, error)
|
||||
WithTx(tx *gorm.DB) FollowStore
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user