Files
backend/internal/service/admin_report_service.go
lafay 5f7b02ee8e
Some checks failed
Build Backend / build (push) Failing after 5m19s
Build Backend / build-docker (push) Has been skipped
feat: 添加举报功能支持
- 新增 Report 数据模型、DTO、Repository、Service 层
- 实现用户端举报 API (POST /api/v1/reports)
- 实现管理端举报管理 API (列表、详情、处理、批量处理)
- 添加举报自动隐藏机制(达到阈值自动隐藏内容)
- 集成系统通知(举报处理结果通知)
- 更新路由和 Wire 依赖注入配置

Made-with: Cursor
2026-03-29 20:18:36 +08:00

442 lines
12 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"
"time"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"go.uber.org/zap"
)
// AdminReportService 管理端举报服务接口
type AdminReportService interface {
// GetReportList 获取举报列表
GetReportList(ctx context.Context, query dto.AdminReportListQuery) ([]dto.AdminReportListResponse, int64, error)
// GetReportDetail 获取举报详情
GetReportDetail(ctx context.Context, id string) (*dto.AdminReportDetailResponse, error)
// HandleReport 处理举报
HandleReport(ctx context.Context, id string, action, result, handledBy string) error
// BatchHandle 批量处理举报
BatchHandle(ctx context.Context, ids []string, action, result, handledBy string) (*dto.AdminBatchHandleResponse, error)
}
// adminReportServiceImpl 管理端举报服务实现
type adminReportServiceImpl struct {
reportRepo repository.ReportRepository
postRepo repository.PostRepository
commentRepo repository.CommentRepository
messageRepo repository.MessageRepository
userRepo repository.UserRepository
systemNotify SystemNotificationService
txManager repository.TransactionManager
logService *LogService
}
// NewAdminReportService 创建管理端举报服务
func NewAdminReportService(
reportRepo repository.ReportRepository,
postRepo repository.PostRepository,
commentRepo repository.CommentRepository,
messageRepo repository.MessageRepository,
userRepo repository.UserRepository,
systemNotify SystemNotificationService,
txManager repository.TransactionManager,
logService *LogService,
) AdminReportService {
return &adminReportServiceImpl{
reportRepo: reportRepo,
postRepo: postRepo,
commentRepo: commentRepo,
messageRepo: messageRepo,
userRepo: userRepo,
systemNotify: systemNotify,
txManager: txManager,
logService: logService,
}
}
// GetReportList 获取举报列表
func (s *adminReportServiceImpl) GetReportList(ctx context.Context, query dto.AdminReportListQuery) ([]dto.AdminReportListResponse, int64, error) {
// 设置默认分页参数
if query.Page <= 0 {
query.Page = 1
}
if query.PageSize <= 0 {
query.PageSize = 20
}
// 查询举报列表
reports, total, err := s.reportRepo.List(query)
if err != nil {
zap.L().Error("failed to get report list", zap.Error(err))
return nil, 0, errors.New("failed to get report list")
}
// 收集用户ID
userIDs := make(map[string]bool)
for _, report := range reports {
userIDs[report.ReporterID] = true
if report.HandledBy != nil {
userIDs[*report.HandledBy] = true
}
}
// 批量获取用户信息
users, err := s.getUsersByIDs(ctx, userIDs)
if err != nil {
zap.L().Error("failed to get users", zap.Error(err))
}
// 转换响应
responses := make([]dto.AdminReportListResponse, len(reports))
for i, report := range reports {
var reporter, handler *model.User
if user, ok := users[report.ReporterID]; ok {
reporter = user
}
if report.HandledBy != nil {
if user, ok := users[*report.HandledBy]; ok {
handler = user
}
}
responses[i] = *dto.ConvertReportToAdminListResponse(report, reporter, handler)
}
return responses, total, nil
}
// GetReportDetail 获取举报详情
func (s *adminReportServiceImpl) GetReportDetail(ctx context.Context, id string) (*dto.AdminReportDetailResponse, error) {
// 查询举报
report, err := s.reportRepo.FindByID(id)
if err != nil {
zap.L().Error("failed to get report", zap.Error(err), zap.String("id", id))
return nil, errors.New("report not found")
}
// 获取举报人信息
var reporter *model.User
if user, err := s.userRepo.GetByID(report.ReporterID); err == nil {
reporter = user
}
// 获取处理人信息
var handler *model.User
if report.HandledBy != nil {
if user, err := s.userRepo.GetByID(*report.HandledBy); err == nil {
handler = user
}
}
// 获取被举报内容详情
targetContent, err := s.getTargetContent(ctx, report.TargetType, report.TargetID)
if err != nil {
zap.L().Warn("failed to get target content", zap.Error(err))
}
return dto.ConvertReportToAdminDetailResponse(report, reporter, handler, targetContent), nil
}
// HandleReport 处理举报
func (s *adminReportServiceImpl) HandleReport(ctx context.Context, id string, action, result, handledBy string) error {
// 查询举报
report, err := s.reportRepo.FindByID(id)
if err != nil {
return errors.New("report not found")
}
// 检查状态
if report.Status != model.ReportStatusPending && report.Status != model.ReportStatusProcessing {
return errors.New("report has already been handled")
}
// 确定新状态
var newStatus model.ReportStatus
if action == "approve" {
newStatus = model.ReportStatusResolved
} else {
newStatus = model.ReportStatusRejected
}
// 使用事务处理
err = s.txManager.RunInTransaction(ctx, func(ctx context.Context) error {
// 更新举报状态
if err := s.reportRepo.UpdateStatusWithContext(ctx, id, newStatus, handledBy, result); err != nil {
return fmt.Errorf("failed to update report status: %w", err)
}
// 如果确认违规,删除内容
if action == "approve" {
if err := s.deleteTargetContent(ctx, report.TargetType, report.TargetID); err != nil {
zap.L().Error("failed to delete target content", zap.Error(err))
// 不阻止举报处理
}
}
return nil
})
if err != nil {
return err
}
// 发送通知给举报人
s.notifyReporter(ctx, report, action, result)
// 记录操作日志
if s.logService != nil {
s.logService.LogOperation(ctx, "handle_report", "report", id, handledBy, map[string]interface{}{
"action": action,
"result": result,
})
}
return nil
}
// BatchHandle 批量处理举报
func (s *adminReportServiceImpl) BatchHandle(ctx context.Context, ids []string, action, result, handledBy string) (*dto.AdminBatchHandleResponse, error) {
// 确定新状态
var newStatus model.ReportStatus
if action == "approve" {
newStatus = model.ReportStatusResolved
} else {
newStatus = model.ReportStatusRejected
}
response := &dto.AdminBatchHandleResponse{
FailedIDs: []string{},
}
// 使用事务处理
err := s.txManager.RunInTransaction(ctx, func(ctx context.Context) error {
// 获取举报列表
reports, err := s.reportRepo.GetByIDsWithContext(ctx, ids)
if err != nil {
return fmt.Errorf("failed to get reports: %w", err)
}
// 过滤出可以处理的举报
var validIDs []string
for _, report := range reports {
if report.Status == model.ReportStatusPending || report.Status == model.ReportStatusProcessing {
validIDs = append(validIDs, report.ID)
} else {
response.FailedIDs = append(response.FailedIDs, report.ID)
response.FailedCount++
}
}
if len(validIDs) == 0 {
return nil
}
// 批量更新状态
affected, err := s.reportRepo.BatchUpdateStatusWithContext(ctx, validIDs, newStatus, handledBy, result)
if err != nil {
return fmt.Errorf("failed to batch update reports: %w", err)
}
response.SuccessCount = int(affected)
// 如果确认违规,批量删除内容
if action == "approve" {
for _, report := range reports {
if containsID(validIDs, report.ID) {
if err := s.deleteTargetContent(ctx, report.TargetType, report.TargetID); err != nil {
zap.L().Error("failed to delete target content",
zap.Error(err),
zap.String("target_type", string(report.TargetType)),
zap.String("target_id", report.TargetID),
)
}
}
}
}
// 发送通知给举报人
for _, report := range reports {
if containsID(validIDs, report.ID) {
s.notifyReporter(ctx, report, action, result)
}
}
return nil
})
if err != nil {
return nil, err
}
// 记录操作日志
if s.logService != nil {
s.logService.LogOperation(ctx, "batch_handle_reports", "report", "", handledBy, map[string]interface{}{
"ids": ids,
"action": action,
"success_count": response.SuccessCount,
})
}
return response, nil
}
// getUsersByIDs 批量获取用户
func (s *adminReportServiceImpl) getUsersByIDs(ctx context.Context, userIDs map[string]bool) (map[string]*model.User, error) {
users := make(map[string]*model.User)
for id := range userIDs {
user, err := s.userRepo.GetByID(id)
if err == nil {
users[id] = user
}
}
return users, nil
}
// getTargetContent 获取被举报内容详情
func (s *adminReportServiceImpl) getTargetContent(ctx context.Context, targetType model.ReportTargetType, targetID string) (*dto.ReportTargetContent, error) {
content := &dto.ReportTargetContent{
Type: string(targetType),
ID: targetID,
}
switch targetType {
case model.ReportTargetTypePost:
post, err := s.postRepo.GetByIDForAdmin(targetID)
if err != nil {
return nil, err
}
content.Title = post.Title
content.Content = post.Content
content.Status = string(post.Status)
content.CreatedAt = dto.FormatTime(post.CreatedAt)
if post.User != nil {
content.Author = dto.ConvertUserToResponse(post.User)
}
if len(post.Images) > 0 {
images := make([]string, len(post.Images))
for i, img := range post.Images {
images[i] = img.URL
}
content.Images = images
}
case model.ReportTargetTypeComment:
comment, err := s.commentRepo.GetByID(targetID)
if err != nil {
return nil, err
}
content.Content = comment.Content
content.Status = string(comment.Status)
content.CreatedAt = dto.FormatTime(comment.CreatedAt)
if comment.User != nil {
content.Author = dto.ConvertUserToResponse(comment.User)
}
case model.ReportTargetTypeMessage:
msg, err := s.messageRepo.GetMessageByID(targetID)
if err != nil {
return nil, err
}
content.Content = msg.Content
content.Status = string(msg.Status)
content.CreatedAt = dto.FormatTime(msg.CreatedAt)
// 获取发送者信息
if msg.SenderID != "" {
if sender, err := s.userRepo.GetByID(msg.SenderID); err == nil {
content.Author = dto.ConvertUserToResponse(sender)
}
}
}
return content, nil
}
// deleteTargetContent 删除目标内容
func (s *adminReportServiceImpl) deleteTargetContent(ctx context.Context, targetType model.ReportTargetType, targetID string) error {
switch targetType {
case model.ReportTargetTypePost:
return s.postRepo.Delete(targetID)
case model.ReportTargetTypeComment:
return s.commentRepo.Delete(targetID)
case model.ReportTargetTypeMessage:
return s.messageRepo.UpdateMessageStatus(targetID, model.MessageStatusDeleted)
}
return nil
}
// notifyReporter 通知举报人
func (s *adminReportServiceImpl) notifyReporter(ctx context.Context, report *model.Report, action, result string) {
if s.systemNotify == nil {
return
}
// 获取举报人信息
reporter, err := s.userRepo.GetByID(report.ReporterID)
if err != nil {
return
}
var title, content string
var notifyType model.SystemNotificationType
if action == "approve" {
notifyType = model.SysNotifyReportResolved
title = "举报处理结果"
content = fmt.Sprintf("您举报的%s已确认违规内容已被删除。", s.getTargetTypeName(report.TargetType))
if result != "" {
content += fmt.Sprintf(" 处理说明:%s", result)
}
} else {
notifyType = model.SysNotifyReportRejected
title = "举报处理结果"
content = fmt.Sprintf("您举报的%s经审核未发现违规举报已驳回。", s.getTargetTypeName(report.TargetType))
if result != "" {
content += fmt.Sprintf(" 处理说明:%s", result)
}
}
// 发送通知
_, err = s.systemNotify.CreateNotification(ctx, &model.SystemNotification{
ReceiverID: report.ReporterID,
Type: notifyType,
Title: title,
Content: content,
ExtraData: &model.SystemNotificationExtra{
ActorIDStr: report.ReporterID,
ActorName: reporter.Nickname,
TargetID: report.TargetID,
TargetType: string(report.TargetType),
},
})
if err != nil {
zap.L().Error("failed to send report notification", zap.Error(err))
}
}
// getTargetTypeName 获取目标类型名称
func (s *adminReportServiceImpl) getTargetTypeName(targetType model.ReportTargetType) string {
switch targetType {
case model.ReportTargetTypePost:
return "帖子"
case model.ReportTargetTypeComment:
return "评论"
case model.ReportTargetTypeMessage:
return "消息"
}
return "内容"
}
// containsID 检查ID是否在列表中
func containsID(ids []string, id string) bool {
for _, v := range ids {
if v == id {
return true
}
}
return false
}