feat: 增强奖池与钱包服务的多币种支持能力

更新 JackpotManualBurstService:在解析头奖中奖者时支持币种代码处理。
重构 SettlementBatchWorkflowService:按币种聚合玩家派奖金额,确保各币种结算准确入账。
修改 SettlementOrchestrator:按币种分别处理奖池爆奖流程,提升派奖准确性。
优化 TicketWalletService:在派奖幂等键中加入币种信息,避免多币种场景下的重复处理问题。
新增测试用例,验证多币种派奖场景及待处理交易的正确处理逻辑。
This commit is contained in:
2026-05-28 16:50:24 +08:00
parent 8ccf39dff5
commit 0323d92381
8 changed files with 857 additions and 73 deletions

View File

@@ -460,3 +460,89 @@ test('replay while order still processing returns 1002', function (): void {
->assertStatus(409)
->assertJsonPath('code', ErrorCode::WalletTransferPending->value);
});
test('transfer in replay while pending_reconcile stays pending without wallet credit', function (): void {
Http::fake([
'timeout-debit-replay.test/*' => Http::response([], 504),
]);
config(['lottery.main_site.wallet_api_url' => 'https://timeout-debit-replay.test']);
config(['lottery.main_site.wallet_debit_path' => 'debit']);
$player = Player::query()->create([
'site_code' => 'main',
'site_player_id' => 'in-pending-replay',
'username' => null,
'nickname' => null,
'default_currency' => 'NPR',
'status' => 0,
]);
$key = 'in-pending-replay-key';
$payload = [
'amount' => 500,
'currency' => 'NPR',
'idempotent_key' => $key,
];
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
->postJson('/api/v1/wallet/transfer-in', $payload)
->assertStatus(409)
->assertJsonPath('code', ErrorCode::WalletTransferPending->value);
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
->postJson('/api/v1/wallet/transfer-in', $payload)
->assertStatus(409)
->assertJsonPath('code', ErrorCode::WalletTransferPending->value);
expect(TransferOrder::query()->where('idempotent_key', $key)->count())->toBe(1)
->and(TransferOrder::query()->where('idempotent_key', $key)->value('status'))->toBe('pending_reconcile')
->and(WalletTxn::query()->where('biz_type', 'transfer_in')->count())->toBe(0);
});
test('transfer out replay while pending_reconcile keeps single pending txn', function (): void {
Http::fake([
'timeout-credit-replay.test/*' => Http::response([], 504),
]);
config(['lottery.main_site.wallet_api_url' => 'https://timeout-credit-replay.test']);
config(['lottery.main_site.wallet_credit_path' => 'credit']);
$player = Player::query()->create([
'site_code' => 'main',
'site_player_id' => 'out-pending-replay',
'username' => null,
'nickname' => null,
'default_currency' => 'NPR',
'status' => 0,
]);
PlayerWallet::query()->create([
'player_id' => $player->id,
'wallet_type' => 'lottery',
'currency_code' => 'NPR',
'balance' => 1_000,
'frozen_balance' => 0,
'status' => 0,
'version' => 0,
]);
$key = 'out-pending-replay-key';
$payload = [
'amount' => 200,
'idempotent_key' => $key,
];
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
->postJson('/api/v1/wallet/transfer-out', $payload)
->assertStatus(409)
->assertJsonPath('code', ErrorCode::WalletTransferPending->value);
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
->postJson('/api/v1/wallet/transfer-out', $payload)
->assertStatus(409)
->assertJsonPath('code', ErrorCode::WalletTransferPending->value);
expect(TransferOrder::query()->where('idempotent_key', $key)->count())->toBe(1)
->and(TransferOrder::query()->where('idempotent_key', $key)->value('status'))->toBe('pending_reconcile')
->and(WalletTxn::query()->where('biz_type', 'transfer_out')->count())->toBe(1)
->and(WalletTxn::query()->where('biz_type', 'transfer_out')->value('status'))->toBe('pending_reconcile')
->and((int) PlayerWallet::query()->where('player_id', $player->id)->value('balance'))->toBe(800);
});