package service import ( "errors" "time" "github.com/freefire/jiang13-bbs/model" "gorm.io/gorm" ) // 内容审核相关错误 var ( ErrModerationForbidden = errors.New("无权审核该板块的内容") ErrNotPending = errors.New("该内容不在待审核状态") ) // ModerationService 帖子/评论审核队列 type ModerationService struct { db *gorm.DB notif *NotificationService } func NewModerationService(db *gorm.DB, notif *NotificationService) *ModerationService { return &ModerationService{db: db, notif: notif} } // PendingCommentItem 待审评论列表项(带上所属帖子与板块,便于按板块隔离与展示) type PendingCommentItem struct { ID uint `json:"id"` PostID uint `json:"post_id"` PostTitle string `json:"post_title"` BoardID uint `json:"board_id"` Board model.Board `json:"board"` Content string `json:"content"` CreatedAt time.Time `json:"created_at"` User model.User `json:"user"` } // boardScope 板块管理员只能看到/操作被授权板块;管理员及以上不限 func boardScope(q *gorm.DB, actor *Actor) *gorm.DB { if actor.Role == model.RoleBoardAdmin { return q.Where("board_id IN ?", actor.BoardIDs) } return q } // PendingPosts 待审核帖子分页 func (s *ModerationService) PendingPosts(actor *Actor, page, size int) ([]model.Post, int64, error) { if page < 1 { page = 1 } if size < 1 || size > 50 { size = 15 } q := s.db.Model(&model.Post{}).Where("status = ?", model.ContentStatusPending) q = boardScope(q, actor) var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err } var posts []model.Post if err := q.Order("created_at ASC"). Offset((page - 1) * size).Limit(size). Preload("Board").Preload("User"). Find(&posts).Error; err != nil { return nil, 0, err } return posts, total, nil } // PendingComments 待审核评论分页(JOIN 帖子取板块与标题) func (s *ModerationService) PendingComments(actor *Actor, page, size int) ([]PendingCommentItem, int64, error) { if page < 1 { page = 1 } if size < 1 || size > 50 { size = 15 } base := s.db.Table("comments"). Joins("JOIN posts ON posts.id = comments.post_id"). Where("comments.status = ? AND comments.deleted_at IS NULL AND posts.deleted_at IS NULL", model.ContentStatusPending) if actor.Role == model.RoleBoardAdmin { base = base.Where("posts.board_id IN ?", actor.BoardIDs) } var total int64 if err := base.Count(&total).Error; err != nil { return nil, 0, err } type row struct { ID uint PostID uint PostTitle string BoardID uint Content string CreatedAt time.Time UserID uint } var rows []row if err := base.Select("comments.id, comments.post_id, posts.title AS post_title, posts.board_id, " + "comments.content, comments.created_at, comments.user_id"). Order("comments.created_at ASC"). Offset((page - 1) * size).Limit(size). Scan(&rows).Error; err != nil { return nil, 0, err } items := make([]PendingCommentItem, 0, len(rows)) if len(rows) > 0 { userIDs := map[uint]bool{} boardIDs := map[uint]bool{} for _, r := range rows { userIDs[r.UserID] = true if r.BoardID > 0 { boardIDs[r.BoardID] = true } } var users []model.User s.db.Where("id IN ?", keys(userIDs)).Find(&users) userMap := map[uint]model.User{} for _, u := range users { userMap[u.ID] = u } var boards []model.Board s.db.Where("id IN ?", keys(boardIDs)).Find(&boards) boardMap := map[uint]model.Board{} for _, b := range boards { boardMap[b.ID] = b } for _, r := range rows { items = append(items, PendingCommentItem{ ID: r.ID, PostID: r.PostID, PostTitle: r.PostTitle, BoardID: r.BoardID, Board: boardMap[r.BoardID], Content: r.Content, CreatedAt: r.CreatedAt, User: userMap[r.UserID], }) } } return items, total, nil } // PendingCounts 当前操作者可见的待审数量(导航角标) func (s *ModerationService) PendingCounts(actor *Actor) (posts int64, comments int64) { q := s.db.Model(&model.Post{}).Where("status = ?", model.ContentStatusPending) boardScope(q, actor).Count(&posts) cq := s.db.Table("comments"). Joins("JOIN posts ON posts.id = comments.post_id"). Where("comments.status = ? AND comments.deleted_at IS NULL AND posts.deleted_at IS NULL", model.ContentStatusPending) if actor.Role == model.RoleBoardAdmin { cq = cq.Where("posts.board_id IN ?", actor.BoardIDs) } cq.Count(&comments) return } // ApprovePost 帖子审核通过;成功时返回板块 ID 供 feed 广播 func (s *ModerationService) ApprovePost(actor *Actor, id uint) (boardID uint, err error) { err = s.db.Transaction(func(tx *gorm.DB) error { var post model.Post if err := tx.First(&post, id).Error; err != nil { return err } if !actor.CanModerateBoard(post.BoardID) { return ErrModerationForbidden } if post.Status != model.ContentStatusPending { return ErrNotPending } updates := map[string]interface{}{ "status": model.ContentStatusPublished, } // 悬赏/抽奖:首次公开发布时补写 ends_at if raw, ok := EnsureDeadlineOnPublish(post.TypeMeta, post.PostType, time.Now().UTC()); ok { updates["type_meta"] = raw } if err := tx.Model(&post).Updates(updates).Error; err != nil { return err } boardID = post.BoardID s.notif.Create(post.UserID, actor.ID, model.NotificationTypeApproved, post.ID, 0, "你的帖子《"+post.Title+"》已通过审核") return nil }) return boardID, err } // RejectPost 帖子审核拒绝(标记 rejected,不物理删除;作者仍可在详情页看到结果) func (s *ModerationService) RejectPost(actor *Actor, id uint) error { return s.db.Transaction(func(tx *gorm.DB) error { var post model.Post if err := tx.First(&post, id).Error; err != nil { return err } if !actor.CanModerateBoard(post.BoardID) { return ErrModerationForbidden } if post.Status != model.ContentStatusPending { return ErrNotPending } if err := tx.Model(&post).Update("status", model.ContentStatusRejected).Error; err != nil { return err } s.notif.Create(post.UserID, actor.ID, model.NotificationTypeRejected, post.ID, 0, "你的帖子《"+post.Title+"》未通过审核") return nil }) } // ApproveComment 评论审核通过(通过时才计入帖子评论数);成功时返回帖子 ID 与板块 ID func (s *ModerationService) ApproveComment(actor *Actor, id uint) (postID, boardID uint, err error) { err = s.db.Transaction(func(tx *gorm.DB) error { var cm model.Comment if err := tx.First(&cm, id).Error; err != nil { return err } var post model.Post if err := tx.Select("id", "board_id", "title", "user_id").First(&post, cm.PostID).Error; err != nil { return err } if !actor.CanModerateBoard(post.BoardID) { return ErrModerationForbidden } if cm.Status != model.ContentStatusPending { return ErrNotPending } if err := tx.Model(&cm).Update("status", model.ContentStatusPublished).Error; err != nil { return err } if err := tx.Model(&model.Post{}).Where("id = ?", cm.PostID). UpdateColumn("comment_count", gorm.Expr("comment_count + 1")).Error; err != nil { return err } postID = cm.PostID boardID = post.BoardID s.notif.Create(cm.UserID, actor.ID, model.NotificationTypeApproved, cm.PostID, cm.ID, "你在帖子《"+post.Title+"》下的评论已通过审核") // 评论此时才公开,补发送业务通知(以评论者为 actor): // 主评论通知帖子作者;子回复通知父评论作者 if cm.ParentID == nil { s.notif.Create(post.UserID, cm.UserID, model.NotificationTypeComment, cm.PostID, cm.ID, cm.Content) } else { var parent model.Comment if err := tx.Select("user_id").First(&parent, *cm.ParentID).Error; err == nil { s.notif.Create(parent.UserID, cm.UserID, model.NotificationTypeReply, cm.PostID, cm.ID, cm.Content) } } return nil }) return postID, boardID, err } // RejectComment 评论审核拒绝 func (s *ModerationService) RejectComment(actor *Actor, id uint) error { return s.db.Transaction(func(tx *gorm.DB) error { var cm model.Comment if err := tx.First(&cm, id).Error; err != nil { return err } var post model.Post if err := tx.Select("id", "board_id", "title").First(&post, cm.PostID).Error; err != nil { return err } if !actor.CanModerateBoard(post.BoardID) { return ErrModerationForbidden } if cm.Status != model.ContentStatusPending { return ErrNotPending } if err := tx.Model(&cm).Update("status", model.ContentStatusRejected).Error; err != nil { return err } s.notif.Create(cm.UserID, actor.ID, model.NotificationTypeRejected, cm.PostID, cm.ID, "你在帖子《"+post.Title+"》下的评论未通过审核") return nil }) } // UserHasApprovedContent 用户是否已有至少一篇已发布帖子或一条已发布评论(即曾通过审核或免审直发) func (s *ModerationService) UserHasApprovedContent(userID uint) (bool, error) { var n int64 if err := s.db.Model(&model.Post{}). Where("user_id = ? AND status = ?", userID, model.ContentStatusPublished). Limit(1).Count(&n).Error; err != nil { return false, err } if n > 0 { return true, nil } n = 0 if err := s.db.Model(&model.Comment{}). Where("user_id = ? AND status = ?", userID, model.ContentStatusPublished). Limit(1).Count(&n).Error; err != nil { return false, err } return n > 0, nil } func keys(m map[uint]bool) []uint { out := make([]uint, 0, len(m)) for k := range m { out = append(out, k) } return out }