fix(funds): 加固转账冲正、结算收付幂等与坏账核销
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

- 转入 main_site_timeout 等不可冲正场景直接拒绝,避免假结案
- payment_records / settlement_adjustments 增加 partial unique 索引
- 坏账核销行锁 + meta 幂等回放;补差单记录 result_bill_id
- 新增 FundOperationsHardeningTest 覆盖关键路径
This commit is contained in:
2026-06-26 15:19:17 +08:00
parent 3f04b4ebe3
commit 7ec8f4c5a2
8 changed files with 535 additions and 104 deletions

View File

@@ -4,6 +4,8 @@ namespace App\Services\AgentSettlement;
use App\Models\Player;
use App\Services\Player\PlayerCreditService;
use App\Support\DatabaseUniqueViolation;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
@@ -38,19 +40,46 @@ final class SettlementPaymentService
}
}
DB::transaction(function () use ($billId, $amount, $adminUserId, $meta, $idempotencyKey): void {
if ($idempotencyKey !== null && $idempotencyKey !== '') {
$existing = DB::table('payment_records')
->where('settlement_bill_id', $billId)
->where('idempotency_key', $idempotencyKey)
->lockForUpdate()
->first();
if ($existing !== null) {
return;
}
try {
DB::transaction(function () use ($billId, $amount, $adminUserId, $meta, $idempotencyKey): void {
$this->insertPaymentRecord($billId, $amount, $adminUserId, $meta, $idempotencyKey);
});
} catch (QueryException $e) {
if (
$idempotencyKey !== null
&& $idempotencyKey !== ''
&& DatabaseUniqueViolation::matches($e)
&& $this->paymentIdempotencyExists($billId, $idempotencyKey)
) {
return;
}
$bill = DB::table('settlement_bills')->where('id', $billId)->lockForUpdate()->first();
throw $e;
}
}
/**
* @param array{method?: string|null, proof?: string|null, remark?: string|null, idempotency_key?: string|null} $meta
*/
private function insertPaymentRecord(
int $billId,
int $amount,
int $adminUserId,
array $meta,
?string $idempotencyKey,
): void {
if ($idempotencyKey !== null && $idempotencyKey !== '') {
$existing = DB::table('payment_records')
->where('settlement_bill_id', $billId)
->where('idempotency_key', $idempotencyKey)
->lockForUpdate()
->first();
if ($existing !== null) {
return;
}
}
$bill = DB::table('settlement_bills')->where('id', $billId)->lockForUpdate()->first();
if ($bill === null) {
throw new \InvalidArgumentException('bill_not_found');
}
@@ -122,7 +151,14 @@ final class SettlementPaymentService
}
$this->periodCompletion->syncIfReady((int) $bill->settlement_period_id);
});
}
private function paymentIdempotencyExists(int $billId, string $idempotencyKey): bool
{
return DB::table('payment_records')
->where('settlement_bill_id', $billId)
->where('idempotency_key', $idempotencyKey)
->exists();
}
/**