支持评论点赞与举报,并统一帖子/评论的举报入口交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 01:06:02 +08:00
parent c05cc472cf
commit b075495540
13 changed files with 595 additions and 90 deletions

View File

@@ -140,6 +140,7 @@ func (s *CommentService) ListByPost(postID, viewerID uint, isAdmin bool, postAut
rt.ContentHidden = true
}
}
s.fillLiked(visible, viewerID)
return visible, nil
}
@@ -259,6 +260,64 @@ func (s *CommentService) GetByID(id uint) (*model.Comment, error) {
return &c, nil
}
// fillLiked 批量标记当前用户是否已点赞
func (s *CommentService) fillLiked(comments []model.Comment, viewerID uint) {
if viewerID == 0 || len(comments) == 0 {
return
}
ids := make([]uint, 0, len(comments))
for _, c := range comments {
ids = append(ids, c.ID)
}
var likes []model.CommentLike
model.DB.Where("user_id = ? AND comment_id IN ?", viewerID, ids).Find(&likes)
likedSet := make(map[uint]struct{}, len(likes))
for _, l := range likes {
likedSet[l.CommentID] = struct{}{}
}
for i := range comments {
_, comments[i].Liked = likedSet[comments[i].ID]
}
}
// ToggleLike 切换评论点赞
func (s *CommentService) ToggleLike(userID, commentID uint) (liked bool, likeCount int, err error) {
var comment model.Comment
if err := model.DB.Select("id", "like_count").First(&comment, commentID).Error; err != nil {
return false, 0, ErrCommentNotFound
}
var like model.CommentLike
result := model.DB.Where("comment_id = ? AND user_id = ?", commentID, userID).Limit(1).Find(&like)
if result.Error != nil {
return false, 0, result.Error
}
if result.RowsAffected > 0 {
if err := model.DB.Delete(&like).Error; err != nil {
return false, 0, err
}
model.DB.Model(&model.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("CASE WHEN like_count > 0 THEN like_count - 1 ELSE 0 END"))
_ = model.DB.Select("like_count").First(&comment, commentID)
return false, comment.LikeCount, nil
}
like = model.CommentLike{CommentID: commentID, UserID: userID}
if err := model.DB.Create(&like).Error; err != nil {
return false, 0, err
}
model.DB.Model(&model.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("like_count + 1"))
_ = model.DB.Select("like_count").First(&comment, commentID)
return true, comment.LikeCount, nil
}
// IsLiked 用户是否已点赞该评论
func (s *CommentService) IsLiked(userID, commentID uint) bool {
if userID == 0 || commentID == 0 {
return false
}
var count int64
model.DB.Model(&model.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count)
return count > 0
}
// PendingCommentCount 待审评论数
func (s *CommentService) PendingCommentCount() (int64, error) {
var n int64

View File

@@ -12,9 +12,10 @@ import (
)
var (
ErrReportNotFound = errors.New("举报不存在")
ErrReportAlreadyExists = errors.New("你已举报过该帖子,请等待处理")
ErrCannotReportOwnPost = errors.New("不能举报自己的帖子")
ErrReportNotFound = errors.New("举报不存在")
ErrReportAlreadyExists = errors.New("你已举报过该内容,请等待处理")
ErrCannotReportOwnPost = errors.New("不能举报自己的帖子")
ErrCannotReportOwnComment = errors.New("不能举报自己的评论")
)
type ReportService struct {
@@ -22,6 +23,7 @@ type ReportService struct {
settings *ForumSettingsService
messages *MessageService
posts *PostService
comments *CommentService
}
func NewReportService(
@@ -29,8 +31,9 @@ func NewReportService(
settings *ForumSettingsService,
messages *MessageService,
posts *PostService,
comments *CommentService,
) *ReportService {
return &ReportService{filter: filter, settings: settings, messages: messages, posts: posts}
return &ReportService{filter: filter, settings: settings, messages: messages, posts: posts, comments: comments}
}
func normalizeReportReason(reason string) (string, error) {
@@ -107,6 +110,52 @@ func (s *ReportService) Create(reporterID, postID uint, reason, detail string) (
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
@@ -133,7 +182,9 @@ func (s *ReportService) ListAdmin(q ReportListQuery) ([]model.PostReport, int64,
var list []model.PostReport
err := db.Preload("Post", func(tx *gorm.DB) *gorm.DB {
return tx.Unscoped()
}).Preload("Post.User").Preload("Reporter").Preload("Handler").
}).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).
@@ -151,11 +202,11 @@ func (s *ReportService) PendingCount() (int64, error) {
}
type HandleReportInput struct {
ReportID uint
HandlerID uint
Action string // dismiss | resolve | reject_post
HandleNote string
RejectReason string // reject_post 时必填,发给作者
ReportID uint
HandlerID uint
Action string // dismiss | resolve | reject_post | reject_comment
HandleNote string
RejectReason string // reject_post / reject_comment 时发给作者
}
// Handle 处理举报
@@ -163,6 +214,8 @@ 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
}
@@ -188,6 +241,13 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
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":
@@ -195,6 +255,9 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
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("请填写拒绝原因(将私信通知作者)")
@@ -221,6 +284,37 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
&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("无效的处理操作")
}
@@ -232,16 +326,20 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
// 通知举报人处理结果
resultText := "已忽略"
if rep.Status == model.ReportStatusResolved {
if in.Action == "reject_post" {
switch in.Action {
case "reject_post":
resultText = "已核实并下架该帖"
} else {
case "reject_comment":
resultText = "已核实并处理该评论"
default:
resultText = "已处理"
}
}
content := fmt.Sprintf(
"你举报的帖子《%s》#%d已处理%s。",
postTitle, postID, 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
}
@@ -258,6 +356,8 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
_ = 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
}