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
226 lines
7.1 KiB
Go
226 lines
7.1 KiB
Go
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
|
||
}
|