安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/dbutil"
|
||||
)
|
||||
@@ -25,6 +26,8 @@ type VoteSubmissionsStore interface {
|
||||
Create(ctx context.Context, opts CreateVoteSubmissionOptions) (*VoteSubmission, error)
|
||||
GetByID(ctx context.Context, id uint) (*VoteSubmission, error)
|
||||
ListByVoteID(ctx context.Context, voteID uint, cursor *dbutil.Cursor) ([]*VoteSubmission, error)
|
||||
// ListByVoteIDs 批量查询多个投票的全部提交记录(IN 查询 + 内存分组),避免逐投票 N+1。
|
||||
ListByVoteIDs(ctx context.Context, voteIDs []uint) (map[uint][]*VoteSubmission, error)
|
||||
CountByVoteID(ctx context.Context, voteID uint) (int64, error)
|
||||
CountByVoteIDs(ctx context.Context, voteIDs []uint) (map[uint]int64, error)
|
||||
CountByOptionID(ctx context.Context, voteID, optionID uint) (int64, error)
|
||||
@@ -77,6 +80,7 @@ var (
|
||||
// ErrVoteAlreadyVoted 同一投票被同一身份重复提交(数据库唯一索引兜底返回)。
|
||||
ErrVoteAlreadyVoted = errors.New("你已参与过该投票")
|
||||
ErrVoteSubmissionNotExist = errors.New("投票记录不存在")
|
||||
ErrVoteNotAccepting = errors.New("投票未开放或已达到收集上限")
|
||||
)
|
||||
|
||||
// Create 写入一条投票/提交记录。若命中复合唯一索引(同一 user 或同一 token 重复提交),
|
||||
@@ -92,6 +96,26 @@ func (db *voteSubmissions) Create(ctx context.Context, opts CreateVoteSubmission
|
||||
CustomText: opts.CustomText,
|
||||
}
|
||||
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if opts.QuotaUserID != 0 {
|
||||
if err := reserveFeatureQuotaTx(tx, opts.QuotaUserID, FeatureVote); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var vote Vote
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&vote, opts.VoteID).Error; err != nil {
|
||||
return errors.Wrap(err, "lock vote")
|
||||
}
|
||||
now := time.Now()
|
||||
if vote.Status != VoteStatusPublished || (vote.StartAt != nil && now.Before(*vote.StartAt)) || (vote.EndAt != nil && !now.Before(*vote.EndAt)) {
|
||||
return ErrVoteNotAccepting
|
||||
}
|
||||
var count int64
|
||||
if err := tx.Model(&VoteSubmission{}).Where("vote_id = ?", opts.VoteID).Count(&count).Error; err != nil {
|
||||
return errors.Wrap(err, "count vote submissions")
|
||||
}
|
||||
if vote.QuotaLimit > 0 && count >= int64(vote.QuotaLimit) {
|
||||
return ErrVoteNotAccepting
|
||||
}
|
||||
if err := tx.Create(submission).Error; err != nil {
|
||||
if isDuplicateKeyError(err) {
|
||||
return ErrVoteAlreadyVoted
|
||||
@@ -107,6 +131,11 @@ func (db *voteSubmissions) Create(ctx context.Context, opts CreateVoteSubmission
|
||||
return errors.Wrap(err, "increment vote quota")
|
||||
}
|
||||
}
|
||||
if vote.QuotaLimit > 0 && count+1 >= int64(vote.QuotaLimit) {
|
||||
if err := tx.Model(&Vote{}).Where("id = ?", vote.ID).Update("status", VoteStatusEnded).Error; err != nil {
|
||||
return errors.Wrap(err, "end vote on quota")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -148,6 +177,26 @@ func (db *voteSubmissions) ListByVoteID(ctx context.Context, voteID uint, cursor
|
||||
return submissions, nil
|
||||
}
|
||||
|
||||
// ListByVoteIDs 批量查询多个投票的全部提交记录(IN 查询 + 内存分组),避免逐投票 N+1。
|
||||
// 返回 map[voteID][]*VoteSubmission;未出现在结果中的 voteID 视为无提交。
|
||||
func (db *voteSubmissions) ListByVoteIDs(ctx context.Context, voteIDs []uint) (map[uint][]*VoteSubmission, error) {
|
||||
out := make(map[uint][]*VoteSubmission, len(voteIDs))
|
||||
if len(voteIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
var submissions []*VoteSubmission
|
||||
if err := db.WithContext(ctx).
|
||||
Where("vote_id IN ?", voteIDs).
|
||||
Order("created_at ASC").
|
||||
Find(&submissions).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "list vote submissions by vote ids")
|
||||
}
|
||||
for _, s := range submissions {
|
||||
out[s.VoteID] = append(out[s.VoteID], s)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CountByVoteID 统计某投票的总参与人数。
|
||||
func (db *voteSubmissions) CountByVoteID(ctx context.Context, voteID uint) (int64, error) {
|
||||
var count int64
|
||||
|
||||
Reference in New Issue
Block a user