package controller import ( "errors" "net/http" "strconv" "github.com/gin-gonic/gin" pbModeration "github.com/topfans/backend/pkg/proto/moderation" modSvc "github.com/topfans/backend/services/moderationService/service" "github.com/topfans/backend/gateway/dto" "github.com/topfans/backend/gateway/pkg/response" ) // modSpecErrCode 举报/反馈业务错误码(spec §7,50001-50019) // 配套 api.js 的 REPORT_ERROR_MSG 映射表使用 const ( modCodeCategoryInvalid = 50001 // 举报分类不存在或已停用 modCodeFeedbackCatInvalid = 50002 // 反馈分类不存在或已停用 modCodeTargetNotFound = 50003 // 目标对象不存在 modCodeDuplicateReport = 50004 // 已举报过 modCodeDescriptionTooLong = 50005 // 描述过长 modCodeTooManyEvidence = 50006 // 证据图超限 modCodeReportNotFound = 50007 // 工单不存在 modCodeReportClaimed = 50008 // 工单已被他人认领 modCodeReportClosed = 50009 // 工单已结案 modCodeSelfReport = 50011 // 不能举报自己 modCodeRateLimited = 50012 // 提交过于频繁 modCodeInvalidStateMig = 50014 // 状态机非法迁移 modCodeTargetTypeUnsupported = 50016 // target_type 不支持 ) // modErrMapping 错误 → 业务错误码的映射表 // 与 modSpecErrCode 常量集中维护 var modErrMapping = map[error]int{ modSvc.ErrCategoryInvalid: modCodeCategoryInvalid, modSvc.ErrFeedbackCatInvalid: modCodeFeedbackCatInvalid, modSvc.ErrTargetNotFound: modCodeTargetNotFound, modSvc.ErrDuplicateReport: modCodeDuplicateReport, modSvc.ErrDescriptionTooLong: modCodeDescriptionTooLong, modSvc.ErrTooManyEvidence: modCodeTooManyEvidence, modSvc.ErrReportNotFound: modCodeReportNotFound, modSvc.ErrReportClaimed: modCodeReportClaimed, modSvc.ErrReportClosed: modCodeReportClosed, modSvc.ErrSelfReport: modCodeSelfReport, modSvc.ErrRateLimited: modCodeRateLimited, modSvc.ErrInvalidStateMigration: modCodeInvalidStateMig, modSvc.ErrTargetTypeUnsupported: modCodeTargetTypeUnsupported, } // modSpecErrMsg 业务错误码 → spec 文案(spec §7 中文版) // 与 api.js 的 REPORT_ERROR_MSG 同源;后端优先返回这里的中文,前端做兼容兜底 var modSpecErrMsg = map[int]string{ modCodeCategoryInvalid: "举报分类不存在或已停用", modCodeFeedbackCatInvalid: "反馈分类不存在或已停用", modCodeTargetNotFound: "目标对象不存在", modCodeDuplicateReport: "您已举报过该对象", modCodeDescriptionTooLong: "描述过长(最多 500 字)", modCodeTooManyEvidence: "证据图超限(最多 5 张)", modCodeReportNotFound: "工单不存在", modCodeReportClaimed: "工单已被他人认领", modCodeReportClosed: "工单已结案,无法再操作", modCodeSelfReport: "不能举报自己", modCodeRateLimited: "提交过于频繁,请稍后再试", modCodeInvalidStateMig: "工单状态不允许此操作", modCodeTargetTypeUnsupported: "举报对象类型不支持", } // mapModError 把 service 层错误翻译为 (业务错误码, 中文 message)。 // 命中则返回 (code, msg, true);未命中返回 (500, "", false) 由调用方走 500 兜底 func mapModError(err error) (int, string, bool) { if err == nil { return 0, "", false } for target, code := range modErrMapping { if errors.Is(err, target) { return code, modSpecErrMsg[code], true } } return http.StatusInternalServerError, "", false } // ModerationController 举报反馈控制器(spec §5.1 客户端 API) type ModerationController struct { modService pbModeration.ModerationService } // NewModerationController 创建举报反馈控制器(service 由 gateway 注入) func NewModerationController() *ModerationController { return &ModerationController{} } // SetService 由 gateway 注入 func (ctrl *ModerationController) SetService(svc pbModeration.ModerationService) { ctrl.modService = svc } // getUserID 从 gin context 提取 user_id(key 由 auth middleware 写入) func getUserID(c *gin.Context) int64 { if v, ok := c.Get("user_id"); ok { if id, ok := v.(int64); ok { return id } } return 0 } // GetReportCategories 获取举报分类 func (ctrl *ModerationController) GetReportCategories(c *gin.Context) { resp, err := ctrl.modService.GetReportCategories( c.Request.Context(), &pbModeration.GetReportCategoriesRequest{}, ) if err != nil { response.Error(c, http.StatusInternalServerError, err.Error()) return } response.Success(c, dto.NewReportCategoryList(resp.Categories)) } // GetFeedbackCategories 获取反馈分类 func (ctrl *ModerationController) GetFeedbackCategories(c *gin.Context) { resp, err := ctrl.modService.GetFeedbackCategories( c.Request.Context(), &pbModeration.GetFeedbackCategoriesRequest{}, ) if err != nil { response.Error(c, http.StatusInternalServerError, err.Error()) return } response.Success(c, dto.NewFeedbackCategoryList(resp.Categories)) } // SubmitReport 提交举报 func (ctrl *ModerationController) SubmitReport(c *gin.Context) { var req dto.SubmitReportRequestDTO if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "参数错误: "+err.Error()) return } userID := getUserID(c) resp, err := ctrl.modService.SubmitReport(c.Request.Context(), &pbModeration.SubmitReportRequest{ ReporterId: userID, TargetType: req.TargetType, TargetId: req.TargetID, CategoryCode: req.CategoryCode, Description: req.Description, IsAnonymous: req.IsAnonymous, EvidenceKeys: req.EvidenceKeys, ClientIp: c.ClientIP(), DeviceFp: c.GetHeader("X-Device-Fingerprint"), }) if err != nil { if code, msg, ok := mapModError(err); ok { response.ErrorWithCode(c, code, msg) return } response.Error(c, http.StatusInternalServerError, err.Error()) return } response.Success(c, dto.NewSubmitReportResponse(resp)) } // ListMyReports 我的举报列表 func (ctrl *ModerationController) ListMyReports(c *gin.Context) { userID := getUserID(c) page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) resp, err := ctrl.modService.ListMyReports(c.Request.Context(), &pbModeration.ListMyReportsRequest{ ReporterId: userID, Status: c.Query("status"), Page: int32(page), PageSize: int32(pageSize), }) if err != nil { if code, msg, ok := mapModError(err); ok { response.ErrorWithCode(c, code, msg) return } response.Error(c, http.StatusInternalServerError, err.Error()) return } response.Success(c, dto.NewListMyReportsResponse(resp)) } // GetReportDetail 举报详情 func (ctrl *ModerationController) GetReportDetail(c *gin.Context) { userID := getUserID(c) id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "无效的 ID") return } resp, err := ctrl.modService.GetReport(c.Request.Context(), &pbModeration.GetReportRequest{ ReporterId: userID, Id: id, }) if err != nil { if code, msg, ok := mapModError(err); ok { response.ErrorWithCode(c, code, msg) return } response.Error(c, http.StatusInternalServerError, err.Error()) return } response.Success(c, dto.NewGetReportResponse(resp)) } // SubmitFeedback 提交反馈 func (ctrl *ModerationController) SubmitFeedback(c *gin.Context) { var req dto.SubmitFeedbackRequestDTO if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "参数错误: "+err.Error()) return } userID := getUserID(c) resp, err := ctrl.modService.SubmitFeedback(c.Request.Context(), &pbModeration.SubmitFeedbackRequest{ UserId: userID, StarId: req.StarID, CategoryCode: req.CategoryCode, Title: req.Title, Content: req.Content, Contact: req.Contact, IsAnonymous: req.IsAnonymous, EvidenceKeys: req.EvidenceKeys, }) if err != nil { response.Error(c, http.StatusInternalServerError, err.Error()) return } response.Success(c, dto.NewSubmitFeedbackResponse(resp)) } // ListMyFeedbacks 我的反馈列表 func (ctrl *ModerationController) ListMyFeedbacks(c *gin.Context) { userID := getUserID(c) page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) resp, err := ctrl.modService.ListMyFeedbacks(c.Request.Context(), &pbModeration.ListMyFeedbacksRequest{ UserId: userID, Status: c.Query("status"), Page: int32(page), PageSize: int32(pageSize), }) if err != nil { if code, msg, ok := mapModError(err); ok { response.ErrorWithCode(c, code, msg) return } response.Error(c, http.StatusInternalServerError, err.Error()) return } response.Success(c, dto.NewListMyFeedbacksResponse(resp)) } // GetFeedbackDetail 反馈详情 func (ctrl *ModerationController) GetFeedbackDetail(c *gin.Context) { userID := getUserID(c) id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "无效的 ID") return } resp, err := ctrl.modService.GetFeedback(c.Request.Context(), &pbModeration.GetFeedbackRequest{ UserId: userID, Id: id, }) if err != nil { if code, msg, ok := mapModError(err); ok { response.ErrorWithCode(c, code, msg) return } response.Error(c, http.StatusInternalServerError, err.Error()) return } response.Success(c, dto.NewGetFeedbackResponse(resp)) }