安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性
This commit is contained in:
@@ -61,6 +61,9 @@ const (
|
||||
|
||||
type VotesStore interface {
|
||||
Create(ctx context.Context, opts CreateVoteOptions) (*Vote, error)
|
||||
// CreateWithOptions 在单个事务里创建投票 + 选项 + 表单字段。
|
||||
// 任一步失败整体回滚,避免留下「有投票无选项」或「有选项无字段」的半成品。
|
||||
CreateWithOptions(ctx context.Context, opts CreateVoteOptions, optionContents []string, formFields []CreateVoteFormFieldOptions) (*Vote, error)
|
||||
GetByID(ctx context.Context, id uint) (*Vote, error)
|
||||
GetByToken(ctx context.Context, token string) (*Vote, error)
|
||||
ListByUserID(ctx context.Context, userID uint, opts ListVotesOptions) ([]*Vote, error)
|
||||
@@ -68,6 +71,9 @@ type VotesStore interface {
|
||||
ListPublishedPublic(ctx context.Context, limit int) ([]*Vote, error)
|
||||
GetHot(ctx context.Context, limit int) ([]*Vote, error)
|
||||
Update(ctx context.Context, id uint, opts UpdateVoteOptions) error
|
||||
// UpdateWithOptions 在单个事务里更新投票元信息 + 替换选项 + 替换表单字段。
|
||||
// 仅在草稿状态调用(发布后选项/字段锁定,避免破坏历史数据归因)。
|
||||
UpdateWithOptions(ctx context.Context, id uint, opts UpdateVoteOptions, optionContents []string, formFields []CreateVoteFormFieldOptions) error
|
||||
UpdateStatus(ctx context.Context, id uint, status string) error
|
||||
DeleteByID(ctx context.Context, id uint) error
|
||||
// DeleteWithCascade 在一个事务里真删投票及其选项、表单字段、提交记录。
|
||||
@@ -160,6 +166,69 @@ func (db *votes) Create(ctx context.Context, opts CreateVoteOptions) (*Vote, err
|
||||
return vote, nil
|
||||
}
|
||||
|
||||
// CreateWithOptions 在单个事务里创建投票 + 选项 + 表单字段。
|
||||
// 任一步失败整体回滚,避免留下「有投票无选项」或「有选项无字段」的半成品。
|
||||
func (db *votes) CreateWithOptions(ctx context.Context, opts CreateVoteOptions, optionContents []string, formFields []CreateVoteFormFieldOptions) (*Vote, error) {
|
||||
var vote *Vote
|
||||
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
v := &Vote{
|
||||
UserID: opts.UserID,
|
||||
Title: opts.Title,
|
||||
Description: opts.Description,
|
||||
Type: opts.Type,
|
||||
Visibility: opts.Visibility,
|
||||
VoterScope: opts.VoterScope,
|
||||
ShowResult: opts.ShowResult,
|
||||
AllowCustom: opts.AllowCustom,
|
||||
MaxChoices: opts.MaxChoices,
|
||||
StartAt: opts.StartAt,
|
||||
EndAt: opts.EndAt,
|
||||
MaxVotesPerFingerprint: opts.MaxVotesPerFingerprint,
|
||||
RequireCaptcha: opts.RequireCaptcha,
|
||||
CensorMetadata: opts.CensorMetadata,
|
||||
QuotaLimit: opts.QuotaLimit,
|
||||
}
|
||||
if err := tx.Create(v).Error; err != nil {
|
||||
return errors.Wrap(err, "create vote")
|
||||
}
|
||||
vote = v
|
||||
|
||||
if len(optionContents) > 0 {
|
||||
options := make([]*VoteOption, 0, len(optionContents))
|
||||
for i, content := range optionContents {
|
||||
options = append(options, &VoteOption{VoteID: v.ID, Content: content, SortOrder: i})
|
||||
}
|
||||
if err := tx.Create(&options).Error; err != nil {
|
||||
return errors.Wrap(err, "create vote options")
|
||||
}
|
||||
}
|
||||
|
||||
if len(formFields) > 0 {
|
||||
fields := make([]*VoteFormField, 0, len(formFields))
|
||||
for _, f := range formFields {
|
||||
fields = append(fields, &VoteFormField{
|
||||
VoteID: v.ID,
|
||||
FieldType: f.FieldType,
|
||||
Label: f.Label,
|
||||
Required: f.Required,
|
||||
MaxLength: f.MaxLength,
|
||||
Options: f.Options,
|
||||
SortOrder: f.SortOrder,
|
||||
AllowCustom: f.AllowCustom,
|
||||
})
|
||||
}
|
||||
if err := tx.Create(&fields).Error; err != nil {
|
||||
return errors.Wrap(err, "create vote form fields")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vote, nil
|
||||
}
|
||||
|
||||
var ErrVoteNotExist = errors.New("投票不存在")
|
||||
|
||||
func (db *votes) GetByID(ctx context.Context, id uint) (*Vote, error) {
|
||||
@@ -335,6 +404,79 @@ func (db *votes) Update(ctx context.Context, id uint, opts UpdateVoteOptions) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateWithOptions 在单个事务里更新投票元信息 + 替换选项 + 替换表单字段。
|
||||
// 仅在草稿状态调用(发布后选项/字段锁定,避免破坏历史数据归因)。
|
||||
// 任一步失败整体回滚,避免「元信息已更新、字段只更新一部分」的不一致草稿。
|
||||
func (db *votes) UpdateWithOptions(ctx context.Context, id uint, opts UpdateVoteOptions, optionContents []string, formFields []CreateVoteFormFieldOptions) error {
|
||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
updates := map[string]interface{}{
|
||||
"title": opts.Title,
|
||||
"description": opts.Description,
|
||||
"visibility": opts.Visibility,
|
||||
"voter_scope": opts.VoterScope,
|
||||
"show_result": opts.ShowResult,
|
||||
"allow_custom": opts.AllowCustom,
|
||||
"start_at": opts.StartAt,
|
||||
"end_at": opts.EndAt,
|
||||
"max_votes_per_fingerprint": opts.MaxVotesPerFingerprint,
|
||||
"require_captcha": opts.RequireCaptcha,
|
||||
"censor_metadata": opts.CensorMetadata,
|
||||
"quota_limit": opts.QuotaLimit,
|
||||
}
|
||||
if opts.Type != nil {
|
||||
updates["type"] = *opts.Type
|
||||
}
|
||||
if opts.MaxChoices != nil {
|
||||
updates["max_choices"] = *opts.MaxChoices
|
||||
}
|
||||
res := tx.Model(&Vote{}).Where("id = ?", id).Updates(updates)
|
||||
if res.Error != nil {
|
||||
return errors.Wrap(res.Error, "update vote")
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return ErrVoteNotExist
|
||||
}
|
||||
|
||||
// 替换选项(先删后建)
|
||||
if err := tx.Where("vote_id = ?", id).Delete(&VoteOption{}).Error; err != nil {
|
||||
return errors.Wrap(err, "delete old vote options")
|
||||
}
|
||||
if len(optionContents) > 0 {
|
||||
options := make([]*VoteOption, 0, len(optionContents))
|
||||
for i, content := range optionContents {
|
||||
options = append(options, &VoteOption{VoteID: id, Content: content, SortOrder: i})
|
||||
}
|
||||
if err := tx.Create(&options).Error; err != nil {
|
||||
return errors.Wrap(err, "create vote options")
|
||||
}
|
||||
}
|
||||
|
||||
// 替换表单字段(先删后建)
|
||||
if err := tx.Where("vote_id = ?", id).Delete(&VoteFormField{}).Error; err != nil {
|
||||
return errors.Wrap(err, "delete old vote form fields")
|
||||
}
|
||||
if len(formFields) > 0 {
|
||||
fields := make([]*VoteFormField, 0, len(formFields))
|
||||
for _, f := range formFields {
|
||||
fields = append(fields, &VoteFormField{
|
||||
VoteID: id,
|
||||
FieldType: f.FieldType,
|
||||
Label: f.Label,
|
||||
Required: f.Required,
|
||||
MaxLength: f.MaxLength,
|
||||
Options: f.Options,
|
||||
SortOrder: f.SortOrder,
|
||||
AllowCustom: f.AllowCustom,
|
||||
})
|
||||
}
|
||||
if err := tx.Create(&fields).Error; err != nil {
|
||||
return errors.Wrap(err, "create vote form fields")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateStatus 更新投票状态(draft → published → ended → archived)。
|
||||
func (db *votes) UpdateStatus(ctx context.Context, id uint, status string) error {
|
||||
res := db.WithContext(ctx).Model(&Vote{}).Where("id = ?", id).Update("status", status)
|
||||
|
||||
Reference in New Issue
Block a user