$payload * @return array */ public function preview(Player $player, array $payload): array { $drawNo = trim((string) ($payload['draw_id'] ?? '')); $draw = $drawNo === '' ? null : Draw::query()->where('draw_no', $drawNo)->first(); if ($draw === null) { throw new TicketOperationException('draw_not_found', ErrorCode::BetInvalidDraw->value); } if (! $this->drawHallSnapshot->isBettingOpen($draw)) { throw new TicketOperationException('draw_closed', ErrorCode::DrawClosed->value); } $currencyCode = strtoupper((string) $payload['currency_code']); $this->assertCreditCurrencyMatchesPlayer($player, $currencyCode); $fallbackProviderCodes = is_array($payload['provider_codes'] ?? null) ? $payload['provider_codes'] : null; $lines = []; $totalBet = 0; $totalRebate = 0; $totalActualDeduct = 0; $totalEstimatedPayout = 0; $warningRows = []; $closedPlayCleanupRows = []; foreach ((array) $payload['lines'] as $index => $line) { $providers = $this->betProviderResolver->resolve( is_array($line['provider_codes'] ?? null) ? $line['provider_codes'] : $fallbackProviderCodes, ); foreach ($providers as $provider) { try { $resolved = $this->catalogResolver->resolve((string) $line['play_code'], $currencyCode, (string) $provider['code']); } catch (TicketOperationException $e) { if ($e->lotteryCode === ErrorCode::PlayModeClosed->value) { $closedPlayCleanupRows[] = [ 'client_line_no' => $index + 1, 'play_code' => (string) ($line['play_code'] ?? ''), ]; continue 2; } throw $e; } $evaluated = $this->ruleEngine->evaluateLine( (array) $line, $resolved['play_config'], $resolved['odds_items'], ); $evaluated = $this->rebateApplicator->apply($player, $evaluated); $locks = array_map(fn (array $combo): array => [ 'number_4d' => $combo['number_4d'], 'amount' => $combo['estimated_payout'], ], $evaluated['combinations']); $riskPreview = $this->riskPoolService->preview((int) $draw->id, (string) $provider['code'], $locks); foreach ($riskPreview as $riskRow) { if ($riskRow['warning']) { $warningRows[] = [ 'provider_code' => $provider['code'], 'provider_name' => $provider['name'], 'number_4d' => $riskRow['number_4d'], 'message' => '该号码赔付池已使用 80% 以上,可能即将售罄', ]; } } $rebateAmount = $this->rebateApplicator->displayRebateAmount($player, $evaluated); $totalBet += (int) $evaluated['total_bet_amount']; $totalRebate += $rebateAmount; $totalActualDeduct += (int) $evaluated['actual_deduct_amount']; $totalEstimatedPayout += (int) $evaluated['estimated_max_payout']; $lines[] = [ 'client_line_no' => $index + 1, 'provider_code' => $provider['code'], 'provider_name' => $provider['name'], 'number' => $evaluated['original_number'], 'play_code' => $evaluated['play_code'], 'normalized_number' => $evaluated['normalized_number'], 'combination_count' => $evaluated['combination_count'], 'total_bet_amount' => $evaluated['total_bet_amount'], 'rebate_rate' => $evaluated['rebate_rate_snapshot'], 'rebate_amount' => $rebateAmount, 'actual_deduct_amount' => $evaluated['actual_deduct_amount'], 'estimated_max_payout' => $evaluated['estimated_max_payout'], 'risk_status' => 'ok', 'warnings' => [], 'rule_snapshot_json' => $evaluated['rule_snapshot_json'], ]; } } if ($closedPlayCleanupRows !== []) { throw new TicketOperationException( 'play_closed_need_cleanup', ErrorCode::PlayModeClosed->value, 400, [ 'cleanup_hint' => '玩法已关闭,相关注项已清理', 'cleanup_lines' => $closedPlayCleanupRows, ], ); } $nowUtc = now()->utc(); return [ 'draw' => [ 'draw_id' => $draw->draw_no, 'status' => $this->drawHallSnapshot->effectiveHallDisplayStatus($draw, $nowUtc), 'db_status' => $draw->status, ], 'config_versions' => $this->catalogResolver->currentActiveVersionStamp(), 'summary' => [ 'total_bet_amount' => $totalBet, 'total_rebate_amount' => $totalRebate, 'total_actual_deduct' => $totalActualDeduct, 'total_estimated_payout' => $totalEstimatedPayout, 'instant_rebate_applied' => ! PlayerFundingMode::usesCredit($player), ], 'lines' => $lines, 'warnings' => $warningRows, ]; } private function assertCreditCurrencyMatchesPlayer(Player $player, string $currencyCode): void { if (! PlayerFundingMode::usesCredit($player)) { return; } $defaultCurrency = strtoupper((string) $player->default_currency); if ($currencyCode === $defaultCurrency) { return; } throw new TicketOperationException('credit_currency_mismatch', ErrorCode::ConfigCurrencyInvalid->value, 400, [ 'currency_code' => $currencyCode, 'expected_currency' => $defaultCurrency, ]); } }