fix: P0-02 修复提现竞态条件

使用 SELECT ... FOR UPDATE SKIP LOCKED 实现原子化提现创建

问题:
- HasPendingOrProcessingWithdraw 和 CreateInTx 分开调用导致竞态
- 两个并发请求可能同时通过检查并创建提现

解决方案:
- 新增 CreateWithdrawTx 方法,先锁定 pending 记录再检查插入
- 使用 FOR UPDATE SKIP LOCKED 防止并发插入

涉及文件:
- internal/repository/settlement.go: 新增 CreateWithdrawTx
- internal/adapter/adapter.go: 实现 CreateWithdrawTx
- internal/domain/settlement.go: 使用 CreateWithdrawTx
- internal/storage/store.go: 实现内存存储版本
- sql/postgresql/settlement_withdraw_constraint_v1.sql: 文档说明

测试: go test -short ./... 通过
This commit is contained in:
Your Name
2026-04-09 22:16:08 +08:00
parent 2012e23278
commit da385ee744
10 changed files with 624 additions and 5 deletions

View File

@@ -0,0 +1,30 @@
-- Settlement Withdraw Constraint v1.0
-- 提现唯一约束说明
--
-- 问题: PRD 要求确保每个供应商同时只有一个 pending/processing 状态的提现
--
-- 解决方案: 在 repository 层使用 SELECT ... FOR UPDATE SKIP LOCKED
-- 参见: internal/repository/settlement.go - CreateWithdrawTx()
--
-- 该方法在事务开始时锁定任何现有的 pending/processing 提现记录,
-- 然后检查是否存在,如果不存在则插入新记录。
--
-- SQL 实现:
-- 1. 首先执行: SELECT id FROM supply_settlements
-- WHERE user_id = $1 AND status IN ('pending', 'processing')
-- FOR UPDATE SKIP LOCKED
--
-- 2. 如果上面查询返回行,说明有 pending 的提现,返回错误
--
-- 3. 如果上面查询没有返回行ErrNoRows执行插入
--
-- 注意: 这个方案比使用辅助锁表更简单高效,因为:
-- - 不需要额外的表和复杂的锁管理
-- - 利用数据库原生的事务隔离和行锁机制
-- - 自动处理锁超时和死锁情况
-- 如果未来需要在数据库层面加强约束,可以考虑:
-- 1. 使用触发器在插入前检查
-- 2. 使用 Exclusion Constraint (需要额外的辅助列)
--
-- 当前应用层实现已足够防止并发提现问题。