From 0725c94bbe79a3217aa49ab09bb7bb757df2758d Mon Sep 17 00:00:00 2001 From: irving Date: Mon, 13 Oct 2025 22:45:53 -0400 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E9=98=B2=E6=AD=A2?= =?UTF-8?q?=E6=8F=90=E7=8E=B0=E5=88=9B=E5=BB=BA=E4=B8=AD=E7=9A=84=E7=AB=9E?= =?UTF-8?q?=E6=80=81=E6=9D=A1=E4=BB=B6=E5=92=8C=E9=87=8D=E5=A4=8D=E6=94=B6?= =?UTF-8?q?=E7=9B=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 earnings_line 表添加唯一约束 (tenant_id, order_id, clerk_id, earning_type, deleted) - 重排提现创建流程:先预留收益行,成功后才创建提现请求 - 在收益行预留时添加状态验证,检测并发修改 - 使用临时提现ID进行预留,创建请求后替换为真实ID - 添加唯一约束前先清理重复的收益记录(V10 迁移) 此修复解决了关键的竞态条件问题:并发提现可能创建没有资金支持的孤儿请求记录。 修复后确保快速失败行为 - 如果收益行已被占用,提现请求永远不会被创建。 --- .../service/impl/WithdrawalServiceImpl.java | 32 +++++++++++++------ .../V10__dedupe_and_unique_earnings_line.sql | 18 +++++++++++ 2 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 play-admin/src/main/resources/db/migration/V10__dedupe_and_unique_earnings_line.sql diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalServiceImpl.java index 674e60c..47ac30c 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalServiceImpl.java @@ -65,6 +65,23 @@ public class WithdrawalServiceImpl extends ServiceImpl lines = earningsService.findWithdrawable(clerkId, amount, now); if (lines.isEmpty()) throw new CustomException("可提现余额不足"); + // Reserve lines FIRST with temp ID (fail fast before creating request) + String tempWithdrawalId = "TEMP_" + IdUtils.getUuid(); + int reservedCount = 0; + for (EarningsLineEntity line : lines) { + boolean updated = earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class) + .eq(EarningsLineEntity::getId, line.getId()) + .eq(EarningsLineEntity::getStatus, line.getStatus()) // Verify status unchanged + .set(EarningsLineEntity::getStatus, "withdrawing") + .set(EarningsLineEntity::getWithdrawalId, tempWithdrawalId)); + if (!updated) { + // Another request already took this line + throw new CustomException("部分收益已被其他提现请求锁定,请重试"); + } + reservedCount++; + } + + // Only NOW create the request (after successful reservation) LocalDateTime confirmedAt = payeeProfile.getLastConfirmedAt(); String snapshotJson = buildPayeeSnapshot(payeeProfile, confirmedAt); @@ -80,20 +97,17 @@ public class WithdrawalServiceImpl extends ServiceImpl