- 在 earnings_line 表添加唯一约束 (tenant_id, order_id, clerk_id, earning_type, deleted) - 重排提现创建流程:先预留收益行,成功后才创建提现请求 - 在收益行预留时添加状态验证,检测并发修改 - 使用临时提现ID进行预留,创建请求后替换为真实ID - 添加唯一约束前先清理重复的收益记录(V10 迁移) 此修复解决了关键的竞态条件问题:并发提现可能创建没有资金支持的孤儿请求记录。 修复后确保快速失败行为 - 如果收益行已被占用,提现请求永远不会被创建。
This commit is contained in:
@@ -65,6 +65,23 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
|
||||
List<EarningsLineEntity> 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<WithdrawalRequestMapper,
|
||||
req.setStatus("pending");
|
||||
req.setOutBizNo(req.getId());
|
||||
this.save(req);
|
||||
|
||||
// Update temp ID to real ID
|
||||
earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
||||
.eq(EarningsLineEntity::getWithdrawalId, tempWithdrawalId)
|
||||
.set(EarningsLineEntity::getWithdrawalId, req.getId()));
|
||||
|
||||
// log created
|
||||
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
||||
"CREATED", null, req.getStatus(), "提现申请创建", null);
|
||||
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
||||
"PAYEE_CONFIRMED", req.getStatus(), req.getStatus(), "店员确认收款码", snapshotJson);
|
||||
|
||||
int reservedCount = 0;
|
||||
for (EarningsLineEntity line : lines) {
|
||||
earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
||||
.eq(EarningsLineEntity::getId, line.getId())
|
||||
.set(EarningsLineEntity::getStatus, "withdrawing")
|
||||
.set(EarningsLineEntity::getWithdrawalId, req.getId()));
|
||||
reservedCount++;
|
||||
}
|
||||
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
||||
"RESERVED", req.getStatus(), req.getStatus(),
|
||||
"冻结收益已预留,条数=" + reservedCount + ", 金额=" + amount, null);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Remove duplicate earnings lines per tenant/order/clerk/type and add unique constraint
|
||||
|
||||
DELETE FROM `play_earnings_line`
|
||||
WHERE `id` IN (
|
||||
SELECT dup_id FROM (
|
||||
SELECT id AS dup_id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY tenant_id, order_id, clerk_id, earning_type
|
||||
ORDER BY created_time, id
|
||||
) AS rn
|
||||
FROM `play_earnings_line`
|
||||
WHERE deleted = 0
|
||||
) ranked
|
||||
WHERE ranked.rn > 1
|
||||
);
|
||||
|
||||
ALTER TABLE `play_earnings_line`
|
||||
ADD UNIQUE KEY `uk_tenant_order_clerk_type` (`tenant_id`, `order_id`, `clerk_id`, `earning_type`, `deleted`);
|
||||
Reference in New Issue
Block a user