diff --git a/app/Http/Controllers/Api/V1/Admin/BetProvider/AdminBetProviderController.php b/app/Http/Controllers/Api/V1/Admin/BetProvider/AdminBetProviderController.php new file mode 100644 index 0000000..0320708 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/BetProvider/AdminBetProviderController.php @@ -0,0 +1,78 @@ + BetProvider::query() + ->orderBy('sort_order') + ->orderBy('id') + ->get() + ->map(fn (BetProvider $provider): array => $this->present($provider)) + ->values() + ->all(), + ]); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'code' => ['required', 'string', 'max:32', 'regex:/^[A-Za-z0-9_-]+$/', Rule::unique('bet_providers', 'code')], + 'name' => ['required', 'string', 'max:64'], + 'is_enabled' => ['sometimes', 'boolean'], + 'sort_order' => ['sometimes', 'integer', 'min:0'], + ]); + + $provider = BetProvider::query()->create([ + 'code' => strtoupper((string) $data['code']), + 'name' => trim((string) $data['name']), + 'is_enabled' => (bool) ($data['is_enabled'] ?? true), + 'sort_order' => (int) ($data['sort_order'] ?? 0), + ]); + + return ApiResponse::success($this->present($provider)); + } + + public function update(Request $request, BetProvider $betProvider): JsonResponse + { + $data = $request->validate([ + 'name' => ['sometimes', 'required', 'string', 'max:64'], + 'is_enabled' => ['sometimes', 'boolean'], + 'sort_order' => ['sometimes', 'integer', 'min:0'], + ]); + + $betProvider->fill(array_filter([ + 'name' => isset($data['name']) ? trim((string) $data['name']) : null, + 'is_enabled' => array_key_exists('is_enabled', $data) ? (bool) $data['is_enabled'] : null, + 'sort_order' => array_key_exists('sort_order', $data) ? (int) $data['sort_order'] : null, + ], static fn (mixed $value): bool => $value !== null))->save(); + + $betProvider->refresh(); + + return ApiResponse::success($this->present($betProvider)); + } + + private function present(BetProvider $provider): array + { + return [ + 'id' => (int) $provider->id, + 'code' => $provider->code, + 'name' => $provider->name, + 'is_enabled' => (bool) $provider->is_enabled, + 'sort_order' => (int) $provider->sort_order, + 'is_default' => $provider->code === BetProvider::DEFAULT_CODE, + 'created_at' => $provider->created_at?->toIso8601String(), + 'updated_at' => $provider->updated_at?->toIso8601String(), + ]; + } +} diff --git a/app/Http/Controllers/Api/V1/Admin/Ticket/AdminTicketItemIndexController.php b/app/Http/Controllers/Api/V1/Admin/Ticket/AdminTicketItemIndexController.php index d39d30e..c1cf957 100644 --- a/app/Http/Controllers/Api/V1/Admin/Ticket/AdminTicketItemIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Ticket/AdminTicketItemIndexController.php @@ -83,6 +83,10 @@ final class AdminTicketItemIndexController extends Controller $number = trim((string) ($validated['number'] ?? '')); $this->applyTicketItemNumberSearch($query, $number); + $providerCode = strtoupper(trim((string) ($validated['provider_code'] ?? ''))); + if ($providerCode !== '') { + $query->where('ticket_items.provider_code', $providerCode); + } $this->applyOrderPlacedDateRange( $query, is_string($validated['start_date'] ?? null) ? $validated['start_date'] : null, @@ -113,6 +117,8 @@ final class AdminTicketItemIndexController extends Controller 'order_no' => $row->order?->order_no, 'draw_no' => $row->draw?->draw_no, 'currency_code' => $row->order?->currency_code, + 'provider_code' => $row->provider_code, + 'provider_name' => $row->provider_name, 'play_code' => $row->play_code, 'original_number' => $row->original_number, // 历史字段:保留兼容,实际单位为 minor。 diff --git a/app/Http/Controllers/Api/V1/Admin/Ticket/AdminTicketItemShowController.php b/app/Http/Controllers/Api/V1/Admin/Ticket/AdminTicketItemShowController.php index 7e65277..d1a773c 100644 --- a/app/Http/Controllers/Api/V1/Admin/Ticket/AdminTicketItemShowController.php +++ b/app/Http/Controllers/Api/V1/Admin/Ticket/AdminTicketItemShowController.php @@ -57,6 +57,8 @@ final class AdminTicketItemShowController extends Controller 'draw_id' => (int) $item->draw_id, 'draw_no' => $item->draw?->draw_no, 'currency_code' => $item->order?->currency_code, + 'provider_code' => $item->provider_code, + 'provider_name' => $item->provider_name, 'player_id' => $item->player_id, 'site_code' => $item->player?->site_code, 'site_player_id' => $item->player?->site_player_id, diff --git a/app/Http/Controllers/Api/V1/BetProvider/BetProviderIndexController.php b/app/Http/Controllers/Api/V1/BetProvider/BetProviderIndexController.php new file mode 100644 index 0000000..a23b9a3 --- /dev/null +++ b/app/Http/Controllers/Api/V1/BetProvider/BetProviderIndexController.php @@ -0,0 +1,31 @@ +where('is_enabled', true) + ->orderBy('sort_order') + ->orderBy('id') + ->get(['code', 'name', 'sort_order']) + ->map(fn (BetProvider $provider): array => [ + 'code' => $provider->code, + 'name' => $provider->name, + 'sort_order' => (int) $provider->sort_order, + 'is_default' => $provider->code === BetProvider::DEFAULT_CODE, + ]) + ->values() + ->all(); + + return ApiResponse::success(['items' => $items]); + } +} diff --git a/app/Http/Controllers/Api/V1/Ticket/TicketItemShowController.php b/app/Http/Controllers/Api/V1/Ticket/TicketItemShowController.php index 3edfe76..58e6444 100644 --- a/app/Http/Controllers/Api/V1/Ticket/TicketItemShowController.php +++ b/app/Http/Controllers/Api/V1/Ticket/TicketItemShowController.php @@ -148,6 +148,8 @@ final class TicketItemShowController extends Controller 'order_status' => $item->order?->status, 'draw_no' => $draw?->draw_no, 'currency_code' => $item->order?->currency_code, + 'provider_code' => $item->provider_code, + 'provider_name' => $item->provider_name, 'play_code' => $item->play_code, 'dimension' => $item->dimension, 'digit_slot' => $item->digit_slot, diff --git a/app/Http/Controllers/Api/V1/Ticket/TicketItemsIndexController.php b/app/Http/Controllers/Api/V1/Ticket/TicketItemsIndexController.php index 72e1307..03153f3 100644 --- a/app/Http/Controllers/Api/V1/Ticket/TicketItemsIndexController.php +++ b/app/Http/Controllers/Api/V1/Ticket/TicketItemsIndexController.php @@ -79,6 +79,8 @@ final class TicketItemsIndexController extends Controller 'order_status' => $row->order?->status, 'draw_no' => $row->draw?->draw_no, 'currency_code' => $row->order?->currency_code, + 'provider_code' => $row->provider_code, + 'provider_name' => $row->provider_name, 'play_code' => $row->play_code, 'original_number' => $row->original_number, 'total_bet_amount' => $totalBet, diff --git a/app/Http/Requests/Admin/TicketItemListRequest.php b/app/Http/Requests/Admin/TicketItemListRequest.php index 90a2764..d552fe2 100644 --- a/app/Http/Requests/Admin/TicketItemListRequest.php +++ b/app/Http/Requests/Admin/TicketItemListRequest.php @@ -30,6 +30,7 @@ final class TicketItemListRequest extends ApiFormRequest 'site_code' => ['sometimes', 'nullable', 'string', 'max:64'], 'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'], 'draw_no' => ['sometimes', 'nullable', 'string', 'max:32'], + 'provider_code' => ['sometimes', 'nullable', 'string', 'max:32'], 'status' => ['sometimes'], 'status.*' => ['string', 'max:32'], 'number' => ['sometimes', 'nullable', 'string', 'max:64'], diff --git a/app/Http/Requests/Ticket/TicketBetRequest.php b/app/Http/Requests/Ticket/TicketBetRequest.php index 7fcaeca..1835d09 100644 --- a/app/Http/Requests/Ticket/TicketBetRequest.php +++ b/app/Http/Requests/Ticket/TicketBetRequest.php @@ -20,6 +20,8 @@ abstract class TicketBetRequest extends ApiFormRequest 'draw_id' => ['required', 'string', 'max:32'], 'currency_code' => ['required', 'string', 'max:16'], 'client_trace_id' => ['nullable', 'string', 'max:64'], + 'provider_codes' => ['nullable', 'array', 'min:1', 'max:16'], + 'provider_codes.*' => ['required', 'string', 'max:32', 'regex:/^[A-Za-z0-9_-]+$/'], 'lines' => ['required', 'array', 'min:1', 'max:100'], 'lines.*.number' => ['required', 'string', 'max:32'], 'lines.*.play_code' => ['required', 'string', 'max:32'], diff --git a/app/Models/BetProvider.php b/app/Models/BetProvider.php new file mode 100644 index 0000000..c870fae --- /dev/null +++ b/app/Models/BetProvider.php @@ -0,0 +1,26 @@ + 'boolean', + 'sort_order' => 'integer', + ]; + } +} diff --git a/app/Models/TicketItem.php b/app/Models/TicketItem.php index 19d4541..815d46c 100644 --- a/app/Models/TicketItem.php +++ b/app/Models/TicketItem.php @@ -15,6 +15,8 @@ final class TicketItem extends Model 'order_id', 'player_id', 'draw_id', + 'provider_code', + 'provider_name', 'original_number', 'normalized_number', 'play_code', diff --git a/app/Services/Ticket/BetProviderResolver.php b/app/Services/Ticket/BetProviderResolver.php new file mode 100644 index 0000000..09c8ce0 --- /dev/null +++ b/app/Services/Ticket/BetProviderResolver.php @@ -0,0 +1,68 @@ +|null $providerCodes + * @return list + */ + public function resolve(?array $providerCodes): array + { + $usingImplicitDefault = $providerCodes === null || $providerCodes === []; + $codes = $usingImplicitDefault ? [BetProvider::DEFAULT_CODE] : $providerCodes; + + $codes = array_values(array_unique(array_filter(array_map( + static fn (mixed $code): string => strtoupper(trim((string) $code)), + $codes, + )))); + + if ($codes === []) { + $codes = [BetProvider::DEFAULT_CODE]; + $usingImplicitDefault = true; + } + + $providers = BetProvider::query() + ->whereIn('code', $codes) + ->where('is_enabled', true) + ->orderBy('sort_order') + ->orderBy('id') + ->get(['code', 'name']) + ->keyBy('code'); + + $missing = array_values(array_filter( + $codes, + static fn (string $code): bool => ! $providers->has($code), + )); + + if ( + $missing === [BetProvider::DEFAULT_CODE] + && $usingImplicitDefault + && ! BetProvider::query()->exists() + ) { + return [[ + 'code' => BetProvider::DEFAULT_CODE, + 'name' => 'Singapore', + ]]; + } + + if ($missing !== []) { + throw new TicketOperationException('bet_provider_unavailable', ErrorCode::ValidationFailed->value, 422, [ + 'provider_codes' => $missing, + ]); + } + + return array_map( + static fn (string $code): array => [ + 'code' => $code, + 'name' => (string) $providers->get($code)?->name, + ], + $codes, + ); + } +} diff --git a/app/Services/Ticket/TicketPlacementService.php b/app/Services/Ticket/TicketPlacementService.php index 2541e04..bb3d0e7 100644 --- a/app/Services/Ticket/TicketPlacementService.php +++ b/app/Services/Ticket/TicketPlacementService.php @@ -30,6 +30,7 @@ final class TicketPlacementService private readonly JackpotContributionService $jackpotContribution, private readonly DrawHallSnapshotBuilder $drawHallSnapshot, private readonly PlayerCreditService $playerCreditService, + private readonly BetProviderResolver $betProviderResolver, ) {} /** @@ -118,6 +119,9 @@ final class TicketPlacementService } $configVersions = $this->catalogResolver->lockActiveConfigVersionsForPlacement($expectedVersions); + $providers = $this->betProviderResolver->resolve( + is_array($payload['provider_codes'] ?? null) ? $payload['provider_codes'] : null, + ); $evaluatedLines = []; $totalBet = 0; @@ -141,25 +145,24 @@ final class TicketPlacementService throw $e; } - $evaluated = $this->ruleEngine->evaluateLine( - (array) $line, - $resolved['play_config'], - $resolved['odds_items'], - ); - $evaluated = $this->rebateApplicator->apply($player, $evaluated); + foreach ($providers as $provider) { + $evaluated = $this->ruleEngine->evaluateLine( + (array) $line, + $resolved['play_config'], + $resolved['odds_items'], + ); + $evaluated = $this->rebateApplicator->apply($player, $evaluated); + $evaluated['provider_code'] = $provider['code']; + $evaluated['provider_name'] = $provider['name']; - $locks = array_map(fn (array $combo): array => [ - 'number_4d' => $combo['number_4d'], - 'amount' => $combo['estimated_payout'], - ], $evaluated['combinations']); - // place 阶段以 acquire 的原子扣减结果为准,允许单行售罄后形成混合成功/失败结果。 - - $evaluatedLines[] = $evaluated; - $rebateAmount = $this->rebateApplicator->persistedInstantRebateAmount($player, $evaluated); - $totalBet += (int) $evaluated['total_bet_amount']; - $totalRebate += $rebateAmount; - $totalActualDeduct += (int) $evaluated['actual_deduct_amount']; - $totalEstimatedPayout += (int) $evaluated['estimated_max_payout']; + // place 阶段以 acquire 的原子扣减结果为准,允许单行售罄后形成混合成功/失败结果。 + $evaluatedLines[] = $evaluated; + $rebateAmount = $this->rebateApplicator->persistedInstantRebateAmount($player, $evaluated); + $totalBet += (int) $evaluated['total_bet_amount']; + $totalRebate += $rebateAmount; + $totalActualDeduct += (int) $evaluated['actual_deduct_amount']; + $totalEstimatedPayout += (int) $evaluated['estimated_max_payout']; + } } if ($closedPlayCleanupRows !== []) { @@ -241,6 +244,8 @@ final class TicketPlacementService 'order_id' => $order->id, 'player_id' => $player->id, 'draw_id' => $draw->id, + 'provider_code' => $evaluated['provider_code'], + 'provider_name' => $evaluated['provider_name'], 'original_number' => $evaluated['original_number'], 'normalized_number' => $this->normalizedNumberForStorage($evaluated), 'play_code' => $evaluated['play_code'], @@ -483,6 +488,8 @@ final class TicketPlacementService ->get() ->map(fn (TicketItem $item) => [ 'ticket_no' => $item->ticket_no, + 'provider_code' => $item->provider_code, + 'provider_name' => $item->provider_name, 'play_code' => $item->play_code, 'number' => $item->original_number, 'total_bet_amount' => (int) $item->total_bet_amount, diff --git a/app/Services/Ticket/TicketPreviewService.php b/app/Services/Ticket/TicketPreviewService.php index 88c8811..6660510 100644 --- a/app/Services/Ticket/TicketPreviewService.php +++ b/app/Services/Ticket/TicketPreviewService.php @@ -17,6 +17,7 @@ final class TicketPreviewService private readonly TicketLineInstantRebateApplicator $rebateApplicator, private readonly RiskPoolService $riskPoolService, private readonly DrawHallSnapshotBuilder $drawHallSnapshot, + private readonly BetProviderResolver $betProviderResolver, ) {} /** @@ -38,6 +39,9 @@ final class TicketPreviewService $currencyCode = strtoupper((string) $payload['currency_code']); $this->assertCreditCurrencyMatchesPlayer($player, $currencyCode); + $providers = $this->betProviderResolver->resolve( + is_array($payload['provider_codes'] ?? null) ? $payload['provider_codes'] : null, + ); $lines = []; $totalBet = 0; @@ -62,48 +66,52 @@ final class TicketPreviewService throw $e; } - $evaluated = $this->ruleEngine->evaluateLine( - (array) $line, - $resolved['play_config'], - $resolved['odds_items'], - ); - $evaluated = $this->rebateApplicator->apply($player, $evaluated); + foreach ($providers as $provider) { + $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, $locks); - foreach ($riskPreview as $riskRow) { - if ($riskRow['warning']) { - $warningRows[] = [ - 'number_4d' => $riskRow['number_4d'], - 'message' => '该号码赔付池已使用 80% 以上,可能即将售罄', - ]; + $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, $locks); + foreach ($riskPreview as $riskRow) { + if ($riskRow['warning']) { + $warningRows[] = [ + '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'], + ]; } - - $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, - '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 !== []) { diff --git a/app/Support/AdminAuthorizationRegistry.php b/app/Support/AdminAuthorizationRegistry.php index 4cdc6a2..c4ed471 100644 --- a/app/Support/AdminAuthorizationRegistry.php +++ b/app/Support/AdminAuthorizationRegistry.php @@ -61,6 +61,7 @@ final class AdminAuthorizationRegistry ['slug' => 'prd.risk.manage', 'name' => '风控中心·可管理', 'nav_segment' => 'risk', 'permission_codes' => ['risk.monitor.manage']], ['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理', 'nav_segment' => 'rules_plays', 'permission_codes' => ['config.play.manage']], + ['slug' => 'prd.bet_provider.manage', 'name' => '开注商·可管理', 'nav_segment' => 'rules_plays', 'permission_codes' => ['config.play.manage']], ['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理', 'nav_segment' => 'rules_odds', 'permission_codes' => ['config.odds.manage']], ['slug' => 'prd.odds.view', 'name' => '赔率配置·查看', 'nav_segment' => 'rules_odds', 'permission_codes' => ['config.odds.view']], ['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理', 'nav_segment' => 'risk_cap', 'permission_codes' => ['config.risk_cap.manage']], @@ -157,6 +158,7 @@ final class AdminAuthorizationRegistry ['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'nav_group' => 'finance', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'], 'agent_hidden' => true], ['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'nav_group' => 'finance', 'requiredAny' => ['prd.report.view']], ['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']], + ['segment' => 'bet_providers', 'label' => 'Bet providers', 'href' => '/admin/config/bet-providers', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/config/bet-providers', 'requiredAny' => ['prd.bet_provider.manage']], ['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']], ['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']], ['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']], @@ -464,6 +466,9 @@ final class AdminAuthorizationRegistry ['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.report.view', 'prd.dashboard.view']], ['code' => 'admin.play-types.patch', 'module_code' => 'config', 'name' => '玩法类型切换', 'http_method' => 'PATCH', 'uri_pattern' => '/api/v1/admin/play-types/{play_code}', 'route_name' => 'api.v1.admin.play-types.patch', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage']], + ['code' => 'admin.bet-providers.index', 'module_code' => 'config', 'name' => '开注商列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/bet-providers', 'route_name' => 'api.v1.admin.bet-providers.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.bet_provider.manage', 'prd.play_switch.manage']], + ['code' => 'admin.bet-providers.store', 'module_code' => 'config', 'name' => '创建开注商', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/bet-providers', 'route_name' => 'api.v1.admin.bet-providers.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.bet_provider.manage']], + ['code' => 'admin.bet-providers.update', 'module_code' => 'config', 'name' => '更新开注商', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/bet-providers/{betProvider}', 'route_name' => 'api.v1.admin.bet-providers.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.bet_provider.manage']], ['code' => 'admin.config.play-versions.index', 'module_code' => 'config', 'name' => '玩法版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']], ['code' => 'admin.config.play-versions.show', 'module_code' => 'config', 'name' => '玩法版本详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}', 'route_name' => 'api.v1.admin.config.play-versions.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']], ['code' => 'admin.config.play-versions.store', 'module_code' => 'config', 'name' => '创建玩法版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage']], diff --git a/database/migrations/2026_07_08_150000_create_bet_providers_table.php b/database/migrations/2026_07_08_150000_create_bet_providers_table.php new file mode 100644 index 0000000..2517314 --- /dev/null +++ b/database/migrations/2026_07_08_150000_create_bet_providers_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('code', 32)->unique(); + $table->string('name', 64); + $table->boolean('is_enabled')->default(true); + $table->unsignedInteger('sort_order')->default(0); + $table->timestamps(); + }); + + Schema::table('ticket_items', function (Blueprint $table): void { + $table->string('provider_code', 32)->default('SG')->after('draw_id'); + $table->string('provider_name', 64)->default('Singapore')->after('provider_code'); + $table->index('provider_code', 'idx_ticket_items_provider_code'); + }); + + } + + public function down(): void + { + Schema::table('ticket_items', function (Blueprint $table): void { + $table->dropIndex('idx_ticket_items_provider_code'); + $table->dropColumn(['provider_code', 'provider_name']); + }); + + Schema::dropIfExists('bet_providers'); + } +}; diff --git a/database/seeders/BetProviderSeeder.php b/database/seeders/BetProviderSeeder.php new file mode 100644 index 0000000..856d41d --- /dev/null +++ b/database/seeders/BetProviderSeeder.php @@ -0,0 +1,29 @@ + 'SG', 'name' => 'Singapore', 'sort_order' => 10], + ['code' => 'MY', 'name' => 'Malaysia', 'sort_order' => 20], + ['code' => 'TH', 'name' => 'Thailand', 'sort_order' => 30], + ]; + + foreach ($rows as $row) { + BetProvider::query()->updateOrCreate( + ['code' => $row['code']], + [ + 'name' => $row['name'], + 'is_enabled' => true, + 'sort_order' => $row['sort_order'], + ], + ); + } + } +} diff --git a/database/seeders/FoundationSeeder.php b/database/seeders/FoundationSeeder.php index 930ef7f..00fa343 100644 --- a/database/seeders/FoundationSeeder.php +++ b/database/seeders/FoundationSeeder.php @@ -20,6 +20,7 @@ final class FoundationSeeder extends Seeder { $this->call([ CurrencySeeder::class, + BetProviderSeeder::class, PlayTypeSeeder::class, OperationalConfigV1Seeder::class, OddsPrizeScopesBackfillSeeder::class, diff --git a/routes/api/v1/admin/config.php b/routes/api/v1/admin/config.php index bd4cc1f..afca141 100644 --- a/routes/api/v1/admin/config.php +++ b/routes/api/v1/admin/config.php @@ -22,6 +22,7 @@ use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigVersionPublishController; use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigVersionDestroyController; use App\Http\Controllers\Api\V1\Admin\Config\RiskCapVersionDestroyController; use App\Http\Controllers\Api\V1\Admin\AdminSettingController; +use App\Http\Controllers\Api\V1\Admin\BetProvider\AdminBetProviderController; /** * 管理员配置管理路由(玩法/赔率/封顶)。 @@ -32,6 +33,19 @@ Route::middleware('admin.api-resource') ->get('play-types', PlayTypeIndexController::class) ->name('api.v1.admin.play-types.index'); +Route::middleware('admin.api-resource') + ->prefix('bet-providers') + ->name('api.v1.admin.bet-providers.') + ->group(function (): void { + Route::get('/', [AdminBetProviderController::class, 'index']) + ->name('index'); + Route::post('/', [AdminBetProviderController::class, 'store']) + ->name('store'); + Route::put('{betProvider}', [AdminBetProviderController::class, 'update']) + ->whereNumber('betProvider') + ->name('update'); + }); + // 玩法版本只读 Route::middleware('admin.api-resource') ->prefix('config') diff --git a/routes/api/v1/player.php b/routes/api/v1/player.php index 0a55608..bda7c0d 100644 --- a/routes/api/v1/player.php +++ b/routes/api/v1/player.php @@ -12,6 +12,7 @@ use App\Http\Controllers\Api\V1\Ticket\TicketItemsIndexController; use App\Http\Controllers\Api\V1\Wallet\WalletTransferInController; use App\Http\Controllers\Api\V1\Ticket\TicketDrawMyMatchController; use App\Http\Controllers\Api\V1\Wallet\WalletTransferOutController; +use App\Http\Controllers\Api\V1\BetProvider\BetProviderIndexController; /** * 玩家端路由(需 middleware lottery.player)。 @@ -49,4 +50,7 @@ Route::middleware('lottery.player')->group(function (): void { ->where('draw_no', '[0-9]{8}-[0-9]{3}') ->name('draws.my-match'); }); + + Route::get('bet-providers', BetProviderIndexController::class) + ->name('api.v1.bet-providers.index'); });