安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性

This commit is contained in:
tamakyi
2026-11-05 12:17:05 +08:00
parent 7636456d4e
commit 25aee35030
18 changed files with 1002 additions and 194 deletions
+22 -41
View File
@@ -626,33 +626,15 @@ func VoteCreate(ctx context.Context) {
}
form.opts.CensorMetadata = form.censorMetadata
vote, err := db.Votes.Create(ctx.Request().Context(), form.opts)
// 事务化创建:投票 + 选项 + 表单字段。任一步失败整体回滚,
// 避免留下「有投票无选项」或「有选项无字段」的半成品。
vote, err := db.Votes.CreateWithOptions(ctx.Request().Context(), form.opts, form.options, form.formFields)
if err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create vote")
renderVoteFormWithInput(ctx, false, 0, "", true)
return
}
if len(form.options) > 0 {
if _, err := db.VoteOptions.BulkCreate(ctx.Request().Context(), vote.ID, form.options); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create vote options")
_ = db.Votes.DeleteByID(ctx.Request().Context(), vote.ID) // 回滚本次已建的空草稿,避免孤儿记录
renderVoteFormWithInput(ctx, false, 0, "", true)
return
}
}
if len(form.formFields) > 0 {
for i := range form.formFields {
form.formFields[i].VoteID = vote.ID
}
if _, err := db.VoteFormFields.BulkCreate(ctx.Request().Context(), vote.ID, form.formFields); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create vote form fields")
_ = db.Votes.DeleteByID(ctx.Request().Context(), vote.ID) // 回滚本次已建的空草稿,避免孤儿记录
renderVoteFormWithInput(ctx, false, 0, "", true)
return
}
}
editURL := "/user/votes/" + fmt.Sprint(vote.ID) + "/edit"
if action == "publish" {
@@ -704,10 +686,22 @@ func VoteEdit(ctx context.Context) {
// 选项 / 字段回填传结构化数据,模板经 toJSON(template.JS)注入 <script>,
// 避免 template.HTML 在 script 上下文被转义导致初始化失效。
options, _ := db.VoteOptions.ListByVoteID(ctx.Request().Context(), vote.ID)
options, err := db.VoteOptions.ListByVoteID(ctx.Request().Context(), vote.ID)
if err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to list vote options")
ctx.SetInternalErrorFlash()
ctx.Redirect("/user/questions#votes")
return
}
ctx.Data["InitialOptions"] = optionsContent(options)
fields, _ := db.VoteFormFields.ListByVoteID(ctx.Request().Context(), vote.ID)
fields, err := db.VoteFormFields.ListByVoteID(ctx.Request().Context(), vote.ID)
if err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to list vote form fields")
ctx.SetInternalErrorFlash()
ctx.Redirect("/user/questions#votes")
return
}
ctx.Data["InitialFields"] = fieldsContent(fields)
// 时间回填:Go 模板直接输出 time.Time 不符合 datetime-local 格式,预先格式化
@@ -829,7 +823,9 @@ func VoteUpdate(ctx context.Context) {
return
}
if err := db.Votes.Update(ctx.Request().Context(), vote.ID, db.UpdateVoteOptions{
// 事务化更新:投票元信息 + 替换选项 + 替换表单字段。任一步失败整体回滚,
// 避免「元信息已更新、字段只更新一部分」的不一致草稿。
if err := db.Votes.UpdateWithOptions(ctx.Request().Context(), vote.ID, db.UpdateVoteOptions{
Title: form.opts.Title,
Description: form.opts.Description,
Type: &form.opts.Type,
@@ -844,27 +840,12 @@ func VoteUpdate(ctx context.Context) {
RequireCaptcha: form.opts.RequireCaptcha,
CensorMetadata: form.censorMetadata,
QuotaLimit: form.opts.QuotaLimit,
}); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update vote")
}, form.options, form.formFields); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update vote with options")
renderVoteFormWithInput(ctx, true, vote.ID, "", true)
return
}
// 事务重建选项与表单字段(草稿阶段无历史数据,删除重建安全)
if err := db.VoteOptions.ReplaceByVoteID(ctx.Request().Context(), vote.ID, form.options); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to replace vote options")
renderVoteFormWithInput(ctx, true, vote.ID, i18n.T(ctx.Lang(), "vote.err.optionUpdateFailed"), false)
return
}
for i := range form.formFields {
form.formFields[i].VoteID = vote.ID
}
if err := db.VoteFormFields.ReplaceByVoteID(ctx.Request().Context(), vote.ID, form.formFields); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to replace vote form fields")
renderVoteFormWithInput(ctx, true, vote.ID, i18n.T(ctx.Lang(), "vote.err.fieldUpdateFailed"), false)
return
}
// 选项/字段已重建,结果聚合可能变化:主动失效该投票的结果缓存。
votepkg.InvalidateResultsCache(vote.ID)