whereKey($pool->id)->lockForUpdate()->firstOrFail(); if ((int) $locked->status !== 1) { throw new \RuntimeException('jackpot_disabled'); } if ((int) $locked->current_amount <= 0) { throw new \RuntimeException('jackpot_pool_empty'); } $draw = Draw::query()->whereKey($drawId)->firstOrFail(); $this->assertDrawReady($draw); if (JackpotPayoutLog::query() ->where('jackpot_pool_id', $locked->id) ->where('draw_id', $drawId) ->exists()) { throw new \RuntimeException('jackpot_already_burst_for_draw'); } $batch = $this->resolveSettlementBatch($draw); $winnerItems = $this->firstPrizeWinnerItems($batch); if ($winnerItems->isEmpty()) { throw new \RuntimeException('jackpot_manual_no_first_prize_winners'); } $existingJackpot = (int) $batch->total_jackpot_payout_amount; if ($existingJackpot > 0) { throw new \RuntimeException('jackpot_already_allocated_for_draw'); } $burst = $this->allocator->burstManual($draw, $locked, $winnerItems); $poolPayout = (int) $burst['pool_payout']; if ($poolPayout <= 0) { return [ 'current_amount' => (int) $locked->current_amount, 'burst_amount' => 0, 'log_id' => null, 'winner_count' => 0, 'draw_no' => (string) $draw->draw_no, 'wallet_credited' => false, ]; } $allocations = $burst['allocations']; $this->applyAllocationsToSettlement($batch, $allocations); $walletCredited = $this->creditWalletsIfAlreadyPaid($batch, $allocations, (int) $burst['log_id'], $locked->currency_code); $locked->refresh(); $firstPrizeNumber = $this->drawResults->firstPrizeNumber4dForDraw($draw); if ($firstPrizeNumber === '') { $firstPrizeNumber = '----'; } $this->hallRealtime->notifyJackpotBurst( (int) $draw->id, (string) $draw->draw_no, $firstPrizeNumber, (string) $locked->currency_code, $poolPayout, count($allocations), 'manual', (int) $locked->current_amount, ); $this->hallRealtime->notifyStatusChange($this->hallSnapshot->build()); return [ 'current_amount' => (int) $locked->current_amount, 'burst_amount' => $poolPayout, 'log_id' => (int) $burst['log_id'], 'winner_count' => count($allocations), 'draw_no' => (string) $draw->draw_no, 'wallet_credited' => $walletCredited, ]; }); } private function assertDrawReady(Draw $draw): void { $allowed = [ DrawStatus::Settling->value, DrawStatus::Settled->value, ]; if (! in_array($draw->status, $allowed, true)) { throw new \RuntimeException('draw_not_ready_for_jackpot_burst'); } $hasPublished = DrawResultBatch::query() ->where('draw_id', $draw->id) ->where('status', DrawResultBatchStatus::Published->value) ->where('result_version', (int) $draw->current_result_version) ->exists(); if (! $hasPublished) { throw new \RuntimeException('draw_result_not_published'); } } private function resolveSettlementBatch(Draw $draw): SettlementBatch { $batch = SettlementBatch::query() ->where('draw_id', $draw->id) ->whereIn('status', [ SettlementBatchStatus::PendingReview->value, SettlementBatchStatus::Approved->value, SettlementBatchStatus::Paid->value, SettlementBatchStatus::Completed->value, ]) ->orderByDesc('id') ->first(); if ($batch === null) { throw new \RuntimeException('settlement_batch_not_found'); } return $batch; } /** * @return Collection */ private function firstPrizeWinnerItems(SettlementBatch $batch): Collection { $details = TicketSettlementDetail::query() ->where('settlement_batch_id', $batch->id) ->where('matched_prize_tier', 'first') ->where('win_amount', '>', 0) ->with('ticketItem') ->get(); return $details ->map(fn (TicketSettlementDetail $d) => $d->ticketItem) ->filter(fn (?TicketItem $item): bool => $item instanceof TicketItem) ->values(); } /** * @param array $allocations */ private function applyAllocationsToSettlement(SettlementBatch $batch, array $allocations): void { $addedJackpot = 0; foreach ($allocations as $ticketItemId => $share) { $detail = TicketSettlementDetail::query() ->where('settlement_batch_id', $batch->id) ->where('ticket_item_id', $ticketItemId) ->first(); if ($detail === null) { continue; } $detail->forceFill(['jackpot_allocation_amount' => $share])->save(); $item = $detail->ticketItem; if ($item !== null) { $item->forceFill(['jackpot_win_amount' => $share])->save(); } $addedJackpot += $share; } if ($addedJackpot > 0) { $batch->forceFill([ 'total_jackpot_payout_amount' => (int) $batch->total_jackpot_payout_amount + $addedJackpot, 'total_payout_amount' => (int) $batch->total_payout_amount + $addedJackpot, ])->save(); } } /** * 若结算批次已派彩,则补发 Jackpot 份额到玩家钱包。 * * @param array $allocations */ private function creditWalletsIfAlreadyPaid( SettlementBatch $batch, array $allocations, int $jackpotLogId, string $currencyCode, ): bool { if (! in_array($batch->status, [SettlementBatchStatus::Paid->value, SettlementBatchStatus::Completed->value], true)) { return false; } $playerTotals = []; foreach ($allocations as $ticketItemId => $share) { if ($share <= 0) { continue; } $item = TicketItem::query()->whereKey($ticketItemId)->first(); if ($item === null) { continue; } $pid = (int) $item->player_id; $playerTotals[$pid] = ($playerTotals[$pid] ?? 0) + $share; } foreach ($playerTotals as $playerId => $amount) { $player = Player::query()->whereKey($playerId)->firstOrFail(); $this->wallet->creditJackpotManualPayout( $player, $currencyCode, $amount, (int) $batch->id, $jackpotLogId, ); } return $playerTotals !== []; } }