feat: 添加举报功能支持
Some checks failed
Build Backend / build (push) Failing after 5m19s
Build Backend / build-docker (push) Has been skipped

- 新增 Report 数据模型、DTO、Repository、Service 层
- 实现用户端举报 API (POST /api/v1/reports)
- 实现管理端举报管理 API (列表、详情、处理、批量处理)
- 添加举报自动隐藏机制(达到阈值自动隐藏内容)
- 集成系统通知(举报处理结果通知)
- 更新路由和 Wire 依赖注入配置

Made-with: Cursor
This commit is contained in:
lafay
2026-03-29 20:18:36 +08:00
parent 2205b8ad04
commit 5f7b02ee8e
16 changed files with 1426 additions and 0 deletions

187
internal/dto/report.go Normal file
View File

@@ -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
}