fix(data): 删除改成一个事务且真删、配额与内容同事务
四类问题都出在「多次独立写入 + 失败只打日志」这个模式上。
1) 删除投票的级联没有事务(route/user/vote.go)
选项 / 表单字段 / 提交记录 / 投票本体是 4 次独立写入,前 3 步失败只 Warn。
中途失败会留下「选项没了但票还在」这种界面无法修复的状态 —— 重试删除时
投票已不存在,只能报 not exist,脏数据永远留着。
→ db.Votes.DeleteWithCascade:四张表在同一事务里删完。
2) 删除其实是软删除,个人数据没真删掉
模型都嵌了 dbutil.Model(含 DeletedAt),原来所有 Delete 都是软删:
提交记录里参与者填写的文本、voter_ip、voter_token,以及提问里的 FromIP,
只是「界面上看不见」,库里永久留着。用户点删除的预期不是这样。
软删还会占住 link_token 唯一索引,删过的链接永远不能复用。
→ 删除投票与删除提问统一 Unscoped 真删;提问的追问/图片关联/配额回退
原来也是三个独立写入(失败只 Warn),一并收进事务。
3) 配额计数会永久偏低
「创建内容」与「quota_count+1」是两次写入,后者失败只 Warn:
箱主看到的「已收集 7/10」偏小,达到上限后因为计数偏小实际还能继续收,
等于定量收集被静默绕过;重置计数也校准不回来(重置是把计数清零,更脱节)。
→ 抽出 incrementQuotaTx(tx, userID, feature, delta) 作为唯一实现,
提问 Questions.Create、私聊 CreateWithFirstMessage、投票提交
VoteSubmissions.Create(新增 QuotaUserID)都在自己的事务里完成 +1。
注意口径:投票的定量收集是「累计收到的票数」(提交成功时 +1),
不是「创建了几个投票」—— 所以删除投票不回退这个计数。
4) 改写追问回答每次都发通知邮件
箱主改措辞、补一句、修错别字都会重新保存,每次都发的话提问者会连着收到
好几封一模一样的「您的提问有回复」。主回答那边早有 question.Answer == ""
的判定,追问这里补齐:UpdateAnswerByID 在事务内读旧值返回 wasFirst。
顺带:
- isQuestioner 的 token 比较从 == 改为 subtle.ConstantTimeCompare,
与 Pager 中间件里的校验同口径(== 按字节短路,可用响应时间逐字节试探)。
- CSV 导出的表头(字段名)与选项内容列补 sanitizeCSVCell。原来只有数据列走它,
而选项名同样可能是 =HYPERLINK(...) 形式;判断也改成看 trim 后的首字符。
- 写路径的功能开关改 fail-closed:新增 db.FeatureStateOfStrict,读配置失败时
拒绝而不是回退默认配置 —— 提问/投票的默认值是「开启」,fail-open 等于让一次
数据库抖动绕过箱主设的关闭与定量上限。读展示仍用宽松版。
验证:全量单测(带 DB)与 go build ./... 通过;verify_round3 6/6(私聊
message_count 与实际消息数一致)、verify_p1 4/4、verify_round2 19/19、
verify_page_next 6/6、verify_chat_owner 3/3。
This commit is contained in:
@@ -70,6 +70,8 @@ type VotesStore interface {
|
||||
Update(ctx context.Context, id uint, opts UpdateVoteOptions) error
|
||||
UpdateStatus(ctx context.Context, id uint, status string) error
|
||||
DeleteByID(ctx context.Context, id uint) error
|
||||
// DeleteWithCascade 在一个事务里真删投票及其选项、表单字段、提交记录。
|
||||
DeleteWithCascade(ctx context.Context, id uint) error
|
||||
CountByUserID(ctx context.Context, userID uint) (int64, error)
|
||||
}
|
||||
|
||||
@@ -359,6 +361,50 @@ func (db *votes) DeleteByID(ctx context.Context, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWithCascade 删除投票及其全部关联数据(选项、表单字段、提交记录)。
|
||||
//
|
||||
// 为什么必须是一个事务:原来 handler 里是 4 次独立写入,前 3 次失败只打 Warn,
|
||||
// 于是可能留下「选项没了但票数还在」(结果页统计全 0 而提交记录占着库)、
|
||||
// 「投票没了但提交记录成孤儿」这类无法通过界面修复的状态 —— 箱主只能重试删除,
|
||||
// 而重试时 vote 已不存在,直接报 not exist,脏数据永远留着。
|
||||
//
|
||||
// 为什么是 Unscoped(真删)而不是软删:
|
||||
// 1. 用户点「删除」的预期就是删掉,不是「我还能看到但数据库里还在」;
|
||||
// 2. 提交记录里有参与者填写的文本、voter_ip、voter_token —— 软删等于把这些
|
||||
// 个人数据永久留在库里,而界面上已经看不到了,等于删了但没删干净;
|
||||
// 3. 软删还会占住 link_token 唯一索引,删过的链接永远无法复用。
|
||||
func (db *votes) DeleteWithCascade(ctx context.Context, id uint) error {
|
||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var vote Vote
|
||||
if err := tx.First(&vote, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrVoteNotExist
|
||||
}
|
||||
return errors.Wrap(err, "get vote by ID")
|
||||
}
|
||||
|
||||
// 顺序:先清子表再删主表。没有外键约束(项目不用 FK),
|
||||
// 所以顺序只影响可读性;真正的保证来自事务本身。
|
||||
if err := tx.Unscoped().Where("vote_id = ?", id).Delete(&VoteSubmission{}).Error; err != nil {
|
||||
return errors.Wrap(err, "delete vote submissions")
|
||||
}
|
||||
if err := tx.Unscoped().Where("vote_id = ?", id).Delete(&VoteOption{}).Error; err != nil {
|
||||
return errors.Wrap(err, "delete vote options")
|
||||
}
|
||||
if err := tx.Unscoped().Where("vote_id = ?", id).Delete(&VoteFormField{}).Error; err != nil {
|
||||
return errors.Wrap(err, "delete vote form fields")
|
||||
}
|
||||
if err := tx.Unscoped().Delete(&Vote{}, id).Error; err != nil {
|
||||
return errors.Wrap(err, "delete vote")
|
||||
}
|
||||
|
||||
// 注意:这里**不**回退 quota_count。投票的定量收集口径是「累计收到的票数」
|
||||
// (见 route/vote/vote.go 中提交成功后 +1),不是「创建了几个投票」——
|
||||
// 删掉一个投票并不意味着那些票没发生过。箱主要腾额度靠界面上的「重置计数」。
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// CountByUserID 统计箱主的投票总数(全部状态)。
|
||||
func (db *votes) CountByUserID(ctx context.Context, userID uint) (int64, error) {
|
||||
var count int64
|
||||
|
||||
Reference in New Issue
Block a user