Files
lijiaoqiao/supply-api/sql/postgresql/settlement_withdraw_constraint_v1.sql
Your Name da385ee744 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 ./... 通过
2026-04-09 22:16:08 +08:00

31 lines
1.2 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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 (需要额外的辅助列)
--
-- 当前应用层实现已足够防止并发提现问题。