refactor(comment): unify status transitions with atomic count maintenance
Consolidate comment status changes through transitionStatus() to ensure posts.comments_count and comments.replies_count are atomically maintained within the same transaction, preventing inconsistencies between displayed and stored counts. - Remove separate ApplyPublishedStats() calls, now handled in UpdateModerationStatus - Add cache invalidation for admin moderation operations - Update report auto-hide to use unified status transition
This commit is contained in:
@@ -127,7 +127,7 @@ func InitializeApp() (*App, error) {
|
||||
adminUserHandler := handler.NewAdminUserHandler(adminUserService, pushService)
|
||||
adminPostService := wire.ProvideAdminPostService(postRepository, cache, logService)
|
||||
adminPostHandler := handler.NewAdminPostHandler(adminPostService)
|
||||
adminCommentService := wire.ProvideAdminCommentService(commentRepository, postRepository)
|
||||
adminCommentService := wire.ProvideAdminCommentService(commentRepository, postRepository, cache)
|
||||
adminCommentHandler := handler.NewAdminCommentHandler(adminCommentService)
|
||||
adminGroupService := wire.ProvideAdminGroupService(groupRepository, userRepository)
|
||||
adminGroupHandler := handler.NewAdminGroupHandler(adminGroupService)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"with_you/internal/model"
|
||||
"with_you/internal/pkg/cursor"
|
||||
"context"
|
||||
@@ -13,9 +15,8 @@ type CommentRepository interface {
|
||||
Create(comment *model.Comment) error
|
||||
GetByID(id string) (*model.Comment, error)
|
||||
Update(comment *model.Comment) error
|
||||
UpdateModerationStatus(commentID string, status model.CommentStatus, rejectReason string, reviewedBy string) error
|
||||
UpdateModerationStatus(commentID string, status model.CommentStatus, rejectReason string, reviewedBy string) (*model.Comment, error)
|
||||
Delete(id string) error
|
||||
ApplyPublishedStats(comment *model.Comment) error
|
||||
GetByPostID(postID string, page, pageSize int) ([]*model.Comment, int64, error)
|
||||
GetByPostIDWithReplies(postID string, page, pageSize, replyLimit int) ([]*model.Comment, int64, error)
|
||||
GetRepliesByRootID(rootID string, page, pageSize int) ([]*model.Comment, int64, error)
|
||||
@@ -57,31 +58,72 @@ func (r *commentRepository) GetByID(id string) (*model.Comment, error) {
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
// Update 更新评论
|
||||
// Update 更新评论(仅内容字段,如 content / segments / images)。
|
||||
//
|
||||
// 防御性:评论的 status 字段是计数维护的核心,必须经由 UpdateModerationStatus /
|
||||
// Delete 等状态转换通道变更,否则会破坏 comments_count / replies_count 一致性。
|
||||
// 因此本方法显式不更新 status / reviewed_* 等审核字段——即便上层传入的
|
||||
// comment 结构体 status 为零值或脏值,也不会污染状态机。
|
||||
func (r *commentRepository) Update(comment *model.Comment) error {
|
||||
return r.db.Save(comment).Error
|
||||
return r.db.Model(&model.Comment{}).
|
||||
Where("id = ?", comment.ID).
|
||||
Select("content", "segments", "images", "updated_at").
|
||||
Updates(comment).Error
|
||||
}
|
||||
|
||||
// UpdateModerationStatus 更新评论审核状态
|
||||
func (r *commentRepository) UpdateModerationStatus(commentID string, status model.CommentStatus, rejectReason string, reviewedBy string) error {
|
||||
// UpdateModerationStatus 更新评论审核状态,并在同一事务内原子地维护
|
||||
// 帖子评论数 / 父评论回复数等统计字段。
|
||||
//
|
||||
// 计数维护统一收敛到 transitionStatus(评论状态变更的唯一通道),
|
||||
// 因此任何调用方(自动审核、管理端单条 / 批量审核、举报自动隐藏等)
|
||||
// 都能正确维护计数,避免"评论显示 N 条但计数为 N-1"。
|
||||
// 同时内置防御:非法状态转换(如 deleted → published)会被拒绝,
|
||||
// 计数递减使用 CASE WHEN 保证永不为负。
|
||||
//
|
||||
// 返回更新后的评论(含 User 关联),便于调用方做缓存失效、通知等后续处理。
|
||||
func (r *commentRepository) UpdateModerationStatus(commentID string, status model.CommentStatus, rejectReason string, reviewedBy string) (*model.Comment, error) {
|
||||
var updated model.Comment
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
prev, err := transitionStatus(tx, commentID, status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 状态未变化或仅元数据需补充(reviewed_at / reviewed_by / reject_reason)
|
||||
updates := map[string]any{
|
||||
"status": status,
|
||||
"reviewed_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
"reviewed_by": reviewedBy,
|
||||
"reject_reason": rejectReason,
|
||||
"updated_at": gorm.Expr("updated_at"),
|
||||
}
|
||||
return r.db.Model(&model.Comment{}).
|
||||
if prev.Status != status {
|
||||
updates["status"] = status
|
||||
}
|
||||
if err := tx.Model(&model.Comment{}).
|
||||
Where("id = ?", commentID).
|
||||
UpdateColumns(updates).Error
|
||||
UpdateColumns(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete 删除评论(软删除,同时清理关联数据)
|
||||
// 重新加载(含 User 关联)供调用方使用
|
||||
return tx.Preload("User").First(&updated, "id = ?", commentID).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &updated, nil
|
||||
}
|
||||
|
||||
// Delete 删除评论(软删除,同时清理关联数据)。
|
||||
// 删除 = 状态迁移到 deleted,复用 transitionStatus 统一维护计数,
|
||||
// 保证 Delete 与审核路径的计数行为一致、对称。
|
||||
func (r *commentRepository) Delete(id string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 先查询评论获取post_id和parent_id
|
||||
var comment model.Comment
|
||||
if err := tx.First(&comment, "id = ?", id).Error; err != nil {
|
||||
// 统一走状态转换层:published → deleted 会自动 -1 计数;非 published 删除则不调整
|
||||
if _, err := transitionStatus(tx, id, model.CommentStatusDeleted); err != nil {
|
||||
if errors.Is(err, errInvalidStatusTransition) {
|
||||
// 已删除的评论重复删除:视为幂等成功
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -95,54 +137,6 @@ func (r *commentRepository) Delete(id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 仅已发布评论才参与统计,避免 pending/rejected 影响计数
|
||||
if comment.Status == model.CommentStatusPublished {
|
||||
// 减少帖子的评论数并同步热度分
|
||||
// 评论数属于统计字段,不应影响帖子内容更新时间(updated_at)
|
||||
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
|
||||
UpdateColumns(map[string]any{
|
||||
"comments_count": gorm.Expr("comments_count - 1"),
|
||||
"updated_at": gorm.Expr("updated_at"),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是回复,减少父评论的回复数
|
||||
if comment.ParentID != nil && *comment.ParentID != "" {
|
||||
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
|
||||
UpdateColumn("replies_count", gorm.Expr("replies_count - 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ApplyPublishedStats 在评论审核通过后更新帖子评论数/回复数
|
||||
func (r *commentRepository) ApplyPublishedStats(comment *model.Comment) error {
|
||||
if comment == nil {
|
||||
return nil
|
||||
}
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 增加帖子的评论数并同步热度分
|
||||
// 评论数属于统计字段,不应影响帖子内容更新时间(updated_at)
|
||||
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
|
||||
UpdateColumns(map[string]any{
|
||||
"comments_count": gorm.Expr("comments_count + 1"),
|
||||
"updated_at": gorm.Expr("updated_at"),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是回复,增加父评论的回复数
|
||||
if comment.ParentID != nil && *comment.ParentID != "" {
|
||||
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
|
||||
UpdateColumn("replies_count", gorm.Expr("replies_count + 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -413,24 +407,26 @@ func (r *commentRepository) GetAdminCommentByID(id string) (*model.Comment, erro
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除评论(使用单次事务批量删除,避免 N+1 问题)
|
||||
// BatchDelete 批量删除评论。
|
||||
// 逐条复用 transitionStatus 完成 published → deleted 的状态迁移,
|
||||
// 统一维护 comments_count 与父评论 replies_count(修复此前批量删除漏减 replies_count 的问题)。
|
||||
// 返回失败 ID 列表(当前实现下事务整体回滚,故失败时返回全部 ids)。
|
||||
func (r *commentRepository) BatchDelete(ids []string) ([]string, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 先查询所有评论获取 post_id 和状态信息
|
||||
type commentInfo struct {
|
||||
ID string
|
||||
PostID string
|
||||
Status model.CommentStatus
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, id := range ids {
|
||||
// 统一走状态转换层:幂等处理已删除评论,已发布评论自动 -1 计数 + 父回复数
|
||||
if _, terr := transitionStatus(tx, id, model.CommentStatusDeleted); terr != nil {
|
||||
if errors.Is(terr, errInvalidStatusTransition) {
|
||||
continue // 已删除:幂等跳过
|
||||
}
|
||||
return terr
|
||||
}
|
||||
var comments []commentInfo
|
||||
if err := r.db.Model(&model.Comment{}).Where("id IN ?", ids).Scan(&comments).Error; err != nil {
|
||||
return ids, err
|
||||
}
|
||||
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 批量删除评论点赞记录
|
||||
if err := tx.Where("comment_id IN ?", ids).Delete(&model.CommentLike{}).Error; err != nil {
|
||||
return err
|
||||
@@ -440,24 +436,6 @@ func (r *commentRepository) BatchDelete(ids []string) ([]string, error) {
|
||||
if err := tx.Where("id IN ?", ids).Delete(&model.Comment{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新帖子的评论数(仅统计已发布评论)
|
||||
postCountMap := make(map[string]int)
|
||||
for _, c := range comments {
|
||||
if c.Status == model.CommentStatusPublished {
|
||||
postCountMap[c.PostID]++
|
||||
}
|
||||
}
|
||||
for postID, count := range postCountMap {
|
||||
if err := tx.Model(&model.Post{}).Where("id = ?", postID).
|
||||
UpdateColumns(map[string]any{
|
||||
"comments_count": gorm.Expr("comments_count - ?", count),
|
||||
"updated_at": gorm.Expr("updated_at"),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
155
internal/repository/comment_status.go
Normal file
155
internal/repository/comment_status.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"with_you/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// clauseLockUpdate 行级锁子句,供 transitionStatus 加锁读取使用。
|
||||
// 集中定义便于在评论仓储内复用。
|
||||
var clauseLockUpdate = clause.Locking{Strength: "UPDATE"}
|
||||
|
||||
// 评论状态机与计数维护的统一入口。
|
||||
//
|
||||
// 设计目标:
|
||||
// 把"评论状态变更"与"聚合计数维护 (posts.comments_count / comments.replies_count)"
|
||||
// 强绑定到同一个事务内,作为唯一的状态变更通道。任何外部路径
|
||||
// (自动审核、管理端审核、批量审核、举报自动隐藏、删除等)都必须经由
|
||||
// UpdateModerationStatus / Delete / BatchDelete 走到这里,从而杜绝
|
||||
// "调用方忘记维护计数"导致的计数与显示不一致问题(如显示2条但计数为1)。
|
||||
//
|
||||
// 防御性:
|
||||
// 1. SELECT ... FOR UPDATE 加锁读旧状态,避免并发审核造成计数错乱;
|
||||
// 2. 非法状态转换(如 deleted → published)直接拒绝,从源头防止脏数据再次污染计数;
|
||||
// 3. 计数递减使用 CASE WHEN 防止下溢为负,即使历史脏数据导致 count 与实际不一致,
|
||||
// 也不会产生负值计数;
|
||||
// 4. 计数规则集中表达:统计字段只反映 published 评论,状态进出 published 时分别 +1 / -1。
|
||||
|
||||
// errInvalidStatusTransition 非法状态转换
|
||||
var errInvalidStatusTransition = errors.New("invalid comment status transition")
|
||||
|
||||
// isCountedStatus 判断该状态的评论是否参与聚合计数
|
||||
func isCountedStatus(s model.CommentStatus) bool {
|
||||
return s == model.CommentStatusPublished
|
||||
}
|
||||
|
||||
// statusDelta 返回从 oldStatus 迁移到 newStatus 时,聚合计数应当的增量:
|
||||
// +1:非计数状态 → 计数状态(审核通过 / 恢复发布)
|
||||
// -1:计数状态 → 非计数状态(已发布评论被驳回/隐藏/删除)
|
||||
// 0:其余转换(如 pending → rejected,均不参与计数)
|
||||
func statusDelta(oldStatus, newStatus model.CommentStatus) int {
|
||||
oldCounted := isCountedStatus(oldStatus)
|
||||
newCounted := isCountedStatus(newStatus)
|
||||
switch {
|
||||
case !oldCounted && newCounted:
|
||||
return 1
|
||||
case oldCounted && !newCounted:
|
||||
return -1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// canTransition 校验状态转换是否合法。
|
||||
// deleted 是终态:已软删除的评论不应再被审核(恢复发布等),
|
||||
// 否则会引入"幽灵计数"——评论已被关联数据清理,却仍试图调整计数。
|
||||
func canTransition(oldStatus, newStatus model.CommentStatus) error {
|
||||
if oldStatus == newStatus {
|
||||
return nil
|
||||
}
|
||||
if oldStatus == model.CommentStatusDeleted {
|
||||
return errInvalidStatusTransition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyStatusDelta 在事务内根据 delta 原子调整帖子评论数与父评论回复数。
|
||||
// 使用 CASE WHEN 保证计数永不下溢为负(防御历史脏数据)。
|
||||
func applyStatusDelta(tx *gorm.DB, comment *model.Comment, delta int) error {
|
||||
if delta == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 帖子评论数:delta>0 直接相加;delta<0 用 CASE WHEN 防止下溢
|
||||
var postCountExpr any
|
||||
if delta > 0 {
|
||||
postCountExpr = gorm.Expr("comments_count + ?", delta)
|
||||
} else {
|
||||
postCountExpr = gorm.Expr(
|
||||
"CASE WHEN comments_count >= ? THEN comments_count + ? ELSE 0 END",
|
||||
-delta, delta,
|
||||
)
|
||||
}
|
||||
// 统计字段变更不应影响帖子内容更新时间(updated_at)
|
||||
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
|
||||
UpdateColumns(map[string]any{
|
||||
"comments_count": postCountExpr,
|
||||
"updated_at": gorm.Expr("updated_at"),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 若为回复,同步调整父评论回复数(同样防下溢)
|
||||
if comment.ParentID != nil && *comment.ParentID != "" {
|
||||
var replyCountExpr any
|
||||
if delta > 0 {
|
||||
replyCountExpr = gorm.Expr("replies_count + ?", delta)
|
||||
} else {
|
||||
replyCountExpr = gorm.Expr(
|
||||
"CASE WHEN replies_count >= ? THEN replies_count + ? ELSE 0 END",
|
||||
-delta, delta,
|
||||
)
|
||||
}
|
||||
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
|
||||
UpdateColumn("replies_count", replyCountExpr).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// transitionStatus 在指定事务内将评论状态由当前值迁移到 newStatus,
|
||||
// 并原子维护聚合计数。这是评论状态变更的唯一计数维护通道。
|
||||
//
|
||||
// 调用方需在外层开启事务(Delete / BatchDelete 等),并传入该事务的 tx。
|
||||
// 仅做状态迁移与计数维护,不负责清理点赞、软删除标记等副作用——
|
||||
// 那些由调用方在各自事务内完成。
|
||||
//
|
||||
// 返回迁移发生前的旧评论快照(含旧状态),供调用方决策后续清理逻辑。
|
||||
func transitionStatus(tx *gorm.DB, commentID string, newStatus model.CommentStatus) (*model.Comment, error) {
|
||||
// 加锁读取当前状态,避免并发审核造成计数错乱
|
||||
var comment model.Comment
|
||||
if err := tx.Clauses(clauseLockUpdate).First(&comment, "id = ?", commentID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldStatus := comment.Status
|
||||
if err := canTransition(oldStatus, newStatus); err != nil {
|
||||
return &comment, err
|
||||
}
|
||||
|
||||
if oldStatus == newStatus {
|
||||
// 状态未变化:不写库、不调整计数
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
"status": newStatus,
|
||||
"updated_at": gorm.Expr("updated_at"), // 状态变更不应刷新内容更新时间
|
||||
}
|
||||
if err := tx.Model(&model.Comment{}).
|
||||
Where("id = ?", commentID).
|
||||
UpdateColumns(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := applyStatusDelta(tx, &comment, statusDelta(oldStatus, newStatus)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comment.Status = newStatus
|
||||
return &comment, nil
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"with_you/internal/cache"
|
||||
"with_you/internal/dto"
|
||||
"with_you/internal/model"
|
||||
"with_you/internal/repository"
|
||||
@@ -24,13 +25,15 @@ type AdminCommentService interface {
|
||||
type adminCommentServiceImpl struct {
|
||||
commentRepo repository.CommentRepository
|
||||
postRepo repository.PostRepository
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
// NewAdminCommentService 创建管理端评论服务
|
||||
func NewAdminCommentService(commentRepo repository.CommentRepository, postRepo repository.PostRepository) AdminCommentService {
|
||||
func NewAdminCommentService(commentRepo repository.CommentRepository, postRepo repository.PostRepository, cacheBackend cache.Cache) AdminCommentService {
|
||||
return &adminCommentServiceImpl{
|
||||
commentRepo: commentRepo,
|
||||
postRepo: postRepo,
|
||||
cache: cacheBackend,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,29 +104,49 @@ func (s *adminCommentServiceImpl) BatchDeleteComments(ctx context.Context, ids [
|
||||
}
|
||||
|
||||
func (s *adminCommentServiceImpl) ModerateComment(ctx context.Context, commentID string, status model.CommentStatus, reason, reviewedBy string) (*dto.AdminCommentDetailResponse, error) {
|
||||
_, err := s.commentRepo.GetAdminCommentByID(commentID)
|
||||
if _, err := s.commentRepo.GetAdminCommentByID(commentID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// UpdateModerationStatus 内部已原子地维护帖子评论数/父评论回复数,
|
||||
// 因此管理端审核与自动审核路径的计数行为完全一致。
|
||||
updated, err := s.commentRepo.UpdateModerationStatus(commentID, status, reason, reviewedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.commentRepo.UpdateModerationStatus(commentID, status, reason, reviewedBy); err != nil {
|
||||
return nil, err
|
||||
// 状态变更后失效该帖子相关缓存,确保前端立即看到正确计数
|
||||
if s.cache != nil {
|
||||
cache.InvalidatePostDetail(s.cache, updated.PostID)
|
||||
cache.InvalidatePostList(s.cache)
|
||||
}
|
||||
|
||||
comment, err := s.commentRepo.GetAdminCommentByID(commentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.convertCommentToAdminDetailResponse(comment), nil
|
||||
return s.convertCommentToAdminDetailResponse(updated), nil
|
||||
}
|
||||
|
||||
func (s *adminCommentServiceImpl) BatchModerateComments(ctx context.Context, ids []string, status model.CommentStatus, reviewedBy string) (*dto.AdminBatchOperationResponse, error) {
|
||||
var failedIDs []string
|
||||
// 收集受影响的帖子,用于审核完成后统一失效缓存
|
||||
affectedPostIDs := make(map[string]struct{})
|
||||
|
||||
for _, id := range ids {
|
||||
if err := s.commentRepo.UpdateModerationStatus(id, status, "", reviewedBy); err != nil {
|
||||
// UpdateModerationStatus 内部已原子地维护计数,这里只需收集结果
|
||||
updated, err := s.commentRepo.UpdateModerationStatus(id, status, "", reviewedBy)
|
||||
if err != nil {
|
||||
failedIDs = append(failedIDs, id)
|
||||
continue
|
||||
}
|
||||
if updated != nil {
|
||||
affectedPostIDs[updated.PostID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// 统一失效受影响帖子的缓存
|
||||
if s.cache != nil {
|
||||
for postID := range affectedPostIDs {
|
||||
cache.InvalidatePostDetail(s.cache, postID)
|
||||
}
|
||||
cache.InvalidatePostList(s.cache)
|
||||
}
|
||||
|
||||
if len(failedIDs) == 0 {
|
||||
|
||||
@@ -154,18 +154,13 @@ func (s *commentService) reviewCommentAsync(
|
||||
postOwnerID string,
|
||||
) {
|
||||
if s.hookManager == nil {
|
||||
if err := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished, "", "system"); err != nil {
|
||||
if _, err := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished, "", "system"); err != nil {
|
||||
zap.L().Warn("Failed to publish comment without hook manager",
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
if err := s.applyCommentPublishedStats(commentID); err != nil {
|
||||
zap.L().Warn("Failed to apply published stats for comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
// 计数维护已在 UpdateModerationStatus 事务内完成,无需再单独调用
|
||||
s.invalidatePostCaches(postID)
|
||||
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
|
||||
return
|
||||
@@ -210,31 +205,18 @@ func (s *commentService) reviewCommentAsync(
|
||||
return
|
||||
}
|
||||
|
||||
if updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished, "", result.ReviewedBy); updateErr != nil {
|
||||
if _, updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished, "", result.ReviewedBy); updateErr != nil {
|
||||
zap.L().Warn("Failed to publish comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(updateErr),
|
||||
)
|
||||
return
|
||||
}
|
||||
if statsErr := s.applyCommentPublishedStats(commentID); statsErr != nil {
|
||||
zap.L().Warn("Failed to apply published stats for comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(statsErr),
|
||||
)
|
||||
}
|
||||
// 计数维护已在 UpdateModerationStatus 事务内完成,无需再单独调用
|
||||
s.invalidatePostCaches(postID)
|
||||
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
|
||||
}
|
||||
|
||||
func (s *commentService) applyCommentPublishedStats(commentID string) error {
|
||||
comment, err := s.commentRepo.GetByID(commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.commentRepo.ApplyPublishedStats(comment)
|
||||
}
|
||||
|
||||
func (s *commentService) invalidatePostCaches(postID string) {
|
||||
cache.InvalidatePostDetail(s.cache, postID)
|
||||
cache.InvalidatePostList(s.cache)
|
||||
|
||||
@@ -188,7 +188,11 @@ func (s *reportServiceImpl) validateTargetExists(ctx context.Context, targetType
|
||||
return nil
|
||||
}
|
||||
|
||||
// hideTargetContent 隐藏目标内容
|
||||
// hideTargetContent 隐藏目标内容。
|
||||
//
|
||||
// 评论隐藏统一走 commentRepo.UpdateModerationStatus(published → deleted),
|
||||
// 经由仓储内的统一状态转换层维护 posts.comments_count / comments.replies_count,
|
||||
// 避免此前用裸 Update 改 status 导致举报自动隐藏后计数漏减的问题。
|
||||
func (s *reportServiceImpl) hideTargetContent(ctx context.Context, targetType model.ReportTargetType, targetID string) error {
|
||||
switch targetType {
|
||||
case model.ReportTargetTypePost:
|
||||
@@ -205,9 +209,13 @@ func (s *reportServiceImpl) hideTargetContent(ctx context.Context, targetType mo
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if comment != nil && comment.Status != model.CommentStatusDeleted {
|
||||
comment.Status = model.CommentStatusDeleted
|
||||
return s.commentRepo.Update(comment)
|
||||
// 已是终态(deleted/rejected 等非 published)无需再隐藏,也避免重复调整计数
|
||||
if comment == nil || comment.Status != model.CommentStatusPublished {
|
||||
return nil
|
||||
}
|
||||
// published → deleted:统一状态转换层会原子维护计数(永不为负)
|
||||
if _, err := s.commentRepo.UpdateModerationStatus(targetID, model.CommentStatusDeleted, "举报自动隐藏", "system"); err != nil {
|
||||
return err
|
||||
}
|
||||
case model.ReportTargetTypeMessage:
|
||||
// 消息隐藏通过更新状态实现
|
||||
|
||||
@@ -350,8 +350,8 @@ func ProvideAdminPostService(postRepo repository.PostRepository, cacheBackend ca
|
||||
}
|
||||
|
||||
// ProvideAdminCommentService 提供管理端评论服务
|
||||
func ProvideAdminCommentService(commentRepo repository.CommentRepository, postRepo repository.PostRepository) service.AdminCommentService {
|
||||
return service.NewAdminCommentService(commentRepo, postRepo)
|
||||
func ProvideAdminCommentService(commentRepo repository.CommentRepository, postRepo repository.PostRepository, cacheBackend cache.Cache) service.AdminCommentService {
|
||||
return service.NewAdminCommentService(commentRepo, postRepo, cacheBackend)
|
||||
}
|
||||
|
||||
// ProvideAdminGroupService 提供管理端群组服务
|
||||
|
||||
Reference in New Issue
Block a user