feat(posts): 类型特殊块移至正文下,采纳答案卡与中奖楼层按评论标记
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user