From 5f7b02ee8e678c44e3367ab09f30e2d52979ee24 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Sun, 29 Mar 2026 20:18:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=BE=E6=8A=A5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Report 数据模型、DTO、Repository、Service 层 - 实现用户端举报 API (POST /api/v1/reports) - 实现管理端举报管理 API (列表、详情、处理、批量处理) - 添加举报自动隐藏机制(达到阈值自动隐藏内容) - 集成系统通知(举报处理结果通知) - 更新路由和 Wire 依赖注入配置 Made-with: Cursor --- configs/config.yaml | 5 + internal/config/config.go | 3 + internal/config/report.go | 7 + internal/dto/report.go | 187 ++++++++++ internal/handler/admin_report_handler.go | 152 ++++++++ internal/handler/report_handler.go | 61 ++++ internal/model/init.go | 3 + internal/model/report.go | 80 ++++ internal/model/system_notification.go | 4 + internal/repository/report_repo.go | 212 +++++++++++ internal/router/router.go | 23 ++ internal/service/admin_report_service.go | 442 +++++++++++++++++++++++ internal/service/report_service.go | 213 +++++++++++ internal/wire/handler.go | 2 + internal/wire/repository.go | 1 + internal/wire/service.go | 31 ++ 16 files changed, 1426 insertions(+) create mode 100644 internal/config/report.go create mode 100644 internal/dto/report.go create mode 100644 internal/handler/admin_report_handler.go create mode 100644 internal/handler/report_handler.go create mode 100644 internal/model/report.go create mode 100644 internal/repository/report_repo.go create mode 100644 internal/service/admin_report_service.go create mode 100644 internal/service/report_service.go diff --git a/configs/config.yaml b/configs/config.yaml index fe10a42..7872949 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -244,3 +244,8 @@ casbin: - /api/v1/auth/password/reset - /api/v1/auth/refresh - /api/v1/admin/setup-super-admin + +# 举报配置 +# 环境变量: APP_REPORT_AUTO_HIDE_THRESHOLD +report: + auto_hide_threshold: 3 # 自动隐藏阈值,举报次数达到此数值时自动隐藏内容 diff --git a/internal/config/config.go b/internal/config/config.go index 9aeea0b..23f0504 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,6 +30,7 @@ type Config struct { Encryption EncryptionConfig `mapstructure:"encryption"` HotRank HotRankConfig `mapstructure:"hot_rank"` WebRTC WebRTCConfig `mapstructure:"webrtc"` + Report ReportConfig `mapstructure:"report"` } // Load 加载配置文件 @@ -155,6 +156,8 @@ func Load(configPath string) (*Config, error) { viper.SetDefault("webrtc.ice_servers", []map[string]interface{}{ {"urls": []string{"stun:stun.l.google.com:19302"}}, }) + // Report 默认值 + viper.SetDefault("report.auto_hide_threshold", 3) if err := viper.ReadInConfig(); err != nil { return nil, fmt.Errorf("failed to read config: %w", err) diff --git a/internal/config/report.go b/internal/config/report.go new file mode 100644 index 0000000..f9f31f3 --- /dev/null +++ b/internal/config/report.go @@ -0,0 +1,7 @@ +package config + +// ReportConfig 举报配置 +type ReportConfig struct { + // AutoHideThreshold 自动隐藏阈值(举报次数达到此数值时自动隐藏内容) + AutoHideThreshold int `mapstructure:"auto_hide_threshold"` +} \ No newline at end of file diff --git a/internal/dto/report.go b/internal/dto/report.go new file mode 100644 index 0000000..4cc7262 --- /dev/null +++ b/internal/dto/report.go @@ -0,0 +1,187 @@ +package dto + +import ( + "carrot_bbs/internal/model" + "time" +) + +// ==================== 举报 DTOs ==================== + +// CreateReportRequest 创建举报请求 +type CreateReportRequest struct { + TargetType string `json:"target_type" binding:"required,oneof=post comment message"` + TargetID string `json:"target_id" binding:"required"` + Reason string `json:"reason" binding:"required,oneof=spam inappropriate harassment misinformation other"` + Description string `json:"description" binding:"max=500"` +} + +// ReportResponse 举报响应 +type ReportResponse struct { + ID string `json:"id"` + ReporterID string `json:"reporter_id"` + TargetType string `json:"target_type"` + TargetID string `json:"target_id"` + Reason string `json:"reason"` + Description string `json:"description,omitempty"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` +} + +// AdminReportListQuery 管理端举报列表查询参数 +type AdminReportListQuery struct { + Page int `form:"page" binding:"min=1"` + PageSize int `form:"page_size" binding:"min=1,max=100"` + TargetType string `form:"target_type" binding:"omitempty,oneof=post comment message"` + Status string `form:"status" binding:"omitempty,oneof=pending processing resolved rejected"` + StartDate string `form:"start_date" binding:"omitempty"` + EndDate string `form:"end_date" binding:"omitempty"` + Keyword string `form:"keyword" binding:"omitempty"` +} + +// AdminReportListResponse 管理端举报列表响应 +type AdminReportListResponse struct { + ID string `json:"id"` + ReporterID string `json:"reporter_id"` + Reporter *UserResponse `json:"reporter,omitempty"` + TargetType string `json:"target_type"` + TargetID string `json:"target_id"` + Reason string `json:"reason"` + Description string `json:"description,omitempty"` + Status string `json:"status"` + HandledBy *string `json:"handled_by,omitempty"` + Handler *UserResponse `json:"handler,omitempty"` + HandledAt *string `json:"handled_at,omitempty"` + Result *string `json:"result,omitempty"` + CreatedAt string `json:"created_at"` +} + +// AdminReportDetailResponse 管理端举报详情响应 +type AdminReportDetailResponse struct { + ID string `json:"id"` + ReporterID string `json:"reporter_id"` + Reporter *UserResponse `json:"reporter"` + TargetType string `json:"target_type"` + TargetID string `json:"target_id"` + Reason string `json:"reason"` + Description string `json:"description,omitempty"` + Status string `json:"status"` + HandledBy *string `json:"handled_by,omitempty"` + Handler *UserResponse `json:"handler,omitempty"` + HandledAt *string `json:"handled_at,omitempty"` + Result *string `json:"result,omitempty"` + CreatedAt string `json:"created_at"` + + // 被举报内容详情 + TargetContent *ReportTargetContent `json:"target_content,omitempty"` +} + +// ReportTargetContent 被举报内容详情 +type ReportTargetContent struct { + Type string `json:"type"` // post, comment, message + ID string `json:"id"` + Author *UserResponse `json:"author,omitempty"` + Content string `json:"content,omitempty"` + Title string `json:"title,omitempty"` // 帖子标题 + Images []string `json:"images,omitempty"` // 图片列表 + Status string `json:"status,omitempty"` // 内容状态 + CreatedAt string `json:"created_at,omitempty"` +} + +// HandleReportRequest 处理举报请求 +type HandleReportRequest struct { + Action string `json:"action" binding:"required,oneof=approve reject"` // approve: 确认违规, reject: 驳回 + Result string `json:"result" binding:"max=500"` // 处理结果说明 +} + +// BatchHandleReportRequest 批量处理举报请求 +type BatchHandleReportRequest struct { + IDs []string `json:"ids" binding:"required,min=1,max=100"` + Action string `json:"action" binding:"required,oneof=approve reject"` + Result string `json:"result" binding:"max=500"` +} + +// AdminBatchHandleResponse 批量处理响应 +type AdminBatchHandleResponse struct { + SuccessCount int `json:"success_count"` + FailedCount int `json:"failed_count"` + FailedIDs []string `json:"failed_ids,omitempty"` +} + +// ==================== 转换函数 ==================== + +// ConvertReportToResponse 将 Report 模型转换为响应 +func ConvertReportToResponse(report *model.Report) *ReportResponse { + return &ReportResponse{ + ID: report.ID, + ReporterID: report.ReporterID, + TargetType: string(report.TargetType), + TargetID: report.TargetID, + Reason: string(report.Reason), + Description: report.Description, + Status: string(report.Status), + CreatedAt: FormatTime(report.CreatedAt), + } +} + +// ConvertReportToAdminListResponse 将 Report 转换为管理端列表响应 +func ConvertReportToAdminListResponse(report *model.Report, reporter *model.User, handler *model.User) *AdminReportListResponse { + resp := &AdminReportListResponse{ + ID: report.ID, + ReporterID: report.ReporterID, + TargetType: string(report.TargetType), + TargetID: report.TargetID, + Reason: string(report.Reason), + Description: report.Description, + Status: string(report.Status), + HandledBy: report.HandledBy, + Result: report.Result, + CreatedAt: FormatTime(report.CreatedAt), + } + + if reporter != nil { + resp.Reporter = ConvertUserToResponse(reporter) + } + + if handler != nil { + resp.Handler = ConvertUserToResponse(handler) + } + + if report.HandledAt != nil { + handledAt := FormatTime(*report.HandledAt) + resp.HandledAt = &handledAt + } + + return resp +} + +// ConvertReportToAdminDetailResponse 将 Report 转换为管理端详情响应 +func ConvertReportToAdminDetailResponse(report *model.Report, reporter *model.User, handler *model.User, targetContent *ReportTargetContent) *AdminReportDetailResponse { + resp := &AdminReportDetailResponse{ + ID: report.ID, + ReporterID: report.ReporterID, + TargetType: string(report.TargetType), + TargetID: report.TargetID, + Reason: string(report.Reason), + Description: report.Description, + Status: string(report.Status), + HandledBy: report.HandledBy, + Result: report.Result, + CreatedAt: FormatTime(report.CreatedAt), + TargetContent: targetContent, + } + + if reporter != nil { + resp.Reporter = ConvertUserToResponse(reporter) + } + + if handler != nil { + resp.Handler = ConvertUserToResponse(handler) + } + + if report.HandledAt != nil { + handledAt := FormatTime(*report.HandledAt) + resp.HandledAt = &handledAt + } + + return resp +} \ No newline at end of file diff --git a/internal/handler/admin_report_handler.go b/internal/handler/admin_report_handler.go new file mode 100644 index 0000000..210d846 --- /dev/null +++ b/internal/handler/admin_report_handler.go @@ -0,0 +1,152 @@ +package handler + +import ( + "carrot_bbs/internal/dto" + "carrot_bbs/internal/response" + "carrot_bbs/internal/service" + "strconv" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// AdminReportHandler 管理端举报处理器 +type AdminReportHandler struct { + adminReportService service.AdminReportService +} + +// NewAdminReportHandler 创建管理端举报处理器 +func NewAdminReportHandler(adminReportService service.AdminReportService) *AdminReportHandler { + return &AdminReportHandler{ + adminReportService: adminReportService, + } +} + +// List 获取举报列表 +func (h *AdminReportHandler) List(c *gin.Context) { + // 解析查询参数 + var query dto.AdminReportListQuery + if err := c.ShouldBindQuery(&query); err != nil { + response.BadRequest(c, "参数错误: "+err.Error()) + return + } + + // 设置默认分页 + if query.Page == 0 { + query.Page = 1 + } + if query.PageSize == 0 { + query.PageSize = 20 + } + + // 查询列表 + reports, total, err := h.adminReportService.GetReportList(c.Request.Context(), query) + if err != nil { + zap.L().Error("failed to get report list", zap.Error(err)) + response.InternalServerError(c, "获取举报列表失败") + return + } + + response.Paginated(c, reports, total, query.Page, query.PageSize) +} + +// Detail 获取举报详情 +func (h *AdminReportHandler) Detail(c *gin.Context) { + // 获取举报ID + id := c.Param("id") + if id == "" { + response.BadRequest(c, "举报ID不能为空") + return + } + + // 查询详情 + report, err := h.adminReportService.GetReportDetail(c.Request.Context(), id) + if err != nil { + zap.L().Error("failed to get report detail", zap.Error(err), zap.String("id", id)) + response.NotFound(c, "举报不存在") + return + } + + response.Success(c, report) +} + +// Handle 处理举报 +func (h *AdminReportHandler) Handle(c *gin.Context) { + // 获取处理人ID + handledBy := c.GetString("user_id") + if handledBy == "" { + response.Unauthorized(c, "未授权") + return + } + + // 获取举报ID + id := c.Param("id") + if id == "" { + response.BadRequest(c, "举报ID不能为空") + return + } + + // 解析请求 + var req dto.HandleReportRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误: "+err.Error()) + return + } + + // 处理举报 + err := h.adminReportService.HandleReport(c.Request.Context(), id, req.Action, req.Result, handledBy) + if err != nil { + zap.L().Error("failed to handle report", + zap.Error(err), + zap.String("id", id), + zap.String("action", req.Action), + ) + response.Error(c, err.Error()) + return + } + + response.Success(c, gin.H{"message": "处理成功"}) +} + +// BatchHandle 批量处理举报 +func (h *AdminReportHandler) BatchHandle(c *gin.Context) { + // 获取处理人ID + handledBy := c.GetString("user_id") + if handledBy == "" { + response.Unauthorized(c, "未授权") + return + } + + // 解析请求 + var req dto.BatchHandleReportRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误: "+err.Error()) + return + } + + // 批量处理 + result, err := h.adminReportService.BatchHandle(c.Request.Context(), req.IDs, req.Action, req.Result, handledBy) + if err != nil { + zap.L().Error("failed to batch handle reports", + zap.Error(err), + zap.Int("count", len(req.IDs)), + ) + response.Error(c, err.Error()) + return + } + + response.Success(c, result) +} + +// parsePageParams 解析分页参数 +func parsePageParams(c *gin.Context) (int, int) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + return page, pageSize +} \ No newline at end of file diff --git a/internal/handler/report_handler.go b/internal/handler/report_handler.go new file mode 100644 index 0000000..e77593b --- /dev/null +++ b/internal/handler/report_handler.go @@ -0,0 +1,61 @@ +package handler + +import ( + "carrot_bbs/internal/dto" + "carrot_bbs/internal/response" + "carrot_bbs/internal/service" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// ReportHandler 举报处理器 +type ReportHandler struct { + reportService service.ReportService +} + +// NewReportHandler 创建举报处理器 +func NewReportHandler(reportService service.ReportService) *ReportHandler { + return &ReportHandler{ + reportService: reportService, + } +} + +// Create 创建举报 +func (h *ReportHandler) Create(c *gin.Context) { + // 获取用户ID + userID := c.GetString("user_id") + if userID == "" { + response.Unauthorized(c, "未授权") + return + } + + // 解析请求 + var req dto.CreateReportRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误: "+err.Error()) + return + } + + // 创建举报 + report, err := h.reportService.CreateReport( + c.Request.Context(), + userID, + req.TargetType, + req.TargetID, + req.Reason, + req.Description, + ) + if err != nil { + zap.L().Error("failed to create report", + zap.Error(err), + zap.String("user_id", userID), + zap.String("target_type", req.TargetType), + zap.String("target_id", req.TargetID), + ) + response.Error(c, err.Error()) + return + } + + response.Success(c, dto.ConvertReportToResponse(report)) +} \ No newline at end of file diff --git a/internal/model/init.go b/internal/model/init.go index f53a8d5..74f172a 100644 --- a/internal/model/init.go +++ b/internal/model/init.go @@ -134,6 +134,9 @@ func autoMigrate(db *gorm.DB) error { &SensitiveWord{}, &AuditLog{}, + // 举报 + &Report{}, + // 日志相关 &OperationLog{}, // 操作日志 &LoginLog{}, // 登录日志 diff --git a/internal/model/report.go b/internal/model/report.go new file mode 100644 index 0000000..19e20ae --- /dev/null +++ b/internal/model/report.go @@ -0,0 +1,80 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// ReportTargetType 举报目标类型 +type ReportTargetType string + +const ( + ReportTargetTypePost ReportTargetType = "post" // 帖子 + ReportTargetTypeComment ReportTargetType = "comment" // 评论 + ReportTargetTypeMessage ReportTargetType = "message" // 消息 +) + +// ReportStatus 举报处理状态 +type ReportStatus string + +const ( + ReportStatusPending ReportStatus = "pending" // 待处理 + ReportStatusProcessing ReportStatus = "processing" // 处理中 + ReportStatusResolved ReportStatus = "resolved" // 已确认违规 + ReportStatusRejected ReportStatus = "rejected" // 已驳回 +) + +// ReportReason 举报原因类型 +type ReportReason string + +const ( + ReportReasonSpam ReportReason = "spam" // 垃圾广告 + ReportReasonInappropriate ReportReason = "inappropriate" // 违规内容 + ReportReasonHarassment ReportReason = "harassment" // 辱骂/攻击 + ReportReasonMisinformation ReportReason = "misinformation" // 虚假信息 + ReportReasonOther ReportReason = "other" // 其他 +) + +// Report 举报实体 +type Report struct { + ID string `json:"id" gorm:"type:varchar(36);primaryKey"` + ReporterID string `json:"reporter_id" gorm:"type:varchar(36);index;index:idx_reports_reporter_status,priority:1;not null"` + TargetType ReportTargetType `json:"target_type" gorm:"type:varchar(20);index;index:idx_reports_target_type_id,priority:1;index:idx_reports_target_status,priority:1;not null"` + TargetID string `json:"target_id" gorm:"type:varchar(36);index:idx_reports_target_type_id,priority:2;index:idx_reports_target,priority:1;not null"` + Reason ReportReason `json:"reason" gorm:"type:varchar(30);not null"` + Description string `json:"description" gorm:"type:text"` + Status ReportStatus `json:"status" gorm:"type:varchar(20);default:pending;index:idx_reports_status_created,priority:1;index:idx_reports_reporter_status,priority:2;index:idx_reports_target_status,priority:2"` + HandledBy *string `json:"handled_by" gorm:"type:varchar(36);index"` + HandledAt *time.Time `json:"handled_at" gorm:"type:timestamp"` + Result *string `json:"result" gorm:"type:text"` + + // 软删除 + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 时间戳 + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_reports_status_created,priority:2,sort:desc"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` +} + +// BeforeCreate 创建前生成UUID +func (r *Report) BeforeCreate(tx *gorm.DB) error { + if r.ID == "" { + r.ID = uuid.New().String() + } + return nil +} + +// TableName 指定表名 +func (Report) TableName() string { + return "reports" +} + +// ReportStats 举报统计(用于缓存或查询结果) +type ReportStats struct { + TargetType ReportTargetType + TargetID string + ReportCount int + IsHidden bool +} \ No newline at end of file diff --git a/internal/model/system_notification.go b/internal/model/system_notification.go index ef55db8..0b1608d 100644 --- a/internal/model/system_notification.go +++ b/internal/model/system_notification.go @@ -32,6 +32,10 @@ const ( SysNotifyGroupJoinApply SystemNotificationType = "group_join_apply" // 加群申请待审批 SysNotifyGroupJoinApproved SystemNotificationType = "group_join_approved" // 加群申请通过 SysNotifyGroupJoinRejected SystemNotificationType = "group_join_rejected" // 加群申请拒绝 + + // 举报相关 + SysNotifyReportResolved SystemNotificationType = "report_resolved" // 举报已确认违规 + SysNotifyReportRejected SystemNotificationType = "report_rejected" // 举报已驳回 ) // SystemNotificationExtra 额外数据 diff --git a/internal/repository/report_repo.go b/internal/repository/report_repo.go new file mode 100644 index 0000000..c283530 --- /dev/null +++ b/internal/repository/report_repo.go @@ -0,0 +1,212 @@ +package repository + +import ( + "carrot_bbs/internal/dto" + "carrot_bbs/internal/model" + "context" + + "gorm.io/gorm" +) + +// ReportRepository 举报仓储接口 +type ReportRepository interface { + // Create 创建举报 + Create(report *model.Report) error + // CreateWithContext 使用上下文创建举报(支持事务) + CreateWithContext(ctx context.Context, report *model.Report) error + // FindByID 根据ID查询举报 + FindByID(id string) (*model.Report, error) + // FindByIDWithContext 使用上下文查询举报 + FindByIDWithContext(ctx context.Context, id string) (*model.Report, error) + // List 查询举报列表 + List(query dto.AdminReportListQuery) ([]*model.Report, int64, error) + // GetReportCount 获取内容的举报次数 + GetReportCount(targetType model.ReportTargetType, targetID string) (int64, error) + // HasUserReported 检查用户是否已举报过该内容 + HasUserReported(reporterID string, targetType model.ReportTargetType, targetID string) (bool, error) + // GetPendingReportsByTarget 获取目标内容的待处理举报 + GetPendingReportsByTarget(targetType model.ReportTargetType, targetID string) ([]*model.Report, error) + // UpdateStatus 更新举报状态 + UpdateStatus(id string, status model.ReportStatus, handledBy string, result string) error + // UpdateStatusWithContext 使用上下文更新举报状态 + UpdateStatusWithContext(ctx context.Context, id string, status model.ReportStatus, handledBy string, result string) error + // BatchUpdateStatus 批量更新举报状态 + BatchUpdateStatus(ids []string, status model.ReportStatus, handledBy string, result string) (int64, error) + // BatchUpdateStatusWithContext 使用上下文批量更新举报状态 + BatchUpdateStatusWithContext(ctx context.Context, ids []string, status model.ReportStatus, handledBy string, result string) (int64, error) + // GetByIDsWithContext 使用上下文批量查询举报 + GetByIDsWithContext(ctx context.Context, ids []string) ([]*model.Report, error) +} + +// reportRepository 举报仓储实现 +type reportRepository struct { + db *gorm.DB +} + +// NewReportRepository 创建举报仓储 +func NewReportRepository(db *gorm.DB) ReportRepository { + return &reportRepository{db: db} +} + +// getDB 获取数据库实例(支持事务) +func (r *reportRepository) getDB(ctx context.Context) *gorm.DB { + if tx := GetTxFromContext(ctx); tx != nil { + return tx + } + return r.db +} + +// Create 创建举报 +func (r *reportRepository) Create(report *model.Report) error { + return r.db.Create(report).Error +} + +// CreateWithContext 使用上下文创建举报 +func (r *reportRepository) CreateWithContext(ctx context.Context, report *model.Report) error { + return r.getDB(ctx).Create(report).Error +} + +// FindByID 根据ID查询举报 +func (r *reportRepository) FindByID(id string) (*model.Report, error) { + var report model.Report + err := r.db.Where("id = ?", id).First(&report).Error + if err != nil { + return nil, err + } + return &report, nil +} + +// FindByIDWithContext 使用上下文查询举报 +func (r *reportRepository) FindByIDWithContext(ctx context.Context, id string) (*model.Report, error) { + var report model.Report + err := r.getDB(ctx).Where("id = ?", id).First(&report).Error + if err != nil { + return nil, err + } + return &report, nil +} + +// List 查询举报列表 +func (r *reportRepository) List(query dto.AdminReportListQuery) ([]*model.Report, int64, error) { + var reports []*model.Report + var total int64 + + db := r.db.Model(&model.Report{}) + + // 筛选条件 + if query.TargetType != "" { + db = db.Where("target_type = ?", query.TargetType) + } + if query.Status != "" { + db = db.Where("status = ?", query.Status) + } + if query.StartDate != "" { + db = db.Where("created_at >= ?", query.StartDate) + } + if query.EndDate != "" { + db = db.Where("created_at <= ?", query.EndDate+" 23:59:59") + } + if query.Keyword != "" { + keyword := "%" + query.Keyword + "%" + db = db.Where("description LIKE ? OR reason LIKE ?", keyword, keyword) + } + + // 获取总数 + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 分页查询 + offset := (query.Page - 1) * query.PageSize + if query.PageSize <= 0 { + query.PageSize = 20 + } + if query.Page <= 0 { + query.Page = 1 + } + + err := db.Order("created_at DESC"). + Offset(offset). + Limit(query.PageSize). + Find(&reports).Error + if err != nil { + return nil, 0, err + } + + return reports, total, nil +} + +// GetReportCount 获取内容的举报次数 +func (r *reportRepository) GetReportCount(targetType model.ReportTargetType, targetID string) (int64, error) { + var count int64 + err := r.db.Model(&model.Report{}). + Where("target_type = ? AND target_id = ?", targetType, targetID). + Count(&count).Error + return count, err +} + +// HasUserReported 检查用户是否已举报过该内容 +func (r *reportRepository) HasUserReported(reporterID string, targetType model.ReportTargetType, targetID string) (bool, error) { + var count int64 + err := r.db.Model(&model.Report{}). + Where("reporter_id = ? AND target_type = ? AND target_id = ?", reporterID, targetType, targetID). + Count(&count).Error + return count > 0, err +} + +// GetPendingReportsByTarget 获取目标内容的待处理举报 +func (r *reportRepository) GetPendingReportsByTarget(targetType model.ReportTargetType, targetID string) ([]*model.Report, error) { + var reports []*model.Report + err := r.db.Where("target_type = ? AND target_id = ? AND status = ?", targetType, targetID, model.ReportStatusPending). + Find(&reports).Error + return reports, err +} + +// UpdateStatus 更新举报状态 +func (r *reportRepository) UpdateStatus(id string, status model.ReportStatus, handledBy string, result string) error { + updates := map[string]interface{}{ + "status": status, + "handled_by": handledBy, + "result": result, + } + return r.db.Model(&model.Report{}).Where("id = ?", id).Updates(updates).Error +} + +// UpdateStatusWithContext 使用上下文更新举报状态 +func (r *reportRepository) UpdateStatusWithContext(ctx context.Context, id string, status model.ReportStatus, handledBy string, result string) error { + updates := map[string]interface{}{ + "status": status, + "handled_by": handledBy, + "result": result, + } + return r.getDB(ctx).Model(&model.Report{}).Where("id = ?", id).Updates(updates).Error +} + +// BatchUpdateStatus 批量更新举报状态 +func (r *reportRepository) BatchUpdateStatus(ids []string, status model.ReportStatus, handledBy string, result string) (int64, error) { + updates := map[string]interface{}{ + "status": status, + "handled_by": handledBy, + "result": result, + } + resultDB := r.db.Model(&model.Report{}).Where("id IN ?", ids).Updates(updates) + return resultDB.RowsAffected, resultDB.Error +} + +// BatchUpdateStatusWithContext 使用上下文批量更新举报状态 +func (r *reportRepository) BatchUpdateStatusWithContext(ctx context.Context, ids []string, status model.ReportStatus, handledBy string, result string) (int64, error) { + updates := map[string]interface{}{ + "status": status, + "handled_by": handledBy, + "result": result, + } + resultDB := r.getDB(ctx).Model(&model.Report{}).Where("id IN ?", ids).Updates(updates) + return resultDB.RowsAffected, resultDB.Error +} + +// GetByIDsWithContext 使用上下文批量查询举报 +func (r *reportRepository) GetByIDsWithContext(ctx context.Context, ids []string) ([]*model.Report, error) { + var reports []*model.Report + err := r.getDB(ctx).Where("id IN ?", ids).Find(&reports).Error + return reports, err +} \ No newline at end of file diff --git a/internal/router/router.go b/internal/router/router.go index 87f4e1e..ac6e377 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -35,6 +35,8 @@ type Router struct { qrcodeHandler *handler.QRCodeHandler materialHandler *handler.MaterialHandler callHandler *handler.CallHandler + reportHandler *handler.ReportHandler + adminReportHandler *handler.AdminReportHandler wsHandler *handler.WSHandler logService *service.LogService jwtService *service.JWTService @@ -67,6 +69,8 @@ func New( qrcodeHandler *handler.QRCodeHandler, materialHandler *handler.MaterialHandler, callHandler *handler.CallHandler, + reportHandler *handler.ReportHandler, + adminReportHandler *handler.AdminReportHandler, logService *service.LogService, activityService service.UserActivityService, casbinService service.CasbinService, @@ -102,6 +106,8 @@ func New( qrcodeHandler: qrcodeHandler, materialHandler: materialHandler, callHandler: callHandler, + reportHandler: reportHandler, + adminReportHandler: adminReportHandler, logService: logService, jwtService: jwtService, casbinService: casbinService, @@ -356,6 +362,15 @@ func (r *Router) setupRoutes() { messages.POST("/delete_msg", r.messageHandler.HandleDeleteMsg) } + // 举报路由 + if r.reportHandler != nil { + reports := v1.Group("/reports") + reports.Use(authMiddleware) + { + reports.POST("", r.reportHandler.Create) + } + } + // 通知路由 notifications := v1.Group("/notifications") { @@ -557,6 +572,14 @@ func (r *Router) setupRoutes() { admin.PUT("/materials/:id", r.materialHandler.AdminUpdateMaterial) admin.DELETE("/materials/:id", r.materialHandler.AdminDeleteMaterial) } + + // 举报管理 + if r.adminReportHandler != nil { + admin.GET("/reports", r.adminReportHandler.List) + admin.GET("/reports/:id", r.adminReportHandler.Detail) + admin.PUT("/reports/:id/handle", r.adminReportHandler.Handle) + admin.POST("/reports/batch-handle", r.adminReportHandler.BatchHandle) + } } } } diff --git a/internal/service/admin_report_service.go b/internal/service/admin_report_service.go new file mode 100644 index 0000000..c1a3906 --- /dev/null +++ b/internal/service/admin_report_service.go @@ -0,0 +1,442 @@ +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 +} \ No newline at end of file diff --git a/internal/service/report_service.go b/internal/service/report_service.go new file mode 100644 index 0000000..0fded44 --- /dev/null +++ b/internal/service/report_service.go @@ -0,0 +1,213 @@ +package service + +import ( + "context" + "errors" + "fmt" + + "carrot_bbs/internal/config" + "carrot_bbs/internal/dto" + "carrot_bbs/internal/model" + "carrot_bbs/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 + systemNotify SystemNotificationService + txManager repository.TransactionManager + logService *LogService + config *config.ReportConfig +} + +// NewReportService 创建举报服务 +func NewReportService( + reportRepo repository.ReportRepository, + postRepo repository.PostRepository, + commentRepo repository.CommentRepository, + messageRepo repository.MessageRepository, + userRepo repository.UserRepository, + systemNotify SystemNotificationService, + txManager repository.TransactionManager, + logService *LogService, + 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, + systemNotify: systemNotify, + 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.LogOperation(ctx, "create_report", "report", report.ID, reporterID, map[string]interface{}{ + "target_type": targetType, + "target_id": targetID, + "reason": reason, + }) + } + + 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 隐藏目标内容 +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 + } + if comment != nil && comment.Status != model.CommentStatusDeleted { + comment.Status = model.CommentStatusDeleted + return s.commentRepo.Update(comment) + } + case model.ReportTargetTypeMessage: + // 消息隐藏通过更新状态实现 + return s.messageRepo.UpdateMessageStatus(targetID, model.MessageStatusDeleted) + } + return nil +} \ No newline at end of file diff --git a/internal/wire/handler.go b/internal/wire/handler.go index 19e9384..b3a2ce9 100644 --- a/internal/wire/handler.go +++ b/internal/wire/handler.go @@ -28,6 +28,8 @@ var HandlerSet = wire.NewSet( handler.NewQRCodeHandler, handler.NewMaterialHandler, handler.NewCallHandler, + handler.NewReportHandler, + handler.NewAdminReportHandler, // 需要特殊处理的 Handler handler.NewUserHandler, diff --git a/internal/wire/repository.go b/internal/wire/repository.go index 03ad027..e5b7abd 100644 --- a/internal/wire/repository.go +++ b/internal/wire/repository.go @@ -27,6 +27,7 @@ var RepositorySet = wire.NewSet( repository.NewMaterialSubjectRepository, repository.NewMaterialFileRepository, repository.NewCallRepository, + repository.NewReportRepository, ProvideUserActivityRepository, // 日志相关仓储 diff --git a/internal/wire/service.go b/internal/wire/service.go index 817bc80..e19e307 100644 --- a/internal/wire/service.go +++ b/internal/wire/service.go @@ -56,6 +56,8 @@ var ServiceSet = wire.NewSet( ProvideHotRankWorker, ProvideMaterialService, ProvideCallService, + ProvideReportService, + ProvideAdminReportService, // 日志服务 ProvideAsyncLogManager, @@ -394,3 +396,32 @@ func ProvideCallService( ) service.CallService { return service.NewCallService(callRepo, wsHub, cfg, db) } + +// ProvideReportService 提供举报服务 +func ProvideReportService( + reportRepo repository.ReportRepository, + postRepo repository.PostRepository, + commentRepo repository.CommentRepository, + messageRepo repository.MessageRepository, + userRepo repository.UserRepository, + systemNotify service.SystemNotificationService, + txManager repository.TransactionManager, + logService *service.LogService, + cfg *config.Config, +) service.ReportService { + return service.NewReportService(reportRepo, postRepo, commentRepo, messageRepo, userRepo, systemNotify, txManager, logService, cfg) +} + +// ProvideAdminReportService 提供管理端举报服务 +func ProvideAdminReportService( + reportRepo repository.ReportRepository, + postRepo repository.PostRepository, + commentRepo repository.CommentRepository, + messageRepo repository.MessageRepository, + userRepo repository.UserRepository, + systemNotify service.SystemNotificationService, + txManager repository.TransactionManager, + logService *service.LogService, +) service.AdminReportService { + return service.NewAdminReportService(reportRepo, postRepo, commentRepo, messageRepo, userRepo, systemNotify, txManager, logService) +}