feat: 增强奖池与钱包服务的多币种支持能力
更新 JackpotManualBurstService:在解析头奖中奖者时支持币种代码处理。 重构 SettlementBatchWorkflowService:按币种聚合玩家派奖金额,确保各币种结算准确入账。 修改 SettlementOrchestrator:按币种分别处理奖池爆奖流程,提升派奖准确性。 优化 TicketWalletService:在派奖幂等键中加入币种信息,避免多币种场景下的重复处理问题。 新增测试用例,验证多币种派奖场景及待处理交易的正确处理逻辑。
This commit is contained in:
@@ -68,7 +68,7 @@ final class JackpotManualBurstService
|
||||
}
|
||||
|
||||
$batch = $this->resolveSettlementBatch($draw);
|
||||
$winnerItems = $this->firstPrizeWinnerItems($batch);
|
||||
$winnerItems = $this->firstPrizeWinnerItems($batch, (string) $locked->currency_code);
|
||||
if ($winnerItems->isEmpty()) {
|
||||
throw new \RuntimeException('jackpot_manual_no_first_prize_winners');
|
||||
}
|
||||
@@ -170,17 +170,19 @@ final class JackpotManualBurstService
|
||||
/**
|
||||
* @return Collection<int, TicketItem>
|
||||
*/
|
||||
private function firstPrizeWinnerItems(SettlementBatch $batch): Collection
|
||||
private function firstPrizeWinnerItems(SettlementBatch $batch, string $currencyCode): Collection
|
||||
{
|
||||
$targetCurrency = strtoupper($currencyCode);
|
||||
$details = TicketSettlementDetail::query()
|
||||
->where('settlement_batch_id', $batch->id)
|
||||
->where('matched_prize_tier', 'first')
|
||||
->where('win_amount', '>', 0)
|
||||
->whereHas('ticketItem.order', fn ($q) => $q->where('currency_code', $targetCurrency))
|
||||
->with('ticketItem')
|
||||
->get();
|
||||
|
||||
return $details
|
||||
->map(fn (TicketSettlementDetail $d) => $d->ticketItem)
|
||||
->map(fn (TicketSettlementDetail $d): ?TicketItem => $d->ticketItem)
|
||||
->filter(fn (?TicketItem $item): bool => $item instanceof TicketItem)
|
||||
->values();
|
||||
}
|
||||
|
||||
@@ -121,8 +121,7 @@ final class SettlementBatchWorkflowService
|
||||
}
|
||||
|
||||
$details = $locked->details()->with(['ticketItem.order'])->get();
|
||||
$playerTotals = [];
|
||||
$currencyByPlayer = [];
|
||||
$playerCurrencyTotals = [];
|
||||
|
||||
foreach ($details as $detail) {
|
||||
$item = $detail->ticketItem;
|
||||
@@ -132,20 +131,34 @@ final class SettlementBatchWorkflowService
|
||||
$finalCredit = (int) $detail->win_amount + (int) $detail->jackpot_allocation_amount;
|
||||
if ($finalCredit > 0) {
|
||||
$pid = (int) $item->player_id;
|
||||
$playerTotals[$pid] = ($playerTotals[$pid] ?? 0) + $finalCredit;
|
||||
$currencyByPlayer[$pid] = strtoupper((string) ($item->order?->currency_code ?? 'NPR'));
|
||||
$currency = strtoupper((string) ($item->order?->currency_code ?? 'NPR'));
|
||||
$aggregateKey = $pid.':'.$currency;
|
||||
if (! isset($playerCurrencyTotals[$aggregateKey])) {
|
||||
$playerCurrencyTotals[$aggregateKey] = [
|
||||
'player_id' => $pid,
|
||||
'currency_code' => $currency,
|
||||
'amount' => 0,
|
||||
];
|
||||
}
|
||||
$playerCurrencyTotals[$aggregateKey]['amount'] += $finalCredit;
|
||||
$item->forceFill(['status' => 'settled_win', 'settled_at' => now()])->save();
|
||||
} elseif ($item->status !== 'settled_lose') {
|
||||
$item->forceFill(['status' => 'settled_lose', 'settled_at' => now()])->save();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($playerTotals as $playerId => $amount) {
|
||||
foreach ($playerCurrencyTotals as $entry) {
|
||||
$amount = (int) $entry['amount'];
|
||||
if ($amount <= 0) {
|
||||
continue;
|
||||
}
|
||||
$player = Player::query()->whereKey($playerId)->firstOrFail();
|
||||
$this->wallet->creditSettlementPayout($player, $currencyByPlayer[$playerId] ?? 'NPR', $amount, (int) $locked->id);
|
||||
$player = Player::query()->whereKey((int) $entry['player_id'])->firstOrFail();
|
||||
$this->wallet->creditSettlementPayout(
|
||||
$player,
|
||||
(string) $entry['currency_code'],
|
||||
$amount,
|
||||
(int) $locked->id
|
||||
);
|
||||
}
|
||||
|
||||
$orderIds = TicketItem::query()
|
||||
@@ -184,23 +197,35 @@ final class SettlementBatchWorkflowService
|
||||
return;
|
||||
}
|
||||
|
||||
$orderId = TicketItem::query()->where('draw_id', $batch->draw_id)->value('order_id');
|
||||
$currencyCode = strtoupper((string) (TicketOrder::query()
|
||||
->whereKey($orderId)
|
||||
->value('currency_code') ?? 'NPR'));
|
||||
$details = $batch->details()->with(['ticketItem.order'])->get();
|
||||
$restoreByCurrency = [];
|
||||
foreach ($details as $detail) {
|
||||
$amount = (int) $detail->jackpot_allocation_amount;
|
||||
if ($amount <= 0) {
|
||||
continue;
|
||||
}
|
||||
$currency = strtoupper((string) ($detail->ticketItem?->order?->currency_code ?? 'NPR'));
|
||||
$restoreByCurrency[$currency] = ($restoreByCurrency[$currency] ?? 0) + $amount;
|
||||
}
|
||||
|
||||
$pool = JackpotPool::query()
|
||||
->where('currency_code', $currencyCode)
|
||||
->where('status', 1)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($pool === null) {
|
||||
if ($restoreByCurrency === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pool->forceFill([
|
||||
'current_amount' => (int) $pool->current_amount + $restoreAmount,
|
||||
])->save();
|
||||
foreach ($restoreByCurrency as $currency => $amount) {
|
||||
$pool = JackpotPool::query()
|
||||
->where('currency_code', $currency)
|
||||
->where('status', 1)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($pool === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pool->forceFill([
|
||||
'current_amount' => (int) $pool->current_amount + (int) $amount,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,28 +125,41 @@ final class SettlementOrchestrator
|
||||
];
|
||||
}
|
||||
|
||||
$currency = strtoupper((string) ($ticketItems->first()?->order?->currency_code ?? 'NPR'));
|
||||
$pool = JackpotPool::query()
|
||||
->where('currency_code', $currency)
|
||||
->where('status', 1)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
$allocations = [];
|
||||
$totalJackpotPayout = 0;
|
||||
$jackpotTrigger = null;
|
||||
$jackpotPoolAfter = null;
|
||||
if ($pool !== null) {
|
||||
$burstInput = collect($prepared)->map(fn (array $p): array => [
|
||||
$jackpotBursts = [];
|
||||
$preparedByCurrency = collect($prepared)->groupBy(
|
||||
fn (array $p): string => strtoupper((string) ($p['item']->order?->currency_code ?? 'NPR')),
|
||||
);
|
||||
foreach ($preparedByCurrency as $currency => $currencyPrepared) {
|
||||
$pool = JackpotPool::query()
|
||||
->where('currency_code', $currency)
|
||||
->where('status', 1)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
if ($pool === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$burstInput = collect($currencyPrepared)->map(fn (array $p): array => [
|
||||
'item' => $p['item'],
|
||||
'matched_tier' => $p['matched_tier'],
|
||||
'gross_win' => $p['gross_win'],
|
||||
]);
|
||||
$burstOut = $this->jackpotBurst->allocate($locked, $pool, $burstInput);
|
||||
$allocations = $burstOut['allocations'];
|
||||
$totalJackpotPayout = (int) $burstOut['pool_payout'];
|
||||
$jackpotTrigger = $burstOut['trigger'];
|
||||
$jackpotPoolAfter = (int) $pool->fresh()->current_amount;
|
||||
$allocations = array_replace($allocations, $burstOut['allocations']);
|
||||
$currencyPayout = (int) $burstOut['pool_payout'];
|
||||
$totalJackpotPayout += $currencyPayout;
|
||||
|
||||
if ($currencyPayout > 0 && is_string($burstOut['trigger'])) {
|
||||
$jackpotBursts[] = [
|
||||
'currency' => $currency,
|
||||
'payout' => $currencyPayout,
|
||||
'trigger' => $burstOut['trigger'],
|
||||
'pool_after' => (int) $pool->fresh()->current_amount,
|
||||
'winner_count' => count($burstOut['allocations']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$ticketCount = 0;
|
||||
@@ -206,16 +219,16 @@ final class SettlementOrchestrator
|
||||
'settle_version' => $nextSettleVersion,
|
||||
])->save();
|
||||
|
||||
if ($pool !== null && $totalJackpotPayout > 0 && is_string($jackpotTrigger)) {
|
||||
foreach ($jackpotBursts as $burst) {
|
||||
$this->hallRealtime->notifyJackpotBurst(
|
||||
(int) $locked->id,
|
||||
(string) $locked->draw_no,
|
||||
$board->firstPrizeNumber4d(),
|
||||
$currency,
|
||||
$totalJackpotPayout,
|
||||
count($allocations),
|
||||
$jackpotTrigger,
|
||||
(int) $jackpotPoolAfter,
|
||||
(string) $burst['currency'],
|
||||
(int) $burst['payout'],
|
||||
(int) $burst['winner_count'],
|
||||
(string) $burst['trigger'],
|
||||
(int) $burst['pool_after'],
|
||||
);
|
||||
$this->hallRealtime->notifyStatusChange($this->hallSnapshot->build());
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ final class TicketWalletService
|
||||
}
|
||||
|
||||
$currency = strtoupper($currencyCode);
|
||||
$idempotentKey = 'settle-payout:'.$settlementBatchId.':'.$player->id;
|
||||
$idempotentKey = 'settle-payout:'.$settlementBatchId.':'.$player->id.':'.$currency;
|
||||
if (WalletTxn::query()->where('idempotent_key', $idempotentKey)->where('biz_type', 'settle_payout')->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user