安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性
This commit is contained in:
+29
-7
@@ -116,7 +116,10 @@ func computeResults(reqCtx stdctx.Context, v *db.Vote) (*Results, error) {
|
||||
|
||||
switch v.Type {
|
||||
case db.VoteTypeSingle:
|
||||
options, _ := db.VoteOptions.ListByVoteID(reqCtx, v.ID)
|
||||
options, err := db.VoteOptions.ListByVoteID(reqCtx, v.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list vote options")
|
||||
}
|
||||
// 一次分组查询替代逐选项 COUNT(避免 N+1)
|
||||
counts, err := db.VoteSubmissions.CountGroupByOptionID(reqCtx, v.ID)
|
||||
if err != nil {
|
||||
@@ -132,9 +135,15 @@ func computeResults(reqCtx stdctx.Context, v *db.Vote) (*Results, error) {
|
||||
}
|
||||
|
||||
case db.VoteTypeMultiple:
|
||||
options, _ := db.VoteOptions.ListByVoteID(reqCtx, v.ID)
|
||||
options, err := db.VoteOptions.ListByVoteID(reqCtx, v.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list vote options")
|
||||
}
|
||||
submissions, err := db.VoteSubmissions.ListByVoteID(reqCtx, v.ID, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list vote submissions")
|
||||
}
|
||||
counts := make(map[uint]int64, len(options))
|
||||
submissions, _ := db.VoteSubmissions.ListByVoteID(reqCtx, v.ID, nil)
|
||||
var totalSelections int64
|
||||
for _, s := range submissions {
|
||||
var data struct {
|
||||
@@ -163,9 +172,12 @@ func computeResults(reqCtx stdctx.Context, v *db.Vote) (*Results, error) {
|
||||
}
|
||||
|
||||
case db.VoteTypeRating:
|
||||
submissions, err := db.VoteSubmissions.ListByVoteID(reqCtx, v.ID, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list vote submissions")
|
||||
}
|
||||
scoreCounts := make(map[int]int64)
|
||||
var sum int64
|
||||
submissions, _ := db.VoteSubmissions.ListByVoteID(reqCtx, v.ID, nil)
|
||||
for _, s := range submissions {
|
||||
var data struct {
|
||||
Rating int `json:"rating"`
|
||||
@@ -184,10 +196,16 @@ func computeResults(reqCtx stdctx.Context, v *db.Vote) (*Results, error) {
|
||||
}
|
||||
|
||||
case db.VoteTypeRank:
|
||||
options, _ := db.VoteOptions.ListByVoteID(reqCtx, v.ID)
|
||||
options, err := db.VoteOptions.ListByVoteID(reqCtx, v.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list vote options")
|
||||
}
|
||||
submissions, err := db.VoteSubmissions.ListByVoteID(reqCtx, v.ID, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list vote submissions")
|
||||
}
|
||||
rankSums := make(map[uint]float64, len(options))
|
||||
rankCounts := make(map[uint]int64, len(options))
|
||||
submissions, _ := db.VoteSubmissions.ListByVoteID(reqCtx, v.ID, nil)
|
||||
for _, s := range submissions {
|
||||
var data struct {
|
||||
Ranking map[string]float64 `json:"ranking"`
|
||||
@@ -215,7 +233,11 @@ func computeResults(reqCtx stdctx.Context, v *db.Vote) (*Results, error) {
|
||||
}
|
||||
|
||||
case db.VoteTypeForm:
|
||||
res.FormFields, _ = db.VoteFormFields.ListByVoteID(reqCtx, v.ID)
|
||||
fields, err := db.VoteFormFields.ListByVoteID(reqCtx, v.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list vote form fields")
|
||||
}
|
||||
res.FormFields = fields
|
||||
res.FormCount = total
|
||||
res.FormFieldStats = aggregateFormFieldStats(reqCtx, res.FormFields)
|
||||
}
|
||||
|
||||
+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{}
|
||||
|
||||
@@ -21,6 +21,13 @@ type mockVoteSubmissions struct {
|
||||
func (m *mockVoteSubmissions) ListByVoteID(ctx stdctx.Context, voteID uint, cursor *dbutil.Cursor) ([]*db.VoteSubmission, error) {
|
||||
return m.submissions, nil
|
||||
}
|
||||
func (m *mockVoteSubmissions) ListByVoteIDs(ctx stdctx.Context, voteIDs []uint) (map[uint][]*db.VoteSubmission, error) {
|
||||
out := make(map[uint][]*db.VoteSubmission)
|
||||
for _, s := range m.submissions {
|
||||
out[s.VoteID] = append(out[s.VoteID], s)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (m *mockVoteSubmissions) Create(ctx stdctx.Context, opts db.CreateVoteSubmissionOptions) (*db.VoteSubmission, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user