Files
jiang13-forum/services/report.go
freefire 9fe299a45f refactor: Gitea 式目录改组,移除本分支 SPA 与杂项产物
将 model/service/handler/middleware 迁至 models/services/routers/api/modules/auth,并删除 frontend、embed_static、scripts 及误入库缓存/二进制。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 03:54:56 +08:00

364 lines
9.6 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 (
"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
}