feat(posts): 类型帖截止时间、抽奖奖品表与悬赏发帖体验

统一投票/悬赏/抽奖截止与状态展示,抽奖支持多奖品行;发帖页悬赏积分对齐表格输入,并在余额不足时于底部就地提示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 10:21:05 +08:00
parent 6b772380c9
commit b2d3d6dd26
23 changed files with 2711 additions and 1007 deletions

View File

@@ -22,6 +22,7 @@ var (
type PostService struct {
db *gorm.DB
setting *SettingService
devMode bool
}
func NewPostService(db *gorm.DB) *PostService {
@@ -34,6 +35,12 @@ func (s *PostService) WithSetting(setting *SettingService) *PostService {
return s
}
// WithDevMode 开发态:抽奖同 IP 去重对回环地址放宽
func (s *PostService) WithDevMode(dev bool) *PostService {
s.devMode = dev
return s
}
// PostListQuery 帖子列表查询参数
type PostListQuery struct {
BoardID uint
@@ -445,6 +452,7 @@ func (s *PostService) GetByIDForViewer(id, viewerID uint, loadActor func() *Acto
// 惰性结算:悬赏过期退回 / 抽奖到期开奖
_ = s.settleExpiredBountyIfNeeded(&post)
_ = s.settleDueLotteryIfNeeded(&post)
_ = s.settleDuePollIfNeeded(&post)
if post.TypeMeta != "" {
var fresh model.Post
if err := s.db.Select("type_meta").First(&fresh, post.ID).Error; err == nil {
@@ -729,9 +737,8 @@ func (s *PostService) Create(in CreatePostInput) (*PostDetail, error) {
bm.Expired = false
bm.AcceptedCommentID = 0
if status == model.ContentStatusPublished {
days := normalizeExpireDays(bm.ExpireDays, 7, []int{3, 7, 14, 30})
bm.EndsAt = resolveEndsAtOnPublish(bm.EndsAt, bm.ExpireDays, 7, []int{3, 7, 14, 30}, now)
bm.ExpireDays = 0
bm.EndsAt = endsAtFromDays(days, now)
}
typeMeta, _ = encodeMeta(bm)
}
@@ -741,14 +748,26 @@ func (s *PostService) Create(in CreatePostInput) (*PostDetail, error) {
if lm.WinnerIDs == nil {
lm.WinnerIDs = []uint{}
}
if lm.Prizes == nil {
lm.Prizes = []LotteryPrize{}
}
lm.Slots = lotterySlotsFromPrizes(lm.Prizes)
if status == model.ContentStatusPublished {
days := normalizeExpireDays(lm.ExpireDays, 7, []int{1, 3, 7, 14})
lm.EndsAt = resolveEndsAtOnPublish(lm.EndsAt, lm.ExpireDays, 7, []int{1, 3, 7, 14}, now)
lm.ExpireDays = 0
lm.EndsAt = endsAtFromDays(days, now)
}
typeMeta, _ = encodeMeta(lm)
}
if postType == model.PostTypePoll {
pm, _ := parsePollMeta(typeMeta)
if status == model.ContentStatusPublished {
pm.EndsAt = resolveEndsAtOnPublish(pm.EndsAt, pm.ExpireDays, 7, []int{1, 3, 7, 14}, now)
pm.ExpireDays = 0
}
typeMeta, _ = encodeMeta(pm)
}
post := &model.Post{
BoardID: in.BoardID, UserID: in.UserID,
Title: title, Content: content, Tags: in.Tags,
@@ -892,9 +911,15 @@ func (s *PostService) Update(actor *Actor, postID, userID uint, in UpdatePostInp
neu.Multi = old.Multi
neu.Anonymous = old.Anonymous
neu.Closed = old.Closed
neu.EndsAt = old.EndsAt
neu.ClosedAt = old.ClosedAt
neu.ExpireDays = 0
meta, _ = encodeMeta(neu)
} else {
neu.Closed = old.Closed
neu.EndsAt = old.EndsAt
neu.ClosedAt = old.ClosedAt
neu.ExpireDays = 0
meta, _ = encodeMeta(neu)
}
}
@@ -907,8 +932,16 @@ func (s *PostService) Update(actor *Actor, postID, userID uint, in UpdatePostInp
neu.WinnerIDs = old.WinnerIDs
neu.Closed = old.Closed
neu.EndsAt = old.EndsAt
if old.Drawn {
// 已有评论或已开奖:冻结奖品(名额由奖品合计推导)
locked := old.Drawn || post.CommentCount > 0
if locked {
if !lotteryPrizesEqual(neu.Prizes, old.Prizes) || neu.Slots != old.Slots {
return nil, ErrLotteryPrizesLocked
}
neu.Prizes = old.Prizes
neu.Slots = old.Slots
} else {
neu.Slots = lotterySlotsFromPrizes(neu.Prizes)
}
meta, _ = encodeMeta(neu)
}
@@ -920,6 +953,7 @@ func (s *PostService) Update(actor *Actor, postID, userID uint, in UpdatePostInp
// 解题态走专用 API,编辑帖子不覆盖
neu.Solved = old.Solved
neu.AcceptedCommentID = old.AcceptedCommentID
neu.SolvedAt = old.SolvedAt
meta, _ = encodeMeta(neu)
}
}