*/ public function build(?string $currencyCode = null, ?string $providerCode = null): array { $currency = $this->resolveBettableCurrency($currencyCode); $providerCode = strtoupper((string) ($providerCode ?: 'GLOBAL')); $playVersion = PlayConfigVersion::query() ->where('status', ConfigVersionStatus::Active->value) ->firstOrFail(); $oddsVersion = OddsVersion::query() ->where('status', ConfigVersionStatus::Active->value) ->firstOrFail(); $riskVersion = RiskCapVersion::query() ->where('status', ConfigVersionStatus::Active->value) ->firstOrFail(); /** @var Collection $configByCode */ $configByCode = PlayConfigItem::query() ->where('version_id', $playVersion->id) ->get() ->keyBy('play_code'); $oddsRows = OddsItem::query() ->where('version_id', $oddsVersion->id) ->where('currency_code', $currency->code) ->whereIn('provider_code', ['GLOBAL', $providerCode]) ->get(); /** @var Collection> */ $oddsByPlay = $oddsRows->groupBy('play_code'); $riskItems = RiskCapItem::query() ->where('version_id', $riskVersion->id) ->whereIn('provider_code', ['GLOBAL', $providerCode]) ->orderBy('normalized_number') ->get(); $plays = $configByCode->values() ->sortBy([ ['display_order', 'asc'], ['play_code', 'asc'], ]) ->map(function (PlayConfigItem $c) use ($oddsByPlay, $providerCode): array { $items = $this->resolveProviderOddsItems( $oddsByPlay->get($c->play_code, collect()), $providerCode, ); $o = $this->pickPrimaryOddsItem($items); return [ 'play_code' => $c->play_code, 'category' => $c->category, 'dimension' => $c->dimension === null ? null : (int) $c->dimension, 'bet_mode' => $c->bet_mode, 'display_name' => $c->display_name, 'sort_order' => (int) $c->display_order, 'supports_multi_number' => (bool) $c->supports_multi_number, 'master_enabled' => (bool) $c->is_enabled, 'config' => $this->serializePlayConfigItem($c), 'odds' => $o === null ? null : $this->serializeOddsItem($o), ]; })->values()->all(); return [ 'currency_code' => $currency->code, 'provider_code' => $providerCode, 'effective_versions' => [ 'play_config' => $this->serializeVersionHead($playVersion), 'odds' => $this->serializeVersionHead($oddsVersion), 'risk_cap' => $this->serializeVersionHead($riskVersion), ], 'plays' => $plays, 'risk_cap_items' => $this->resolveProviderRiskItems($riskItems, $providerCode) ->map(fn (RiskCapItem $r) => $this->serializeRiskItem($r)) ->all(), ]; } /** * @param Collection $items * @return Collection */ private function resolveProviderOddsItems(Collection $items, string $providerCode): Collection { $selected = []; foreach ($items->sortBy(fn (OddsItem $row): int => (string) $row->provider_code === $providerCode ? 0 : 1) as $row) { $dimension = $row->dimension === null ? 'null' : (string) $row->dimension; $key = implode('|', [ (string) $row->prize_scope, strtoupper((string) $row->currency_code), $dimension, ]); if (! isset($selected[$key])) { $selected[$key] = $row; } } return collect(array_values($selected)); } /** * @param Collection $items * @return Collection */ private function resolveProviderRiskItems(Collection $items, string $providerCode): Collection { $selected = []; foreach ($items->sortBy(fn (RiskCapItem $row): int => (string) ($row->provider_code ?? 'GLOBAL') === $providerCode ? 0 : 1) as $row) { $key = implode('|', [ $row->draw_id === null ? 'null' : (string) $row->draw_id, (string) $row->normalized_number, (string) $row->cap_type, ]); if (! isset($selected[$key])) { $selected[$key] = $row; } } return collect(array_values($selected)); } /** * 大厅列表展示单档赔率:优先头奖档 {@see first},兼容历史 {@see default}。 * * @param Collection $items */ private function pickPrimaryOddsItem(Collection $items): ?OddsItem { if ($items->isEmpty()) { return null; } foreach (['first', 'default', 'second', 'third', 'starter', 'consolation'] as $scope) { $hit = $items->firstWhere('prize_scope', $scope); if ($hit !== null) { return $hit; } } return $items->first(); } private function resolveBettableCurrency(?string $currencyCode): Currency { if ($currencyCode !== null && $currencyCode !== '') { $row = Currency::query()->where('code', strtoupper($currencyCode))->first(); if ($row === null || ! $row->is_enabled || ! $row->is_bettable) { throw new \InvalidArgumentException('currency'); } return $row; } return Currency::query() ->where('is_enabled', true) ->where('is_bettable', true) ->orderBy('code') ->firstOrFail(); } /** @return array */ private function serializeVersionHead(PlayConfigVersion|OddsVersion|RiskCapVersion $v): array { return [ 'id' => (int) $v->getKey(), 'version_no' => (int) $v->version_no, 'effective_at' => $v->effective_at?->toIso8601String(), ]; } /** @return array */ private function serializePlayConfigItem(PlayConfigItem $r): array { return [ 'category' => $r->category, 'dimension' => $r->dimension === null ? null : (int) $r->dimension, 'bet_mode' => $r->bet_mode, 'display_name' => $r->display_name, 'is_enabled' => (bool) $r->is_enabled, 'min_bet_amount' => (int) $r->min_bet_amount, 'max_bet_amount' => (int) $r->max_bet_amount, 'display_order' => (int) $r->display_order, 'supports_multi_number' => (bool) $r->supports_multi_number, 'reserved_rule_json' => $r->reserved_rule_json, 'rule_text_zh' => $r->rule_text_zh, 'rule_text_en' => $r->rule_text_en, 'rule_text_ne' => $r->rule_text_ne, 'extra_config_json' => $r->extra_config_json, ]; } /** @return array */ private function serializeOddsItem(OddsItem $r): array { return [ 'provider_code' => $r->provider_code ?? 'GLOBAL', 'prize_scope' => $r->prize_scope, 'odds_value' => (int) $r->odds_value, 'rebate_rate' => (string) $r->rebate_rate, 'commission_rate' => (string) $r->commission_rate, 'currency_code' => $r->currency_code, 'extra_config_json' => $r->extra_config_json, /** 赔率乘数小数位 = odds_value / 10000 */ 'odds_multiplier' => round($r->odds_value / 10000, 4), ]; } /** @return array */ private function serializeRiskItem(RiskCapItem $r): array { return [ 'provider_code' => $r->provider_code ?? 'GLOBAL', 'draw_id' => $r->draw_id, 'normalized_number' => $r->normalized_number, 'cap_amount' => (int) $r->cap_amount, 'cap_type' => $r->cap_type, ]; } }