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

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
+16 -20
View File
@@ -724,6 +724,8 @@ func Submit(ctx context.Context, recaptcha recaptcha.RecaptchaV3) {
if _, err := db.VoteSubmissions.Create(ctx.Request().Context(), *submission); err != nil {
if errors.Is(err, db.ErrVoteAlreadyVoted) {
ctx.SetErrorFlash(i18n.T(lang, "vote.page.alreadyVoted"))
} else if errors.Is(err, db.ErrVoteNotAccepting) || errors.Is(err, db.ErrFeatureQuotaReached) {
ctx.SetErrorFlash(i18n.T(lang, "vote.flash.ended"))
} else {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create vote submission")
ctx.SetInternalErrorFlash()
@@ -738,19 +740,6 @@ func Submit(ctx context.Context, recaptcha recaptcha.RecaptchaV3) {
// 用 InvalidateResultsCache 会让每一票都变成下一个读请求的同步全量重算(O(N²))。
ExpireResultsCache(vote.ID)
// 单份投票的份数上限:达到即自动结束该投票。
// (箱主级的累计计数已由 VoteSubmissions.Create 在同一事务里 +1,这里不再重复)
if vote.QuotaLimit > 0 {
count, err := db.VoteSubmissions.CountByVoteID(ctx.Request().Context(), vote.ID)
if err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Warn("Failed to count vote submissions for quota")
} else if count >= int64(vote.QuotaLimit) {
if err := db.Votes.UpdateStatus(ctx.Request().Context(), vote.ID, db.VoteStatusEnded); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Warn("Failed to auto end vote on quota reached")
}
}
}
// 表单类型投票:异步通知箱主查看新回复(开启邮件通知时)。owner 在上方已取过,直接复用。
if vote.Type == db.VoteTypeForm && owner.Notify == db.NotifyTypeEmail {
email := owner.Email
@@ -820,9 +809,14 @@ func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.Creat
// 选项合法性集合
validOptions := map[uint]bool{}
var voteOptions []*db.VoteOption
if v.Type == db.VoteTypeSingle || v.Type == db.VoteTypeMultiple || v.Type == db.VoteTypeRank {
options, _ := db.VoteOptions.ListByVoteID(ctx.Request().Context(), v.ID)
for _, o := range options {
var err error
voteOptions, err = db.VoteOptions.ListByVoteID(ctx.Request().Context(), v.ID)
if err != nil {
return nil, errors.Wrap(err, "load vote options")
}
for _, o := range voteOptions {
validOptions[o.ID] = true
}
}
@@ -853,13 +847,12 @@ func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.Creat
formData["rating"] = score
case db.VoteTypeRank:
options, _ := db.VoteOptions.ListByVoteID(ctx.Request().Context(), v.ID)
ranking := map[string]float64{}
used := map[float64]bool{}
for _, o := range options {
for _, o := range voteOptions {
r, err := strconv.ParseFloat(get(fmt.Sprintf("rank_%d", o.ID)), 64)
if err != nil || r < 1 || r > float64(len(options)) {
return nil, fmt.Errorf(i18n.T(lang, "vote.err.rankRequired"), len(options))
if err != nil || r < 1 || r > float64(len(voteOptions)) {
return nil, fmt.Errorf(i18n.T(lang, "vote.err.rankRequired"), len(voteOptions))
}
if used[r] {
return nil, errors.New(i18n.T(lang, "vote.err.rankDuplicate"))
@@ -870,7 +863,10 @@ func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.Creat
formData["ranking"] = ranking
case db.VoteTypeForm:
fields, _ := db.VoteFormFields.ListByVoteID(ctx.Request().Context(), v.ID)
fields, err := db.VoteFormFields.ListByVoteID(ctx.Request().Context(), v.ID)
if err != nil {
return nil, errors.Wrap(err, "load vote form fields")
}
for _, f := range fields {
// 字段选项集合
validFieldOptions := map[string]bool{}