Files
backend/internal/service/report_service.go
lafay 28cfcbf9a8
All checks were successful
Build Backend / build (push) Successful in 3m31s
Build Backend / build-docker (push) Successful in 1m25s
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
2026-06-21 17:17:38 +08:00

226 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"fmt"
"with_you/internal/config"
"with_you/internal/model"
"with_you/internal/repository"
"go.uber.org/zap"
)
// ReportService 举报服务接口(用户端)
type ReportService interface {
// CreateReport 创建举报
CreateReport(ctx context.Context, reporterID, targetType, targetID, reason, description string) (*model.Report, error)
// HasUserReported 检查用户是否已举报
HasUserReported(ctx context.Context, reporterID, targetType, targetID string) (bool, error)
}
// reportServiceImpl 举报服务实现
type reportServiceImpl struct {
reportRepo repository.ReportRepository
postRepo repository.PostRepository
commentRepo repository.CommentRepository
messageRepo repository.MessageRepository
userRepo repository.UserRepository
notifyRepo repository.SystemNotificationRepository
pushService PushService
txManager repository.TransactionManager
logService OperationLogService
config *config.ReportConfig
}
// NewReportService 创建举报服务
func NewReportService(
reportRepo repository.ReportRepository,
postRepo repository.PostRepository,
commentRepo repository.CommentRepository,
messageRepo repository.MessageRepository,
userRepo repository.UserRepository,
notifyRepo repository.SystemNotificationRepository,
pushService PushService,
txManager repository.TransactionManager,
logService OperationLogService,
cfg *config.Config,
) ReportService {
reportConfig := &cfg.Report
if reportConfig.AutoHideThreshold == 0 {
reportConfig.AutoHideThreshold = 3
}
return &reportServiceImpl{
reportRepo: reportRepo,
postRepo: postRepo,
commentRepo: commentRepo,
messageRepo: messageRepo,
userRepo: userRepo,
notifyRepo: notifyRepo,
pushService: pushService,
txManager: txManager,
logService: logService,
config: reportConfig,
}
}
// CreateReport 创建举报
func (s *reportServiceImpl) CreateReport(ctx context.Context, reporterID, targetType, targetID, reason, description string) (*model.Report, error) {
// 验证举报类型
reportTargetType := model.ReportTargetType(targetType)
if reportTargetType != model.ReportTargetTypePost &&
reportTargetType != model.ReportTargetTypeComment &&
reportTargetType != model.ReportTargetTypeMessage {
return nil, errors.New("invalid target type")
}
// 验证举报原因
reportReason := model.ReportReason(reason)
if reportReason != model.ReportReasonSpam &&
reportReason != model.ReportReasonInappropriate &&
reportReason != model.ReportReasonHarassment &&
reportReason != model.ReportReasonMisinformation &&
reportReason != model.ReportReasonOther {
return nil, errors.New("invalid reason")
}
// 检查目标内容是否存在
if err := s.validateTargetExists(ctx, reportTargetType, targetID); err != nil {
return nil, err
}
// 检查用户是否已举报过
hasReported, err := s.reportRepo.HasUserReported(reporterID, reportTargetType, targetID)
if err != nil {
zap.L().Error("failed to check if user has reported", zap.Error(err))
return nil, errors.New("failed to check report status")
}
if hasReported {
return nil, errors.New("you have already reported this content")
}
// 创建举报
report := &model.Report{
ReporterID: reporterID,
TargetType: reportTargetType,
TargetID: targetID,
Reason: reportReason,
Description: description,
Status: model.ReportStatusPending,
}
// 使用事务处理
var createdReport *model.Report
err = s.txManager.RunInTransaction(ctx, func(ctx context.Context) error {
// 创建举报记录
if err := s.reportRepo.CreateWithContext(ctx, report); err != nil {
return fmt.Errorf("failed to create report: %w", err)
}
createdReport = report
// 检查举报次数是否达到阈值
count, err := s.reportRepo.GetReportCount(reportTargetType, targetID)
if err != nil {
zap.L().Error("failed to get report count", zap.Error(err))
return nil // 不阻止举报创建
}
// 达到阈值,自动隐藏内容
if int(count) >= s.config.AutoHideThreshold {
if err := s.hideTargetContent(ctx, reportTargetType, targetID); err != nil {
zap.L().Error("failed to hide target content", zap.Error(err))
return nil // 不阻止举报创建
}
zap.L().Info("content auto-hidden due to report threshold",
zap.String("target_type", targetType),
zap.String("target_id", targetID),
zap.Int64("report_count", count),
)
}
return nil
})
if err != nil {
return nil, err
}
// 记录操作日志
if s.logService != nil {
s.logService.RecordOperation(&model.OperationLog{
UserID: reporterID,
Operation: "create_report",
TargetType: "report",
TargetID: report.ID,
Module: "report",
})
}
return createdReport, nil
}
// HasUserReported 检查用户是否已举报
func (s *reportServiceImpl) HasUserReported(ctx context.Context, reporterID, targetType, targetID string) (bool, error) {
return s.reportRepo.HasUserReported(reporterID, model.ReportTargetType(targetType), targetID)
}
// validateTargetExists 验证目标内容是否存在
func (s *reportServiceImpl) validateTargetExists(ctx context.Context, targetType model.ReportTargetType, targetID string) error {
switch targetType {
case model.ReportTargetTypePost:
post, err := s.postRepo.GetByID(targetID)
if err != nil || post == nil {
return errors.New("post not found")
}
case model.ReportTargetTypeComment:
comment, err := s.commentRepo.GetByID(targetID)
if err != nil || comment == nil {
return errors.New("comment not found")
}
case model.ReportTargetTypeMessage:
_, err := s.messageRepo.GetMessageByID(targetID)
if err != nil {
return errors.New("message not found")
}
}
return nil
}
// 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:
post, err := s.postRepo.GetByID(targetID)
if err != nil {
return err
}
if post != nil && post.Status != model.PostStatusDeleted {
post.Status = model.PostStatusDeleted
return s.postRepo.Update(post)
}
case model.ReportTargetTypeComment:
comment, err := s.commentRepo.GetByID(targetID)
if err != nil {
return err
}
// 已是终态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:
// 消息隐藏通过更新状态实现
return s.messageRepo.UpdateMessageStatus(targetID, model.MessageStatusDeleted)
}
return nil
}