feat(posts): 类型特殊块移至正文下,采纳答案卡与中奖楼层按评论标记

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 08:33:00 +08:00
parent 92695c2143
commit 44fdcace4c
8 changed files with 410 additions and 83 deletions

View File

@@ -327,13 +327,29 @@ func EnsureDeadlineOnPublish(typeMeta, postType string, publishedAt time.Time) (
// ---- 详情附加状态 ----
// AcceptedAnswer 问答/悬赏已采纳答案(详情页正文下展示;与评论分页无关)
type AcceptedAnswer struct {
ID uint `json:"id"`
Floor int `json:"floor"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
Deleted bool `json:"deleted,omitempty"`
User struct {
ID uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
} `json:"user"`
}
type QuestionState struct {
Solved bool `json:"solved"`
AcceptedCommentID uint `json:"accepted_comment_id"`
AcceptedFloor int `json:"accepted_floor"`
CanAccept bool `json:"can_accept"`
CanSolve bool `json:"can_solve"`
CanReopen bool `json:"can_reopen"`
Solved bool `json:"solved"`
AcceptedCommentID uint `json:"accepted_comment_id"`
AcceptedFloor int `json:"accepted_floor"`
AcceptedAnswer *AcceptedAnswer `json:"accepted_answer,omitempty"`
CanAccept bool `json:"can_accept"`
CanSolve bool `json:"can_solve"`
CanReopen bool `json:"can_reopen"`
}
type PollVoter struct {
@@ -365,23 +381,26 @@ type PollState struct {
}
type BountyState struct {
Points int `json:"points"`
AcceptedCommentID uint `json:"accepted_comment_id"`
AcceptedFloor int `json:"accepted_floor"`
Escrowed bool `json:"escrowed"`
Refunded bool `json:"refunded"`
Expired bool `json:"expired"`
EndsAt string `json:"ends_at,omitempty"`
Settled bool `json:"settled"`
CanAccept bool `json:"can_accept"`
CanRefund bool `json:"can_refund"`
Points int `json:"points"`
AcceptedCommentID uint `json:"accepted_comment_id"`
AcceptedFloor int `json:"accepted_floor"`
AcceptedAnswer *AcceptedAnswer `json:"accepted_answer,omitempty"`
Escrowed bool `json:"escrowed"`
Refunded bool `json:"refunded"`
Expired bool `json:"expired"`
EndsAt string `json:"ends_at,omitempty"`
Settled bool `json:"settled"`
CanAccept bool `json:"can_accept"`
CanRefund bool `json:"can_refund"`
}
type LotteryEntrant struct {
ID uint `json:"id"`
Nickname string `json:"nickname"`
Username string `json:"username"`
Avatar string `json:"avatar"`
ID uint `json:"id"`
Nickname string `json:"nickname"`
Username string `json:"username"`
Avatar string `json:"avatar"`
CommentID uint `json:"comment_id"` // 中奖楼层(该用户最早主评),每人仅一条
Floor int `json:"floor"`
}
type LotteryState struct {
@@ -424,10 +443,12 @@ func (s *PostService) buildQuestionState(post *model.Post, canManage bool) *Ques
if err != nil || m == nil {
m = &QuestionMeta{}
}
floor := FloorNumber(s.db, post.ID, m.AcceptedCommentID)
return &QuestionState{
Solved: m.Solved,
AcceptedCommentID: m.AcceptedCommentID,
AcceptedFloor: FloorNumber(s.db, post.ID, m.AcceptedCommentID),
AcceptedFloor: floor,
AcceptedAnswer: s.loadAcceptedAnswer(post.ID, m.AcceptedCommentID, floor),
CanAccept: canManage && !m.Solved,
CanSolve: canManage && !m.Solved,
CanReopen: canManage && m.Solved,
@@ -530,10 +551,12 @@ func (s *PostService) buildBountyState(post *model.Post, canManage bool) *Bounty
return nil
}
settled := m.AcceptedCommentID > 0 || m.Refunded || m.Expired
floor := FloorNumber(s.db, post.ID, m.AcceptedCommentID)
return &BountyState{
Points: m.Points,
AcceptedCommentID: m.AcceptedCommentID,
AcceptedFloor: FloorNumber(s.db, post.ID, m.AcceptedCommentID),
AcceptedFloor: floor,
AcceptedAnswer: s.loadAcceptedAnswer(post.ID, m.AcceptedCommentID, floor),
Escrowed: m.Escrowed,
Refunded: m.Refunded,
Expired: m.Expired,
@@ -544,6 +567,40 @@ func (s *PostService) buildBountyState(post *model.Post, canManage bool) *Bounty
}
}
// loadAcceptedAnswer 加载已采纳评论快照(公开可见正文);软删则清空正文并标记 deleted。
func (s *PostService) loadAcceptedAnswer(postID, commentID uint, floor int) *AcceptedAnswer {
if postID == 0 || commentID == 0 {
return nil
}
var c model.Comment
err := s.db.Unscoped().Preload("User").
Where("id = ? AND post_id = ?", commentID, postID).
First(&c).Error
if err != nil {
return nil
}
// 待审评论不当作公开答案资料
if c.Status != model.ContentStatusPublished && !c.DeletedAt.Valid {
return nil
}
ans := &AcceptedAnswer{
ID: c.ID,
Floor: floor,
CreatedAt: c.CreatedAt,
}
ans.User.ID = c.User.ID
ans.User.Username = c.User.Username
ans.User.Nickname = c.User.Nickname
ans.User.Avatar = c.User.Avatar
if c.DeletedAt.Valid {
ans.Deleted = true
ans.Content = ""
return ans
}
ans.Content = c.Content
return ans
}
func (s *PostService) lotteryEligibleIDs(postID, authorID uint) ([]uint, error) {
var ids []uint
err := s.db.Model(&model.Comment{}).
@@ -554,6 +611,23 @@ func (s *PostService) lotteryEligibleIDs(postID, authorID uint) ([]uint, error)
return ids, err
}
// lotteryWinningFloor 中奖用户的中奖楼层:其在本帖最早一条已发布主评论(每人仅一楼)。
func (s *PostService) lotteryWinningFloor(postID, userID uint) (commentID uint, floor int) {
if postID == 0 || userID == 0 {
return 0, 0
}
var c model.Comment
err := s.db.Select("id").
Where("post_id = ? AND user_id = ? AND parent_id IS NULL AND status = ? AND deleted_at IS NULL",
postID, userID, model.ContentStatusPublished).
Order("created_at ASC, id ASC").
First(&c).Error
if err != nil {
return 0, 0
}
return c.ID, FloorNumber(s.db, postID, c.ID)
}
func (s *PostService) buildLotteryState(post *model.Post, viewerID uint, canManage bool) *LotteryState {
m, err := parseLotteryMeta(post.TypeMeta)
if err != nil {
@@ -577,8 +651,14 @@ func (s *PostService) buildLotteryState(post *model.Post, viewerID uint, canMana
}
for _, id := range m.WinnerIDs {
if u, ok := umap[id]; ok {
cid, floor := s.lotteryWinningFloor(post.ID, id)
winners = append(winners, LotteryEntrant{
ID: u.ID, Nickname: u.Nickname, Username: u.Username, Avatar: u.Avatar,
ID: u.ID,
Nickname: u.Nickname,
Username: u.Username,
Avatar: u.Avatar,
CommentID: cid,
Floor: floor,
})
}
}