feat: SSR 抽奖帖发帖与开奖
compose 支持 lottery;详情抽奖卡可开奖并展示中奖名单。同步勾选已落地的 health/robots/sitemap/uploads/限流清单项。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -27,6 +27,7 @@ type composeData struct {
|
||||
PollEndsAt string
|
||||
PollOptions string
|
||||
BountyPoints int
|
||||
LotteryWinners int
|
||||
Boards []BoardView
|
||||
TitleMax int
|
||||
TagsMax int
|
||||
@@ -46,6 +47,8 @@ func (d Deps) ComposeGet(c *gin.Context) {
|
||||
BoardID: uint(boardID),
|
||||
PostType: models.PostTypeNormal,
|
||||
PollMaxChoices: 1,
|
||||
BountyPoints: 1,
|
||||
LotteryWinners: 1,
|
||||
}, false, 0)
|
||||
}
|
||||
|
||||
@@ -73,9 +76,10 @@ func (d Deps) ComposePost(c *gin.Context) {
|
||||
d.renderCompose(ctx, err.Error(), form, false, 0)
|
||||
return
|
||||
}
|
||||
if post.PostType == models.PostTypePoll || post.PostType == models.PostTypeBounty {
|
||||
if post.PostType == models.PostTypePoll || post.PostType == models.PostTypeBounty || post.PostType == models.PostTypeLottery {
|
||||
var extras services.PostCreateExtras
|
||||
if post.PostType == models.PostTypePoll {
|
||||
switch post.PostType {
|
||||
case models.PostTypePoll:
|
||||
pollJSON, err := services.BuildPollOptionsJSON(form.PollMulti, form.PollMaxChoices, form.PollEndsAt, form.PollOptions)
|
||||
if err != nil {
|
||||
_ = d.Post.Delete(ctx.UserID(), post.ID, true)
|
||||
@@ -83,8 +87,10 @@ func (d Deps) ComposePost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
extras = services.ParsePostExtrasFromForm(pollJSON, "", "")
|
||||
} else {
|
||||
case models.PostTypeBounty:
|
||||
extras = services.ParsePostExtrasFromForm("", strconv.Itoa(form.BountyPoints), "")
|
||||
case models.PostTypeLottery:
|
||||
extras = services.ParsePostExtrasFromForm("", "", strconv.Itoa(form.LotteryWinners))
|
||||
}
|
||||
if err := services.FinalizeSpecialPostCreate(post, ctx.UserID(), extras); err != nil {
|
||||
_ = d.Post.Delete(ctx.UserID(), post.ID, true)
|
||||
@@ -192,11 +198,12 @@ type composeForm struct {
|
||||
PollEndsAt string
|
||||
PollOptions string
|
||||
BountyPoints int
|
||||
LotteryWinners int
|
||||
}
|
||||
|
||||
func normalizeComposePostType(t string) string {
|
||||
switch t {
|
||||
case models.PostTypePoll, models.PostTypeQuestion, models.PostTypeBounty:
|
||||
case models.PostTypePoll, models.PostTypeQuestion, models.PostTypeBounty, models.PostTypeLottery:
|
||||
return t
|
||||
default:
|
||||
return models.PostTypeNormal
|
||||
@@ -210,6 +217,10 @@ func composeFormFrom(c *gin.Context) composeForm {
|
||||
maxChoices = 1
|
||||
}
|
||||
bountyPoints, _ := strconv.Atoi(c.PostForm("bounty_points"))
|
||||
lotteryWinners, _ := strconv.Atoi(c.PostForm("lottery_winner_count"))
|
||||
if lotteryWinners < 1 {
|
||||
lotteryWinners = 1
|
||||
}
|
||||
return composeForm{
|
||||
BoardID: uint(bid),
|
||||
Title: strings.TrimSpace(c.PostForm("title")),
|
||||
@@ -221,6 +232,7 @@ func composeFormFrom(c *gin.Context) composeForm {
|
||||
PollEndsAt: strings.TrimSpace(c.PostForm("poll_ends_at")),
|
||||
PollOptions: c.PostForm("poll_options"),
|
||||
BountyPoints: bountyPoints,
|
||||
LotteryWinners: lotteryWinners,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +252,9 @@ func (d Deps) renderCompose(ctx *webctx.Context, errMsg string, form composeForm
|
||||
if form.BountyPoints < 1 {
|
||||
form.BountyPoints = 1
|
||||
}
|
||||
if form.LotteryWinners < 1 {
|
||||
form.LotteryWinners = 1
|
||||
}
|
||||
chrome := d.chrome(ctx, title+" · "+d.Settings.SiteBranding().Name, "", "")
|
||||
chrome.Error = errMsg
|
||||
chrome.ActiveBoard = form.BoardID
|
||||
@@ -258,6 +273,7 @@ func (d Deps) renderCompose(ctx *webctx.Context, errMsg string, form composeForm
|
||||
PollEndsAt: form.PollEndsAt,
|
||||
PollOptions: form.PollOptions,
|
||||
BountyPoints: form.BountyPoints,
|
||||
LotteryWinners: form.LotteryWinners,
|
||||
Boards: chrome.Boards,
|
||||
TitleMax: d.Settings.PostTitleMax(),
|
||||
TagsMax: d.Settings.PostTagsMax(),
|
||||
|
||||
@@ -36,6 +36,7 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
g.POST("/post/:id/question/resolve", authMW.RequireAuth(), deps.PostQuestionResolvePost)
|
||||
g.POST("/post/:id/bounty/award", authMW.RequireAuth(), deps.PostBountyAwardPost)
|
||||
g.POST("/post/:id/bounty/refund", authMW.RequireAuth(), deps.PostBountyRefundPost)
|
||||
g.POST("/post/:id/lottery/draw", authMW.RequireAuth(), deps.PostLotteryDrawPost)
|
||||
g.POST("/post/:id/unlock", authMW.RequireAuth(), deps.PostUnlock)
|
||||
g.POST("/post/:id/report", authMW.RequireAuth(), deps.PostReportPost)
|
||||
g.POST("/post/:id/comments/:cid/report", authMW.RequireAuth(), deps.CommentReportPost)
|
||||
|
||||
@@ -50,6 +50,7 @@ type PostPageData struct {
|
||||
QuestionResolved bool
|
||||
CanToggleQuestion bool
|
||||
Bounty *postBountyView
|
||||
Lottery *postLotteryView
|
||||
Poll *postPollView
|
||||
}
|
||||
|
||||
@@ -62,6 +63,21 @@ type postBountyView struct {
|
||||
AwardedCommentID uint
|
||||
}
|
||||
|
||||
type postLotteryWinnerView struct {
|
||||
UserID uint
|
||||
Name string
|
||||
FloorHint string
|
||||
}
|
||||
|
||||
type postLotteryView struct {
|
||||
WinnerCount int
|
||||
Status string
|
||||
StatusLabel string
|
||||
ParticipantCount int
|
||||
CanDraw bool
|
||||
Winners []postLotteryWinnerView
|
||||
}
|
||||
|
||||
type postPollOptionView struct {
|
||||
ID uint
|
||||
Text string
|
||||
@@ -219,6 +235,29 @@ func (d Deps) PostView(c *gin.Context) {
|
||||
bountyView = bv
|
||||
}
|
||||
|
||||
var lotteryView *postLotteryView
|
||||
if post.PostType == models.PostTypeLottery {
|
||||
if lv, err := services.GetPostLotteryView(post); err == nil && lv != nil {
|
||||
isOperator := ctx.IsAdmin() || post.UserID == ctx.UserID()
|
||||
winners := make([]postLotteryWinnerView, 0, len(lv.Winners))
|
||||
for _, w := range lv.Winners {
|
||||
name := strings.TrimSpace(w.Nickname)
|
||||
if name == "" {
|
||||
name = w.Username
|
||||
}
|
||||
winners = append(winners, postLotteryWinnerView{UserID: w.UserID, Name: name})
|
||||
}
|
||||
lotteryView = &postLotteryView{
|
||||
WinnerCount: lv.WinnerCount,
|
||||
Status: lv.Status,
|
||||
StatusLabel: lotteryStatusLabel(lv.Status),
|
||||
ParticipantCount: lv.ParticipantCount,
|
||||
CanDraw: isOperator && lv.Status != models.PostLotteryStatusDrawn,
|
||||
Winners: winners,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, "post", PostPageData{
|
||||
PageChrome: chrome, PostID: post.ID,
|
||||
PostPath: url.QueryEscape(fmt.Sprintf("/post/%d", post.ID)),
|
||||
@@ -242,6 +281,7 @@ func (d Deps) PostView(c *gin.Context) {
|
||||
QuestionResolved: post.QuestionResolved,
|
||||
CanToggleQuestion: post.PostType == models.PostTypeQuestion && (ctx.IsAdmin() || post.UserID == ctx.UserID()),
|
||||
Bounty: bountyView,
|
||||
Lottery: lotteryView,
|
||||
Poll: pollView,
|
||||
})
|
||||
}
|
||||
@@ -284,6 +324,17 @@ func contentStatusLabel(status string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func lotteryStatusLabel(status string) string {
|
||||
switch status {
|
||||
case models.PostLotteryStatusDrawn:
|
||||
return "已开奖"
|
||||
case models.PostLotteryStatusOpen:
|
||||
return "待开奖"
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
func bountyStatusLabel(status string) string {
|
||||
switch status {
|
||||
case models.BountyStatusOpen:
|
||||
@@ -596,6 +647,28 @@ func (d Deps) PostBountyRefundPost(c *gin.Context) {
|
||||
ctx.Redirect(fmt.Sprintf("/post/%d", id))
|
||||
}
|
||||
|
||||
// PostLotteryDrawPost 抽奖帖开奖
|
||||
func (d Deps) PostLotteryDrawPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
id, err := parsePostID(c, d)
|
||||
if err != nil || id == 0 {
|
||||
d.render404(ctx)
|
||||
return
|
||||
}
|
||||
if !ctx.CheckCSRF() {
|
||||
ctx.SetFlash("无效请求,请重试")
|
||||
ctx.Redirect(fmt.Sprintf("/post/%d", id))
|
||||
return
|
||||
}
|
||||
if _, err := services.DrawPostLottery(id, ctx.UserID(), ctx.IsAdmin()); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/post/%d", id))
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("开奖完成")
|
||||
ctx.Redirect(fmt.Sprintf("/post/%d", id))
|
||||
}
|
||||
|
||||
type commentEditData struct {
|
||||
PageChrome
|
||||
PostID uint
|
||||
|
||||
Reference in New Issue
Block a user