package service import ( "errors" "fmt" "strings" "time" "unicode/utf8" "git.iioio.com/freefire/jiang13-forum/model" "gorm.io/gorm" ) var ( ErrReportNotFound = errors.New("举报不存在") ErrReportAlreadyExists = errors.New("你已举报过该内容,请等待处理") ErrCannotReportOwnPost = errors.New("不能举报自己的帖子") ErrCannotReportOwnComment = errors.New("不能举报自己的评论") ) type ReportService struct { filter *SensitiveFilter settings *ForumSettingsService messages *MessageService posts *PostService comments *CommentService } func NewReportService( filter *SensitiveFilter, settings *ForumSettingsService, messages *MessageService, posts *PostService, comments *CommentService, ) *ReportService { return &ReportService{filter: filter, settings: settings, messages: messages, posts: posts, comments: comments} } func normalizeReportReason(reason string) (string, error) { switch strings.TrimSpace(reason) { case model.ReportReasonSpam, model.ReportReasonAbuse, model.ReportReasonIllegal, model.ReportReasonIrrelevant, model.ReportReasonOther: return reason, nil default: return "", errors.New("请选择有效的举报原因") } } func ReportReasonLabel(reason string) string { switch reason { case model.ReportReasonSpam: return "垃圾广告" case model.ReportReasonAbuse: return "人身攻击 / 辱骂" case model.ReportReasonIllegal: return "违法违规" case model.ReportReasonIrrelevant: return "内容无关 / 灌水" case model.ReportReasonOther: return "其他" default: return reason } } // Create 用户举报帖子 func (s *ReportService) Create(reporterID, postID uint, reason, detail string) (*model.PostReport, error) { reason, err := normalizeReportReason(reason) if err != nil { return nil, err } detail = strings.TrimSpace(detail) if utf8.RuneCountInString(detail) > 500 { return nil, errors.New("补充说明过长") } if s.filter != nil && detail != "" { detail = s.filter.Filter(detail) } var post model.Post if err := model.DB.Select("id", "user_id", "title").First(&post, postID).Error; err != nil { return nil, ErrPostNotFound } if post.UserID == reporterID { return nil, ErrCannotReportOwnPost } var existing int64 model.DB.Model(&model.PostReport{}). Where("post_id = ? AND reporter_id = ? AND status = ?", postID, reporterID, model.ReportStatusPending). Count(&existing) if existing > 0 { return nil, ErrReportAlreadyExists } rep := &model.PostReport{ PostID: postID, ReporterID: reporterID, Reason: reason, Detail: detail, Status: model.ReportStatusPending, } if err := model.DB.Create(rep).Error; err != nil { return nil, err } _ = model.DB.Preload("Post").Preload("Reporter").First(rep, rep.ID).Error return rep, nil } // CreateCommentReport 用户举报评论 func (s *ReportService) CreateCommentReport(reporterID, commentID uint, reason, detail string) (*model.PostReport, error) { reason, err := normalizeReportReason(reason) if err != nil { return nil, err } detail = strings.TrimSpace(detail) if utf8.RuneCountInString(detail) > 500 { return nil, errors.New("补充说明过长") } if s.filter != nil && detail != "" { detail = s.filter.Filter(detail) } comment, err := s.comments.GetByID(commentID) if err != nil { return nil, err } if comment.UserID > 0 && comment.UserID == reporterID { return nil, ErrCannotReportOwnComment } var existing int64 model.DB.Model(&model.PostReport{}). Where("comment_id = ? AND reporter_id = ? AND status = ?", commentID, reporterID, model.ReportStatusPending). Count(&existing) if existing > 0 { return nil, ErrReportAlreadyExists } cid := commentID rep := &model.PostReport{ PostID: comment.PostID, CommentID: &cid, ReporterID: reporterID, Reason: reason, Detail: detail, Status: model.ReportStatusPending, } if err := model.DB.Create(rep).Error; err != nil { return nil, err } _ = model.DB.Preload("Post").Preload("Comment").Preload("Reporter").First(rep, rep.ID).Error return rep, nil } type ReportListQuery struct { Status string Page int Size int } // ListAdmin 管理员举报列表 func (s *ReportService) ListAdmin(q ReportListQuery) ([]model.PostReport, int64, error) { if q.Page < 1 { q.Page = 1 } q.Size = s.settings.NormalizePageSize(q.Size) db := model.DB.Model(&model.PostReport{}) if q.Status != "" && q.Status != "all" { db = db.Where("status = ?", q.Status) } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } var list []model.PostReport err := db.Preload("Post", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).Preload("Post.User").Preload("Comment", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).Preload("Comment.User").Preload("Reporter").Preload("Handler"). Order("CASE WHEN status = 'pending' THEN 0 ELSE 1 END, id DESC"). Offset((q.Page - 1) * q.Size). Limit(q.Size). Find(&list).Error return list, total, err } // PendingCount 待处理举报数 func (s *ReportService) PendingCount() (int64, error) { var n int64 err := model.DB.Model(&model.PostReport{}). Where("status = ?", model.ReportStatusPending). Count(&n).Error return n, err } type HandleReportInput struct { ReportID uint HandlerID uint Action string // dismiss | resolve | reject_post | reject_comment HandleNote string RejectReason string // reject_post / reject_comment 时发给作者 } // Handle 处理举报 func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) { var rep model.PostReport if err := model.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).Preload("Comment", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).First(&rep, in.ReportID).Error; err != nil { return nil, ErrReportNotFound } if rep.Status != model.ReportStatusPending { return nil, errors.New("该举报已处理") } note := strings.TrimSpace(in.HandleNote) if utf8.RuneCountInString(note) > 500 { return nil, errors.New("处理备注过长") } now := time.Now() handlerID := in.HandlerID rep.HandlerID = &handlerID rep.HandleNote = note rep.HandledAt = &now postID := rep.PostID postTitle := "" authorID := uint(0) if rep.Post.ID > 0 { postTitle = rep.Post.Title authorID = rep.Post.UserID } isCommentReport := rep.CommentID != nil && *rep.CommentID > 0 commentAuthorID := uint(0) commentFloor := 0 if isCommentReport && rep.Comment != nil { commentAuthorID = rep.Comment.UserID commentFloor = rep.Comment.Floor } switch in.Action { case "dismiss": rep.Status = model.ReportStatusDismissed case "resolve": rep.Status = model.ReportStatusResolved case "reject_post": if isCommentReport { return nil, errors.New("评论举报请使用「拒绝该评论」") } reason := strings.TrimSpace(in.RejectReason) if reason == "" { return nil, errors.New("请填写拒绝原因(将私信通知作者)") } if utf8.RuneCountInString(reason) > 1000 { return nil, errors.New("拒绝原因过长") } if err := s.posts.SetStatus(postID, model.ContentStatusRejected); err != nil { return nil, err } rep.Status = model.ReportStatusResolved if note == "" { rep.HandleNote = "已拒绝该帖并通知作者" } if authorID > 0 { pid := postID rid := rep.ID _, _ = s.messages.SendSystem( authorID, fmt.Sprintf("帖子《%s》未通过审核", postTitle), FormatRejectContent(postTitle, postID, reason), model.MessageKindReject, &pid, &rid, ) } case "reject_comment": if !isCommentReport { return nil, errors.New("仅评论举报可拒绝评论") } reason := strings.TrimSpace(in.RejectReason) if reason == "" { return nil, errors.New("请填写拒绝原因(将私信通知作者)") } if utf8.RuneCountInString(reason) > 1000 { return nil, errors.New("拒绝原因过长") } if err := s.comments.SetStatus(*rep.CommentID, model.ContentStatusRejected); err != nil { return nil, err } rep.Status = model.ReportStatusResolved if note == "" { rep.HandleNote = "已拒绝该评论并通知作者" } if commentAuthorID > 0 { pid := postID rid := rep.ID body := fmt.Sprintf("你在帖子《%s》下的评论(#%d)未通过审核。\n\n原因:\n%s", postTitle, commentFloor, reason) _, _ = s.messages.SendSystem( commentAuthorID, fmt.Sprintf("评论未通过审核 · 《%s》", postTitle), body, model.MessageKindReject, &pid, &rid, ) } default: return nil, errors.New("无效的处理操作") } if err := model.DB.Save(&rep).Error; err != nil { return nil, err } // 通知举报人处理结果 resultText := "已忽略" if rep.Status == model.ReportStatusResolved { switch in.Action { case "reject_post": resultText = "已核实并下架该帖" case "reject_comment": resultText = "已核实并处理该评论" default: resultText = "已处理" } } targetDesc := fmt.Sprintf("帖子《%s》(#%d)", postTitle, postID) if isCommentReport { targetDesc = fmt.Sprintf("帖子《%s》下的评论(#%d)", postTitle, commentFloor) } content := fmt.Sprintf("你举报的%s已处理:%s。", targetDesc, resultText) if note != "" { content += "\n\n管理员备注:\n" + note } pid := postID rid := rep.ID _, _ = s.messages.SendSystem( rep.ReporterID, "举报处理结果通知", content, model.MessageKindReportResult, &pid, &rid, ) _ = model.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).Preload("Comment", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).Preload("Reporter").Preload("Handler").First(&rep, rep.ID).Error return &rep, nil }