- 在 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);
|
List<EarningsLineEntity> lines = earningsService.findWithdrawable(clerkId, amount, now);
|
||||||
if (lines.isEmpty()) throw new CustomException("可提现余额不足");
|
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();
|
LocalDateTime confirmedAt = payeeProfile.getLastConfirmedAt();
|
||||||
String snapshotJson = buildPayeeSnapshot(payeeProfile, confirmedAt);
|
String snapshotJson = buildPayeeSnapshot(payeeProfile, confirmedAt);
|
||||||
|
|
||||||
@@ -80,20 +97,17 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
|
|||||||
req.setStatus("pending");
|
req.setStatus("pending");
|
||||||
req.setOutBizNo(req.getId());
|
req.setOutBizNo(req.getId());
|
||||||
this.save(req);
|
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
|
// log created
|
||||||
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
||||||
"CREATED", null, req.getStatus(), "提现申请创建", null);
|
"CREATED", null, req.getStatus(), "提现申请创建", null);
|
||||||
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
||||||
"PAYEE_CONFIRMED", req.getStatus(), req.getStatus(), "店员确认收款码", snapshotJson);
|
"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(),
|
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
||||||
"RESERVED", req.getStatus(), req.getStatus(),
|
"RESERVED", req.getStatus(), req.getStatus(),
|
||||||
"冻结收益已预留,条数=" + reservedCount + ", 金额=" + amount, null);
|
"冻结收益已预留,条数=" + 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