安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性
This commit is contained in:
+16
-20
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user