From 1948b10fe6a2df6e44fd187f7db85b038abe396f Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 10 Jun 2026 10:29:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BD=A9=E7=A5=A8=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=B9=B6=E8=A1=A5=E5=85=A8=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E6=9D=83=E9=99=90=E4=B8=8E=E4=BB=A3=E7=90=86=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 43 +- .../Commands/LotteryDatabaseInitCommand.php | 2 +- .../Commands/LotteryHallCountdownCommand.php | 12 +- app/Services/Draw/DrawHallSnapshotBuilder.php | 159 +- app/Services/Draw/DrawRngRunner.php | 1 + app/Services/Draw/DrawTickService.php | 78 +- .../Draw/LotteryHallRealtimeBroadcaster.php | 39 +- .../Settlement/SettlementOrchestrator.php | 62 +- .../Settlement/SettlementTickFinalizer.php | 1 + composer.json | 4 +- config/cache.php | 2 +- config/lottery.php | 14 + .../0001_01_01_000000_create_users_table.php | 49 + .../0001_01_01_000001_create_cache_table.php | 35 + .../0001_01_01_000002_create_jobs_table.php | 57 + ...6_05_08_100000_create_currencies_table.php | 26 + ...2026_05_08_100001_create_players_table.php | 31 + ..._05_08_100002_create_admin_users_table.php | 28 + ...ate_admin_roles_and_permissions_tables.php | 45 + ..._08_100004_create_player_wallets_table.php | 30 + ..._05_08_100005_create_wallet_txns_table.php | 38 + ...08_100006_create_transfer_orders_table.php | 35 + .../2026_05_08_100007_create_draws_table.php | 35 + ...00008_create_draw_result_batches_table.php | 32 + ..._100009_create_draw_result_items_table.php | 33 + ...efault_users_and_password_reset_tables.php | 37 + ...6_05_08_130000_create_play_types_table.php | 32 + ..._play_config_versions_and_items_tables.php | 44 + ..._create_odds_versions_and_items_tables.php | 46 + ...ate_risk_cap_versions_and_items_tables.php | 39 + ...5_08_130004_create_ticket_orders_table.php | 34 + ...05_08_130005_create_ticket_items_table.php | 51 + ...30006_create_ticket_combinations_table.php | 29 + ...create_risk_pools_and_lock_logs_tables.php | 45 + ...8_create_settlement_and_jackpot_tables.php | 93 + ...9_create_report_audit_reconcile_tables.php | 87 + ...8_140000_create_lottery_settings_table.php | 34 + ...35_create_personal_access_tokens_table.php | 33 + ...duplicate_migration_filenames_in_table.php | 40 + ...name_and_nullable_email_to_admin_users.php | 79 + ...002_migrate_draw_status_to_domain_dict.php | 22 + ..._admin_user_id_to_reconcile_jobs_table.php | 25 + ...00_create_admin_user_permissions_table.php | 22 + ...000_rebuild_admin_authorization_system.php | 728 +++ ...hot_columns_to_play_config_items_table.php | 79 + ...d_combo_trigger_to_jackpot_pools_table.php | 22 + ...fig_version_snapshots_to_ticket_orders.php | 28 + ...0000_sync_complete_admin_api_resources.php | 83 + ...5_19_112752_seed_default_jackpot_pools.php | 51 + ...te_admin_role_legacy_permissions_table.php | 48 + ...1000_sync_admin_role_manage_permission.php | 124 + ...nc_player_permission_resource_bindings.php | 82 + ...01_add_admin_ticket_items_api_resource.php | 71 + ...00002_add_admin_currency_api_resources.php | 109 + ...3141_add_dimension_to_odds_items_table.php | 49 + ...dd_admin_currency_destroy_api_resource.php | 105 + ..._add_currency_manage_legacy_permission.php | 143 + ..._move_currency_menu_to_top_level_route.php | 29 + ...6_05_22_100000_add_admin_report_module.php | 160 + ..._110000_fix_admin_report_authorization.php | 122 + ...drop_redundant_admin_and_system_tables.php | 29 + ...22_130000_consolidate_admin_rbac_slugs.php | 46 + ...frontend_play_rules_html_i18n_settings.php | 50 + ..._consolidate_play_display_name_columns.php | 54 + ...5_120002_expand_audit_logs_target_type.php | 25 + ...03_refine_admin_permission_granularity.php | 62 + ...130000_remove_stale_admin_menu_actions.php | 48 + ...add_admin_dashboard_analytics_resource.php | 87 + ...00_add_settlement_batch_review_columns.php | 51 + ...00_expand_admin_permission_granularity.php | 216 + ...d_unique_client_trace_to_ticket_orders.php | 25 + ...00_add_jackpot_manual_burst_permission.php | 112 + ..._add_integration_fields_to_admin_sites.php | 237 + ...7_140001_seed_integration_menu_actions.php | 105 + ...min_api_resources_after_dashboard_view.php | 72 + ...icket_item_id_to_jackpot_contributions.php | 41 + ..._create_jackpot_pool_adjustments_table.php | 30 + ..._jackpot_pool_adjustment_api_resources.php | 105 + ...1_100000_add_query_performance_indexes.php | 53 + ...min_settings_batch_update_api_resource.php | 106 + ...2_100000_create_agent_hierarchy_tables.php | 132 + ..._02_100001_seed_agent_node_permissions.php | 195 + ...00_agent_scoped_roles_and_player_agent.php | 78 + ..._02_110001_seed_agent_role_permissions.php | 116 + ..._120000_create_agent_delegation_grants.php | 30 + ..._130000_backfill_players_agent_node_id.php | 34 + ...000_split_agent_permission_granularity.php | 177 + ..._agent_admin_user_destroy_api_resource.php | 87 + ...26_06_03_150000_align_root_agent_codes.php | 60 + ...000_agent_credit_and_settlement_tables.php | 140 + ...00_seed_agent_settlement_api_resources.php | 155 + ...000_add_agent_profile_capability_flags.php | 38 + ...00_fix_agent_primary_admin_user_status.php | 51 + ...04_100000_agent_game_settlement_ledger.php | 88 + ...20000_add_player_auth_and_funding_mode.php | 43 + ..._120000_agent_settlement_payment_proof.php | 23 + ...00_resync_agent_owner_role_permissions.php | 30 + ...latform_agent_role_and_resync_bindings.php | 63 + ...0000_agent_settlement_reports_and_tags.php | 38 + ...000_bind_agents_to_platform_agent_role.php | 44 + ...000_ensure_platform_fixed_system_roles.php | 17 + ..._seed_credit_ledger_admin_api_resource.php | 94 + ...ayer_credit_accounts_and_credit_ledger.php | 32 +- ...lign_postgres_indexes_with_live_schema.php | 48 + database/migrations/README.md | 15 +- database/schema/pgsql-schema.sql | 4889 ----------------- lottery | Bin 0 -> 786432 bytes tests/Feature/DrawPipelineTest.php | 129 + 108 files changed, 7083 insertions(+), 5033 deletions(-) create mode 100644 database/migrations/0001_01_01_000000_create_users_table.php create mode 100644 database/migrations/0001_01_01_000001_create_cache_table.php create mode 100644 database/migrations/0001_01_01_000002_create_jobs_table.php create mode 100644 database/migrations/2026_05_08_100000_create_currencies_table.php create mode 100644 database/migrations/2026_05_08_100001_create_players_table.php create mode 100644 database/migrations/2026_05_08_100002_create_admin_users_table.php create mode 100644 database/migrations/2026_05_08_100003_create_admin_roles_and_permissions_tables.php create mode 100644 database/migrations/2026_05_08_100004_create_player_wallets_table.php create mode 100644 database/migrations/2026_05_08_100005_create_wallet_txns_table.php create mode 100644 database/migrations/2026_05_08_100006_create_transfer_orders_table.php create mode 100644 database/migrations/2026_05_08_100007_create_draws_table.php create mode 100644 database/migrations/2026_05_08_100008_create_draw_result_batches_table.php create mode 100644 database/migrations/2026_05_08_100009_create_draw_result_items_table.php create mode 100644 database/migrations/2026_05_08_120000_drop_laravel_default_users_and_password_reset_tables.php create mode 100644 database/migrations/2026_05_08_130000_create_play_types_table.php create mode 100644 database/migrations/2026_05_08_130001_create_play_config_versions_and_items_tables.php create mode 100644 database/migrations/2026_05_08_130002_create_odds_versions_and_items_tables.php create mode 100644 database/migrations/2026_05_08_130003_create_risk_cap_versions_and_items_tables.php create mode 100644 database/migrations/2026_05_08_130004_create_ticket_orders_table.php create mode 100644 database/migrations/2026_05_08_130005_create_ticket_items_table.php create mode 100644 database/migrations/2026_05_08_130006_create_ticket_combinations_table.php create mode 100644 database/migrations/2026_05_08_130007_create_risk_pools_and_lock_logs_tables.php create mode 100644 database/migrations/2026_05_08_130008_create_settlement_and_jackpot_tables.php create mode 100644 database/migrations/2026_05_08_130009_create_report_audit_reconcile_tables.php create mode 100644 database/migrations/2026_05_08_140000_create_lottery_settings_table.php create mode 100644 database/migrations/2026_05_09_023835_create_personal_access_tokens_table.php create mode 100644 database/migrations/2026_05_09_119999_rename_duplicate_migration_filenames_in_table.php create mode 100644 database/migrations/2026_05_09_120001_add_username_and_nullable_email_to_admin_users.php create mode 100644 database/migrations/2026_05_09_120002_migrate_draw_status_to_domain_dict.php create mode 100644 database/migrations/2026_05_11_120000_add_admin_user_id_to_reconcile_jobs_table.php create mode 100644 database/migrations/2026_05_11_173000_create_admin_user_permissions_table.php create mode 100644 database/migrations/2026_05_13_100000_rebuild_admin_authorization_system.php create mode 100644 database/migrations/2026_05_16_000100_add_snapshot_columns_to_play_config_items_table.php create mode 100644 database/migrations/2026_05_18_000001_add_combo_trigger_to_jackpot_pools_table.php create mode 100644 database/migrations/2026_05_18_090000_add_config_version_snapshots_to_ticket_orders.php create mode 100644 database/migrations/2026_05_18_120000_sync_complete_admin_api_resources.php create mode 100644 database/migrations/2026_05_19_112752_seed_default_jackpot_pools.php create mode 100644 database/migrations/2026_05_19_120000_create_admin_role_legacy_permissions_table.php create mode 100644 database/migrations/2026_05_19_121000_sync_admin_role_manage_permission.php create mode 100644 database/migrations/2026_05_19_122000_sync_player_permission_resource_bindings.php create mode 100644 database/migrations/2026_05_20_000001_add_admin_ticket_items_api_resource.php create mode 100644 database/migrations/2026_05_21_000002_add_admin_currency_api_resources.php create mode 100644 database/migrations/2026_05_21_093141_add_dimension_to_odds_items_table.php create mode 100644 database/migrations/2026_05_21_150000_add_admin_currency_destroy_api_resource.php create mode 100644 database/migrations/2026_05_21_160000_add_currency_manage_legacy_permission.php create mode 100644 database/migrations/2026_05_21_170000_move_currency_menu_to_top_level_route.php create mode 100644 database/migrations/2026_05_22_100000_add_admin_report_module.php create mode 100644 database/migrations/2026_05_22_110000_fix_admin_report_authorization.php create mode 100644 database/migrations/2026_05_22_120000_drop_redundant_admin_and_system_tables.php create mode 100644 database/migrations/2026_05_22_130000_consolidate_admin_rbac_slugs.php create mode 100644 database/migrations/2026_05_22_140000_add_frontend_play_rules_html_i18n_settings.php create mode 100644 database/migrations/2026_05_25_120001_consolidate_play_display_name_columns.php create mode 100644 database/migrations/2026_05_25_120002_expand_audit_logs_target_type.php create mode 100644 database/migrations/2026_05_25_120003_refine_admin_permission_granularity.php create mode 100644 database/migrations/2026_05_25_130000_remove_stale_admin_menu_actions.php create mode 100644 database/migrations/2026_05_25_140000_add_admin_dashboard_analytics_resource.php create mode 100644 database/migrations/2026_05_25_180000_add_settlement_batch_review_columns.php create mode 100644 database/migrations/2026_05_26_100000_expand_admin_permission_granularity.php create mode 100644 database/migrations/2026_05_26_120000_add_unique_client_trace_to_ticket_orders.php create mode 100644 database/migrations/2026_05_27_100000_add_jackpot_manual_burst_permission.php create mode 100644 database/migrations/2026_05_27_140000_add_integration_fields_to_admin_sites.php create mode 100644 database/migrations/2026_05_27_140001_seed_integration_menu_actions.php create mode 100644 database/migrations/2026_05_28_100000_resync_admin_api_resources_after_dashboard_view.php create mode 100644 database/migrations/2026_05_29_100000_add_unique_ticket_item_id_to_jackpot_contributions.php create mode 100644 database/migrations/2026_05_30_100000_create_jackpot_pool_adjustments_table.php create mode 100644 database/migrations/2026_05_30_100001_add_jackpot_pool_adjustment_api_resources.php create mode 100644 database/migrations/2026_05_31_100000_add_query_performance_indexes.php create mode 100644 database/migrations/2026_06_01_100000_add_admin_settings_batch_update_api_resource.php create mode 100644 database/migrations/2026_06_02_100000_create_agent_hierarchy_tables.php create mode 100644 database/migrations/2026_06_02_100001_seed_agent_node_permissions.php create mode 100644 database/migrations/2026_06_02_110000_agent_scoped_roles_and_player_agent.php create mode 100644 database/migrations/2026_06_02_110001_seed_agent_role_permissions.php create mode 100644 database/migrations/2026_06_02_120000_create_agent_delegation_grants.php create mode 100644 database/migrations/2026_06_02_130000_backfill_players_agent_node_id.php create mode 100644 database/migrations/2026_06_03_120000_split_agent_permission_granularity.php create mode 100644 database/migrations/2026_06_03_140000_ensure_agent_admin_user_destroy_api_resource.php create mode 100644 database/migrations/2026_06_03_150000_align_root_agent_codes.php create mode 100644 database/migrations/2026_06_03_160000_agent_credit_and_settlement_tables.php create mode 100644 database/migrations/2026_06_03_170000_seed_agent_settlement_api_resources.php create mode 100644 database/migrations/2026_06_03_180000_add_agent_profile_capability_flags.php create mode 100644 database/migrations/2026_06_03_190000_fix_agent_primary_admin_user_status.php create mode 100644 database/migrations/2026_06_04_100000_agent_game_settlement_ledger.php create mode 100644 database/migrations/2026_06_04_120000_add_player_auth_and_funding_mode.php create mode 100644 database/migrations/2026_06_04_120000_agent_settlement_payment_proof.php create mode 100644 database/migrations/2026_06_04_120000_resync_agent_owner_role_permissions.php create mode 100644 database/migrations/2026_06_04_130000_seed_platform_agent_role_and_resync_bindings.php create mode 100644 database/migrations/2026_06_04_140000_agent_settlement_reports_and_tags.php create mode 100644 database/migrations/2026_06_04_140000_bind_agents_to_platform_agent_role.php create mode 100644 database/migrations/2026_06_04_150000_ensure_platform_fixed_system_roles.php create mode 100644 database/migrations/2026_06_05_120000_seed_credit_ledger_admin_api_resource.php create mode 100644 database/migrations/2026_06_10_120000_align_postgres_indexes_with_live_schema.php delete mode 100644 database/schema/pgsql-schema.sql create mode 100644 lottery diff --git a/README.md b/README.md index d181e94..0440306 100644 --- a/README.md +++ b/README.md @@ -11,28 +11,15 @@ 侧栏与 `prd.*` 权限目录见 [`docs/admin-rbac.md`](docs/admin-rbac.md)。维护命令:`php artisan lottery:admin-auth-sync --audit`。 -## 数据库基线 +## 数据库迁移 -项目当前已经整理出一份**最终版基线结构**,并已将历史迁移链清理为 schema dump 模式: +项目当前使用 **纯 migration 链** 维护 PostgreSQL 结构: -- PostgreSQL 基线文件:[`database/schema/pgsql-schema.sql`](database/schema/pgsql-schema.sql) -- 适用场景:**新环境初始化** -- 后续 migration 目录:`database/migrations/` -- 适用场景:**从当前基线之后继续新增结构变更** +- 新环境初始化:直接执行完整 migration +- 已有环境升级:继续通过新增 migration 演进 +- 结构来源:`database/migrations/` -推荐约定如下: - -- 新环境初始化时,优先使用 Laravel schema dump,让框架先加载 `database/schema/pgsql-schema.sql`,再执行该时间点之后的新迁移。 -- 已上线或已有数据的环境,如果已经接受 schema dump 作为唯一基线,可不再保留历史 migration 文件。 -- 之后的数据库结构演进,从当前 schema dump 往后继续追加新的 migration。 - -当数据库结构发生一轮阶段性稳定变更后,可重新生成基线: - -```bash -php artisan schema:dump --database=pgsql -``` - -如果只是日常开发中的普通字段变更,仍然按正常方式新增 migration 即可;等累积到一段时间后,再统一刷新一次 schema dump。 +不再依赖 `schema dump` 作为数据库基线,部署时也不需要先导入 SQL 基线文件。 ## 统一数据库初始化 @@ -44,7 +31,7 @@ php artisan lottery:db-init 这条命令会自动完成: -- 执行 `migrate`,让 Laravel 在空库时优先加载 `database/schema/pgsql-schema.sql` +- 执行 `migrate`,直接跑完整 migration 链 - 执行生产安全的基础种子 `FoundationSeeder` - 执行后台权限同步与体检 `lottery:admin-auth-sync --audit` - 在非 `production` 环境默认补充联调用演示数据 `LocalDemoSeeder` @@ -133,6 +120,12 @@ php artisan schedule:work > 仅用系统 cron 每分钟执行一次 `schedule:run` **无法覆盖「每秒」的 `lottery:hall-countdown`**,开发大厅实时倒计时时请用 `schedule:work`(或生产上等价常驻调度进程)。 +**队列消费者(推荐 `queue:work`,不要再用 `queue:listen`)** + +```bash +php artisan queue:work --tries=3 --timeout=120 --sleep=1 +``` + 只做 HTTP / 降级轮询、不测 WebSocket 时:**终端 2、3 可先不开**;要完整大厅 WS,则 **三项都开**。 ## 统一配置说明 @@ -146,6 +139,16 @@ php artisan schedule:work - `REVERB_HOST`:浏览器连接 Reverb 时看到的主机名或 IP - `SANCTUM_STATEFUL_DOMAINS`:允许带 Cookie 的前端来源列表 +## 生产性能基线 + +为避免调度锁、大厅快照缓存与业务表争抢同一数据库,生产环境请至少满足: + +- `CACHE_STORE=redis` +- `QUEUE_CONNECTION=redis` +- 常驻进程使用 `php artisan queue:work`,不要使用 `queue:listen` + +若继续使用 database cache,`schedule:work` 的 `withoutOverlapping()` / `onOneServer()` 锁、大厅 countdown 指纹缓存、以及大厅快照碎片缓存都会额外打数据库,容易放大高频调度的抖动。 + 如果你要用局域网地址访问,比如 `http://192.168.0.101:8000`,通常只需要: 1. 把 `APP_BIND_HOST`、`VITE_HOST` 和 `REVERB_SERVER_HOST` 改成 `0.0.0.0` diff --git a/app/Console/Commands/LotteryDatabaseInitCommand.php b/app/Console/Commands/LotteryDatabaseInitCommand.php index d8726fe..af51503 100644 --- a/app/Console/Commands/LotteryDatabaseInitCommand.php +++ b/app/Console/Commands/LotteryDatabaseInitCommand.php @@ -12,7 +12,7 @@ final class LotteryDatabaseInitCommand extends Command {--no-demo : 禁止写入演示数据} {--skip-auth-sync : 跳过后台权限注册表同步}'; - protected $description = '统一初始化数据库:迁移/基线、基础种子、后台权限同步,以及非生产演示数据'; + protected $description = '统一初始化数据库:纯迁移链、基础种子、后台权限同步,以及非生产演示数据'; public function handle(): int { diff --git a/app/Console/Commands/LotteryHallCountdownCommand.php b/app/Console/Commands/LotteryHallCountdownCommand.php index e686c27..1da5d1d 100644 --- a/app/Console/Commands/LotteryHallCountdownCommand.php +++ b/app/Console/Commands/LotteryHallCountdownCommand.php @@ -3,17 +3,27 @@ namespace App\Console\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Log; use App\Services\Draw\LotteryHallRealtimeBroadcaster; final class LotteryHallCountdownCommand extends Command { protected $signature = 'lottery:hall-countdown'; - protected $description = '大厅 countdown WebSocket:`draw.countdown`(每秒;见界面文档 §2.1)'; + protected $description = '大厅 countdown WebSocket:`draw.countdown`(按配置频率;见界面文档 §2.1)'; public function handle(LotteryHallRealtimeBroadcaster $broadcaster): int { + $startedAt = hrtime(true); $broadcaster->countdownPulse(); + $elapsedMs = (int) round((hrtime(true) - $startedAt) / 1_000_000); + + if ($elapsedMs >= (int) config('lottery.realtime_hall_countdown_warn_threshold_ms', 800)) { + Log::warning('lottery:hall-countdown exceeded warn threshold', [ + 'elapsed_ms' => $elapsedMs, + 'threshold_ms' => (int) config('lottery.realtime_hall_countdown_warn_threshold_ms', 800), + ]); + } return self::SUCCESS; } diff --git a/app/Services/Draw/DrawHallSnapshotBuilder.php b/app/Services/Draw/DrawHallSnapshotBuilder.php index 1399de7..722e5e3 100644 --- a/app/Services/Draw/DrawHallSnapshotBuilder.php +++ b/app/Services/Draw/DrawHallSnapshotBuilder.php @@ -9,6 +9,7 @@ use App\Lottery\DrawStatus; use App\Models\DrawResultItem; use App\Models\DrawResultBatch; use App\Lottery\DrawResultBatchStatus; +use Illuminate\Support\Facades\Cache; use App\Services\Jackpot\JackpotSummaryService; use App\Services\LotterySettings; @@ -19,6 +20,12 @@ use App\Services\LotterySettings; */ final class DrawHallSnapshotBuilder { + private const JACKPOT_CACHE_TTL_SECONDS = 5; + + private const RISK_ALERTS_CACHE_TTL_SECONDS = 2; + + private const RESULT_ITEMS_CACHE_TTL_SECONDS = 5; + public function __construct( private readonly JackpotSummaryService $jackpotSummary, ) {} @@ -108,6 +115,20 @@ final class DrawHallSnapshotBuilder DrawStatus::Settled->value, DrawStatus::Cancelled->value, ]) + ->where(function ($q) use ($nowUtc): void { + $q->where('status', '!=', DrawStatus::Pending->value) + ->orWhere(function ($q2) use ($nowUtc): void { + $q2->where('status', DrawStatus::Pending->value) + ->where(function ($q3) use ($nowUtc): void { + $q3->whereNull('close_time') + ->orWhere('close_time', '>', $nowUtc); + }) + ->where(function ($q3) use ($nowUtc): void { + $q3->whereNull('draw_time') + ->orWhere('draw_time', '>', $nowUtc); + }); + }); + }) ->where(function ($q) use ($nowUtc): void { $q->where(function ($q2) use ($nowUtc): void { $q2->whereNotNull('close_time') @@ -258,56 +279,19 @@ final class DrawHallSnapshotBuilder 'cooling_end_time' => $target->cooling_end_time?->toIso8601String(), 'seconds_remaining_in_cooldown' => $coolingRemain, 'jackpot_currency_code' => $currencyCode, - 'jackpot' => $this->jackpotSummary->summary($currencyCode), + 'jackpot' => $this->cachedJackpotSummary($currencyCode), ]; - $riskAlerts = RiskPool::query() - ->where('draw_id', $target->id) - ->where(function ($q): void { - $q->where('sold_out_status', 1) - ->orWhereRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) >= 0.8'); - }) - ->orderByRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) DESC') - ->orderByDesc('locked_amount') - ->orderBy('normalized_number') - ->limit(500) - ->get(['normalized_number', 'sold_out_status']) - ->map(fn ($row) => [ - 'normalized_number' => (string) $row->normalized_number, - 'status' => (int) $row->sold_out_status === 1 ? 'sold_out' : 'warning', - ]) - ->values() - ->all(); - - $payload['risk_pool_alerts'] = $riskAlerts; + $payload['risk_pool_alerts'] = $this->cachedRiskAlerts((int) $target->id); if ($this->showsPublishedResults((string) $target->status)) { - $batchId = DrawResultBatch::query() - ->where('draw_id', $target->id) - ->where('result_version', (int) $target->current_result_version) - ->where('status', DrawResultBatchStatus::Published->value) - ->value('id'); + $resultItems = $this->cachedPublishedResultItems( + (int) $target->id, + (int) $target->current_result_version, + ); - if ($batchId !== null) { - $payload['result_items'] = DrawResultItem::query() - ->where('result_batch_id', $batchId) - ->orderBy('prize_type') - ->orderBy('prize_index') - ->get([ - 'prize_type', 'prize_index', - 'number_4d', 'suffix_3d', 'suffix_2d', 'head_digit', 'tail_digit', - ]) - ->map(fn ($row) => [ - 'prize_type' => $row->prize_type, - 'prize_index' => (int) $row->prize_index, - 'number_4d' => $row->number_4d, - 'suffix_3d' => $row->suffix_3d, - 'suffix_2d' => $row->suffix_2d, - 'head_digit' => $row->head_digit, - 'tail_digit' => $row->tail_digit, - ]) - ->values() - ->all(); + if ($resultItems !== null) { + $payload['result_items'] = $resultItems; } $payload['result_version'] = (int) $target->current_result_version; @@ -327,4 +311,89 @@ final class DrawHallSnapshotBuilder return LotterySettings::defaultCurrency(); } + + /** + * @return array + */ + private function cachedJackpotSummary(string $currencyCode): array + { + $cacheKey = sprintf('hall_snapshot:jackpot:%s', strtoupper($currencyCode)); + + /** @var array */ + return Cache::remember($cacheKey, self::JACKPOT_CACHE_TTL_SECONDS, fn (): array => $this->jackpotSummary->summary($currencyCode)); + } + + /** + * @return list + */ + private function cachedRiskAlerts(int $drawId): array + { + $cacheKey = sprintf('hall_snapshot:risk_alerts:%d', $drawId); + + /** @var list */ + return Cache::remember($cacheKey, self::RISK_ALERTS_CACHE_TTL_SECONDS, function () use ($drawId): array { + return RiskPool::query() + ->where('draw_id', $drawId) + ->where(function ($q): void { + $q->where('sold_out_status', 1) + ->orWhereRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) >= 0.8'); + }) + ->orderByRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) DESC') + ->orderByDesc('locked_amount') + ->orderBy('normalized_number') + ->limit(500) + ->get(['normalized_number', 'sold_out_status']) + ->map(fn ($row) => [ + 'normalized_number' => (string) $row->normalized_number, + 'status' => (int) $row->sold_out_status === 1 ? 'sold_out' : 'warning', + ]) + ->values() + ->all(); + }); + } + + /** + * @return list>|null + */ + private function cachedPublishedResultItems(int $drawId, int $resultVersion): ?array + { + if ($resultVersion <= 0) { + return null; + } + + $cacheKey = sprintf('hall_snapshot:result_items:%d:%d', $drawId, $resultVersion); + + /** @var list>|null */ + return Cache::remember($cacheKey, self::RESULT_ITEMS_CACHE_TTL_SECONDS, function () use ($drawId, $resultVersion): ?array { + $batchId = DrawResultBatch::query() + ->where('draw_id', $drawId) + ->where('result_version', $resultVersion) + ->where('status', DrawResultBatchStatus::Published->value) + ->value('id'); + + if ($batchId === null) { + return null; + } + + return DrawResultItem::query() + ->where('result_batch_id', $batchId) + ->orderBy('prize_type') + ->orderBy('prize_index') + ->get([ + 'prize_type', 'prize_index', + 'number_4d', 'suffix_3d', 'suffix_2d', 'head_digit', 'tail_digit', + ]) + ->map(fn ($row) => [ + 'prize_type' => $row->prize_type, + 'prize_index' => (int) $row->prize_index, + 'number_4d' => $row->number_4d, + 'suffix_3d' => $row->suffix_3d, + 'suffix_2d' => $row->suffix_2d, + 'head_digit' => $row->head_digit, + 'tail_digit' => $row->tail_digit, + ]) + ->values() + ->all(); + }); + } } diff --git a/app/Services/Draw/DrawRngRunner.php b/app/Services/Draw/DrawRngRunner.php index 594f2a5..04174a7 100644 --- a/app/Services/Draw/DrawRngRunner.php +++ b/app/Services/Draw/DrawRngRunner.php @@ -93,6 +93,7 @@ final class DrawRngRunner ->orWhereDoesntHave('resultBatches'); }) ->orderBy('draw_time') + ->limit((int) config('lottery.draw_tick_rng_limit', 3)) ->pluck('id'); foreach ($ids as $drawId) { diff --git a/app/Services/Draw/DrawTickService.php b/app/Services/Draw/DrawTickService.php index a129d21..9d25624 100644 --- a/app/Services/Draw/DrawTickService.php +++ b/app/Services/Draw/DrawTickService.php @@ -5,6 +5,7 @@ namespace App\Services\Draw; use Carbon\Carbon; use App\Models\Draw; use App\Lottery\DrawStatus; +use Illuminate\Support\Facades\Log; use App\Services\LotterySettings; use App\Services\Settlement\SettlementOrchestrator; use App\Services\Settlement\SettlementTickFinalizer; @@ -38,21 +39,23 @@ final class DrawTickService public function tick(?Carbon $now = null): array { $nowUtc = ($now ?? Carbon::now())->utc(); + $startedAt = hrtime(true); + $stageTimings = []; - $hallFpBefore = $this->hallSnapshot->hallTargetFingerprint($nowUtc); + $hallFpBefore = $this->measureStage('hall_fp_before', $stageTimings, fn () => $this->hallSnapshot->hallTargetFingerprint($nowUtc)); - $statusUpdates = [ + $statusUpdates = $this->measureStage('status_updates', $stageTimings, fn (): array => [ 'pending_to_open_or_later' => $this->promoteStalePendingRows($nowUtc), 'open_to_closing_or_closed' => $this->openToClosingOrClosed($nowUtc), 'closing_to_closed' => $this->closingToClosed($nowUtc), 'cooldown_to_settling' => $this->cooldownToSettling($nowUtc), - ]; + ]); - $settlingSettled = $this->settleSettlingDraws(); - $settlementFinalized = $this->settlementFinalizer->finalizePendingBatches(); + $settlingSettled = $this->measureStage('settle_settling_draws', $stageTimings, fn (): int => $this->settleSettlingDraws()); + $settlementFinalized = $this->measureStage('finalize_pending_batches', $stageTimings, fn (): array => $this->settlementFinalizer->finalizePendingBatches()); - $rngOutcome = $this->rng->runDue($nowUtc); - $planned = $this->planner->ensureBuffer($nowUtc); + $rngOutcome = $this->measureStage('rng_run_due', $stageTimings, fn (): array => $this->rng->runDue($nowUtc)); + $planned = $this->measureStage('ensure_buffer', $stageTimings, fn (): array => $this->planner->ensureBuffer($nowUtc)); $report = [ 'status_updates' => $statusUpdates, @@ -63,10 +66,14 @@ final class DrawTickService 'planned' => $planned, ]; - $snapshotAfter = $this->hallSnapshot->build($nowUtc); - $hallFpAfter = $this->hallSnapshot->hallTargetFingerprint($nowUtc); + $snapshotAfter = $this->measureStage('hall_snapshot_after', $stageTimings, fn () => $this->hallSnapshot->build($nowUtc)); + $hallFpAfter = $this->measureStage('hall_fp_after', $stageTimings, fn () => $this->hallSnapshot->hallTargetFingerprint($nowUtc)); - $this->hallRealtime->notifyStatusChangeIfHallDbChanged($hallFpBefore, $hallFpAfter, $snapshotAfter); + $this->measureStage('notify_status_change', $stageTimings, function () use ($hallFpBefore, $hallFpAfter, $snapshotAfter): void { + $this->hallRealtime->notifyStatusChangeIfHallDbChanged($hallFpBefore, $hallFpAfter, $snapshotAfter); + }); + + $this->logIfSlow($startedAt, $stageTimings, $report); return $report; } @@ -156,7 +163,11 @@ final class DrawTickService } $n = 0; - $ids = Draw::query()->where('status', DrawStatus::Settling->value)->pluck('id'); + $ids = Draw::query() + ->where('status', DrawStatus::Settling->value) + ->orderBy('id') + ->limit((int) config('lottery.draw_tick_settle_limit', 3)) + ->pluck('id'); foreach ($ids as $drawId) { $draw = Draw::query()->find($drawId); if ($draw === null) { @@ -173,4 +184,49 @@ final class DrawTickService return $n; } + + /** + * @template T + * @param array $stageTimings + * @param \Closure(): T $callback + * @return T + */ + private function measureStage(string $stage, array &$stageTimings, \Closure $callback): mixed + { + $startedAt = hrtime(true); + $result = $callback(); + $stageTimings[$stage] = (int) round((hrtime(true) - $startedAt) / 1_000_000); + + return $result; + } + + /** + * @param array $stageTimings + * @param array $report + */ + private function logIfSlow(int $startedAt, array $stageTimings, array $report): void + { + $totalMs = (int) round((hrtime(true) - $startedAt) / 1_000_000); + $thresholdMs = (int) config('lottery.draw_tick_warn_threshold_ms', 1500); + $stageThresholdMs = (int) config('lottery.draw_tick_stage_warn_threshold_ms', 500); + $slowStages = array_filter($stageTimings, fn (int $elapsedMs): bool => $elapsedMs >= $stageThresholdMs); + + if ($totalMs < $thresholdMs && $slowStages === []) { + return; + } + + Log::warning('lottery:draw-tick exceeded warn threshold', [ + 'elapsed_ms' => $totalMs, + 'threshold_ms' => $thresholdMs, + 'stage_threshold_ms' => $stageThresholdMs, + 'slow_stages_ms' => $slowStages, + 'all_stages_ms' => $stageTimings, + 'status_update_rows' => array_sum($report['status_updates'] ?? []), + 'settling_settled' => $report['settling_settled'] ?? 0, + 'approved_batches' => $report['settlement_finalized']['approved'] ?? 0, + 'paid_batches' => $report['settlement_finalized']['paid'] ?? 0, + 'rng_rung' => $report['rng_rung'] ?? 0, + 'planned_created' => $report['planned']['created'] ?? 0, + ]); + } } diff --git a/app/Services/Draw/LotteryHallRealtimeBroadcaster.php b/app/Services/Draw/LotteryHallRealtimeBroadcaster.php index 12766a3..4c22578 100644 --- a/app/Services/Draw/LotteryHallRealtimeBroadcaster.php +++ b/app/Services/Draw/LotteryHallRealtimeBroadcaster.php @@ -2,6 +2,7 @@ namespace App\Services\Draw; +use Carbon\Carbon; use App\Events\OddsUpdateBroadcast; use App\Events\PlayCatalogUpdatedBroadcast; use App\Events\PlayToggleBroadcast; @@ -11,6 +12,7 @@ use App\Events\JackpotBurstBroadcast; use App\Events\DrawCountdownBroadcast; use App\Events\DrawStatusChangeBroadcast; use App\Events\DrawResultPublishedBroadcast; +use Illuminate\Support\Facades\Cache; /** * 对齐界面文档 §2.1:大厅公共频道广播(`lottery-hall`)。 @@ -19,20 +21,33 @@ use App\Events\DrawResultPublishedBroadcast; */ final class LotteryHallRealtimeBroadcaster { + private const COUNTDOWN_FP_CACHE_KEY = 'lottery:hall:countdown:last-fingerprint'; + public function __construct( private readonly DrawHallSnapshotBuilder $snapshot, ) {} - /** 每秒调度:`draw.countdown` 推送大厅快照(与 GET draw/current 一致),避免仅本地倒计时无法切期。 */ + /** 每秒调度:边界变化立刻推送,完整快照按低频校准,避免仅本地倒计时无法切期。 */ public function countdownPulse(): void { if (! $this->driverSupportsRealtime()) { return; } + $nowUtc = Carbon::now()->utc(); $ms = (int) floor(microtime(true) * 1000); + $fingerprint = $this->snapshot->hallTargetFingerprint($nowUtc); + $lastFingerprint = Cache::get(self::COUNTDOWN_FP_CACHE_KEY); + $stateChanged = $this->fingerprintChanged($lastFingerprint, $fingerprint); + $shouldSync = $this->shouldBroadcastSyncPulse($nowUtc); - broadcast(new DrawCountdownBroadcast($this->snapshot->build(), $ms)); + Cache::put(self::COUNTDOWN_FP_CACHE_KEY, $fingerprint, now()->addMinutes(10)); + + if (! $stateChanged && ! $shouldSync) { + return; + } + + broadcast(new DrawCountdownBroadcast($this->snapshot->build($nowUtc), $ms)); } /** @@ -197,4 +212,24 @@ final class LotteryHallRealtimeBroadcaster return ! in_array($driver, ['null', 'log'], true); } + + private function shouldBroadcastSyncPulse(Carbon $nowUtc): bool + { + $interval = match ((int) config('lottery.realtime_hall_countdown_sync_interval_seconds', 5)) { + 1, 2, 5, 10 => (int) config('lottery.realtime_hall_countdown_sync_interval_seconds', 5), + default => 5, + }; + + return $interval === 1 || ((int) $nowUtc->format('s')) % $interval === 0; + } + + private function fingerprintChanged(mixed $before, ?array $after): bool + { + if (! is_array($before)) { + return $after !== null; + } + + return ($before['draw_no'] ?? null) !== ($after['draw_no'] ?? null) + || ($before['status'] ?? null) !== ($after['status'] ?? null); + } } diff --git a/app/Services/Settlement/SettlementOrchestrator.php b/app/Services/Settlement/SettlementOrchestrator.php index ff5ed59..34c9cc2 100644 --- a/app/Services/Settlement/SettlementOrchestrator.php +++ b/app/Services/Settlement/SettlementOrchestrator.php @@ -41,16 +41,16 @@ final class SettlementOrchestrator */ public function trySettleDraw(Draw $draw): bool { - return (bool) DB::transaction(function () use ($draw): bool { + $afterCommit = DB::transaction(function () use ($draw): array { /** @var Draw $locked */ $locked = Draw::query()->whereKey($draw->id)->lockForUpdate()->firstOrFail(); if ($locked->status === DrawStatus::Settled->value) { - return false; + return ['handled' => false, 'jackpot_bursts' => [], 'should_notify_status' => false]; } if ($locked->status !== DrawStatus::Settling->value) { - return false; + return ['handled' => false, 'jackpot_bursts' => [], 'should_notify_status' => false]; } $publishedBatch = DrawResultBatch::query() @@ -61,7 +61,7 @@ final class SettlementOrchestrator ->first(); if ($publishedBatch === null) { - return false; + return ['handled' => false, 'jackpot_bursts' => [], 'should_notify_status' => false]; } $existingDone = SettlementBatch::query() @@ -81,7 +81,11 @@ final class SettlementOrchestrator 'settle_version' => (int) $existingDone->settle_version, ])->save(); - return true; + return [ + 'handled' => true, + 'jackpot_bursts' => [], + 'should_notify_status' => true, + ]; } $items = DrawResultItem::query() @@ -224,21 +228,39 @@ final class SettlementOrchestrator 'settle_version' => $nextSettleVersion, ])->save(); - foreach ($jackpotBursts as $burst) { - $this->hallRealtime->notifyJackpotBurst( - (int) $locked->id, - (string) $locked->draw_no, - $board->firstPrizeNumber4d(), - (string) $burst['currency'], - (int) $burst['payout'], - (int) $burst['winner_count'], - (string) $burst['trigger'], - (int) $burst['pool_after'], - ); - $this->hallRealtime->notifyStatusChange($this->hallSnapshot->build()); - } - - return true; + return [ + 'handled' => true, + 'jackpot_bursts' => array_map(fn (array $burst): array => [ + 'draw_id' => (int) $locked->id, + 'draw_no' => (string) $locked->draw_no, + 'first_prize_number' => $board->firstPrizeNumber4d(), + 'currency' => (string) $burst['currency'], + 'payout' => (int) $burst['payout'], + 'winner_count' => (int) $burst['winner_count'], + 'trigger' => (string) $burst['trigger'], + 'pool_after' => (int) $burst['pool_after'], + ], $jackpotBursts), + 'should_notify_status' => true, + ]; }); + + foreach ($afterCommit['jackpot_bursts'] as $burst) { + $this->hallRealtime->notifyJackpotBurst( + $burst['draw_id'], + $burst['draw_no'], + $burst['first_prize_number'], + $burst['currency'], + $burst['payout'], + $burst['winner_count'], + $burst['trigger'], + $burst['pool_after'], + ); + } + + if (($afterCommit['should_notify_status'] ?? false) === true) { + $this->hallRealtime->notifyStatusChange($this->hallSnapshot->build()); + } + + return (bool) ($afterCommit['handled'] ?? false); } } diff --git a/app/Services/Settlement/SettlementTickFinalizer.php b/app/Services/Settlement/SettlementTickFinalizer.php index 6ad5b3e..8057ac3 100644 --- a/app/Services/Settlement/SettlementTickFinalizer.php +++ b/app/Services/Settlement/SettlementTickFinalizer.php @@ -29,6 +29,7 @@ final class SettlementTickFinalizer $pending = SettlementBatch::query() ->where('status', SettlementBatchStatus::PendingReview->value) ->orderBy('id') + ->limit((int) config('lottery.draw_tick_finalize_limit', 5)) ->get(); foreach ($pending as $batch) { diff --git a/composer.json b/composer.json index 55ffb8f..a424e20 100644 --- a/composer.json +++ b/composer.json @@ -56,11 +56,11 @@ ], "dev": [ "Composer\\Config::disableProcessTimeout", - "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#f472b6\" \"php artisan serve --host=\\\"${APP_BIND_HOST:-127.0.0.1}\\\"\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others" + "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#f472b6\" \"php artisan serve --host=\\\"${APP_BIND_HOST:-127.0.0.1}\\\"\" \"php artisan queue:work --tries=3 --timeout=120 --sleep=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others" ], "dev:realtime": [ "Composer\\Config::disableProcessTimeout", - "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74,#34d399,#f472b6\" \"php artisan serve --host=\\\"${APP_BIND_HOST:-127.0.0.1}\\\"\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"php artisan reverb:start --host=\\\"${REVERB_SERVER_HOST:-0.0.0.0}\\\" --hostname=\\\"${REVERB_HOST:-localhost}\\\" --port=\\\"${REVERB_PORT:-8080}\\\"\" \"php artisan schedule:work\" \"npm run dev\" --names=server,queue,logs,reverb,schedule,vite --kill-others" + "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74,#34d399,#f472b6\" \"php artisan serve --host=\\\"${APP_BIND_HOST:-127.0.0.1}\\\"\" \"php artisan queue:work --tries=3 --timeout=120 --sleep=1\" \"php artisan pail --timeout=0\" \"php artisan reverb:start --host=\\\"${REVERB_SERVER_HOST:-0.0.0.0}\\\" --hostname=\\\"${REVERB_HOST:-localhost}\\\" --port=\\\"${REVERB_PORT:-8080}\\\"\" \"php artisan schedule:work\" \"npm run dev\" --names=server,queue,logs,reverb,schedule,vite --kill-others" ], "dev:schedule": [ "Composer\\Config::disableProcessTimeout", diff --git a/config/cache.php b/config/cache.php index 9eec385..0ef180e 100644 --- a/config/cache.php +++ b/config/cache.php @@ -14,7 +14,7 @@ return [ | */ - 'default' => env('CACHE_STORE', 'database'), + 'default' => env('CACHE_STORE', 'redis'), /* |-------------------------------------------------------------------------- diff --git a/config/lottery.php b/config/lottery.php index d881926..f23ab17 100644 --- a/config/lottery.php +++ b/config/lottery.php @@ -114,4 +114,18 @@ return [ 'cooldown_minutes' => max(0, (int) env('LOTTERY_DRAW_COOLDOWN_MINUTES', 15)), ], + /* + | 大厅实时倒计时广播:调度维持每秒,保障封盘/开奖/切期边界精度; + | 完整快照默认按较低频率校准,边界变化时立即补发。 + | 可选校准秒数仅支持 Laravel 调度原生子分钟频率:1 / 2 / 5 / 10。 + */ + 'realtime_hall_countdown' => filter_var(env('LOTTERY_REALTIME_HALL_COUNTDOWN', true), FILTER_VALIDATE_BOOLEAN), + 'realtime_hall_countdown_sync_interval_seconds' => max(1, (int) env('LOTTERY_REALTIME_HALL_COUNTDOWN_SYNC_INTERVAL_SECONDS', 5)), + 'realtime_hall_countdown_warn_threshold_ms' => max(100, (int) env('LOTTERY_REALTIME_HALL_COUNTDOWN_WARN_THRESHOLD_MS', 800)), + 'draw_tick_warn_threshold_ms' => max(100, (int) env('LOTTERY_DRAW_TICK_WARN_THRESHOLD_MS', 1500)), + 'draw_tick_stage_warn_threshold_ms' => max(50, (int) env('LOTTERY_DRAW_TICK_STAGE_WARN_THRESHOLD_MS', 500)), + 'draw_tick_settle_limit' => max(1, (int) env('LOTTERY_DRAW_TICK_SETTLE_LIMIT', 3)), + 'draw_tick_finalize_limit' => max(1, (int) env('LOTTERY_DRAW_TICK_FINALIZE_LIMIT', 5)), + 'draw_tick_rng_limit' => max(1, (int) env('LOTTERY_DRAW_TICK_RNG_LIMIT', 3)), + ]; diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..2f4866a --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..cc70803 --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->bigInteger('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->bigInteger('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..70d2afd --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedSmallInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/migrations/2026_05_08_100000_create_currencies_table.php b/database/migrations/2026_05_08_100000_create_currencies_table.php new file mode 100644 index 0000000..1948a82 --- /dev/null +++ b/database/migrations/2026_05_08_100000_create_currencies_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('code', 16)->unique(); + $table->string('name', 64); + $table->unsignedTinyInteger('decimal_places')->default(2); + $table->boolean('is_enabled')->default(true); + $table->boolean('is_bettable')->default(false); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('currencies'); + } +}; diff --git a/database/migrations/2026_05_08_100001_create_players_table.php b/database/migrations/2026_05_08_100001_create_players_table.php new file mode 100644 index 0000000..d3d278a --- /dev/null +++ b/database/migrations/2026_05_08_100001_create_players_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('site_code', 64); + $table->string('site_player_id', 128); + $table->string('username', 128)->nullable(); + $table->string('nickname', 128)->nullable(); + $table->string('default_currency', 16)->default('NPR'); + $table->unsignedTinyInteger('status')->default(0)->comment('0=active,1=frozen,2=blocked'); + $table->timestamp('last_login_at')->nullable(); + $table->timestamps(); + + $table->unique(['site_code', 'site_player_id'], 'uk_players_site_player'); + $table->index('status', 'idx_players_status'); + }); + } + + public function down(): void + { + Schema::dropIfExists('players'); + } +}; diff --git a/database/migrations/2026_05_08_100002_create_admin_users_table.php b/database/migrations/2026_05_08_100002_create_admin_users_table.php new file mode 100644 index 0000000..b18a0b0 --- /dev/null +++ b/database/migrations/2026_05_08_100002_create_admin_users_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('name', 128); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->unsignedTinyInteger('status')->default(0)->comment('0=active,1=disabled'); + $table->timestamp('last_login_at')->nullable(); + $table->rememberToken(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('admin_users'); + } +}; diff --git a/database/migrations/2026_05_08_100003_create_admin_roles_and_permissions_tables.php b/database/migrations/2026_05_08_100003_create_admin_roles_and_permissions_tables.php new file mode 100644 index 0000000..093fccd --- /dev/null +++ b/database/migrations/2026_05_08_100003_create_admin_roles_and_permissions_tables.php @@ -0,0 +1,45 @@ +id(); + $table->string('slug', 64)->unique(); + $table->string('name', 128); + $table->timestamps(); + }); + + Schema::create('admin_permissions', function (Blueprint $table) { + $table->id(); + $table->string('slug', 128)->unique(); + $table->string('name', 128); + $table->timestamps(); + }); + + Schema::create('admin_role_permissions', function (Blueprint $table) { + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->foreignId('permission_id')->constrained('admin_permissions')->cascadeOnDelete(); + $table->primary(['role_id', 'permission_id']); + }); + + Schema::create('admin_user_roles', function (Blueprint $table) { + $table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete(); + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->primary(['admin_user_id', 'role_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('admin_user_roles'); + Schema::dropIfExists('admin_role_permissions'); + Schema::dropIfExists('admin_permissions'); + Schema::dropIfExists('admin_roles'); + } +}; diff --git a/database/migrations/2026_05_08_100004_create_player_wallets_table.php b/database/migrations/2026_05_08_100004_create_player_wallets_table.php new file mode 100644 index 0000000..b6dcbc4 --- /dev/null +++ b/database/migrations/2026_05_08_100004_create_player_wallets_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); + $table->string('wallet_type', 32)->default('lottery'); + $table->string('currency_code', 16); + $table->bigInteger('balance')->default(0); + $table->bigInteger('frozen_balance')->default(0); + $table->unsignedTinyInteger('status')->default(0)->comment('0=active,1=frozen'); + $table->unsignedBigInteger('version')->default(0); + $table->timestamps(); + + $table->unique(['player_id', 'wallet_type', 'currency_code'], 'uk_player_wallets_player_type_currency'); + }); + } + + public function down(): void + { + Schema::dropIfExists('player_wallets'); + } +}; diff --git a/database/migrations/2026_05_08_100005_create_wallet_txns_table.php b/database/migrations/2026_05_08_100005_create_wallet_txns_table.php new file mode 100644 index 0000000..3808176 --- /dev/null +++ b/database/migrations/2026_05_08_100005_create_wallet_txns_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('txn_no', 64)->unique(); + $table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); + $table->foreignId('wallet_id')->constrained('player_wallets')->cascadeOnDelete(); + $table->string('biz_type', 32); + $table->string('biz_no', 64)->nullable(); + $table->unsignedTinyInteger('direction')->comment('1=in,2=out'); + $table->bigInteger('amount'); + $table->bigInteger('balance_before'); + $table->bigInteger('balance_after'); + $table->string('status', 32); + $table->string('external_ref_no', 64)->nullable(); + $table->string('idempotent_key', 64)->nullable(); + $table->string('remark', 255)->nullable(); + $table->timestamps(); + + $table->index(['player_id', 'created_at'], 'idx_wallet_txns_player_time'); + $table->index(['biz_type', 'biz_no'], 'idx_wallet_txns_biz'); + $table->unique(['idempotent_key', 'biz_type'], 'uk_wallet_txns_idempotent_biz'); + }); + } + + public function down(): void + { + Schema::dropIfExists('wallet_txns'); + } +}; diff --git a/database/migrations/2026_05_08_100006_create_transfer_orders_table.php b/database/migrations/2026_05_08_100006_create_transfer_orders_table.php new file mode 100644 index 0000000..e2fd40e --- /dev/null +++ b/database/migrations/2026_05_08_100006_create_transfer_orders_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('transfer_no', 64)->unique(); + $table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); + $table->string('direction', 16); + $table->string('currency_code', 16); + $table->bigInteger('amount'); + $table->string('idempotent_key', 64)->unique(); + $table->string('status', 32); + $table->json('external_request_payload')->nullable(); + $table->json('external_response_payload')->nullable(); + $table->string('external_ref_no', 64)->nullable(); + $table->string('fail_reason', 255)->nullable(); + $table->timestamp('finished_at')->nullable(); + $table->timestamps(); + + $table->index(['player_id', 'created_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('transfer_orders'); + } +}; diff --git a/database/migrations/2026_05_08_100007_create_draws_table.php b/database/migrations/2026_05_08_100007_create_draws_table.php new file mode 100644 index 0000000..ba59cf6 --- /dev/null +++ b/database/migrations/2026_05_08_100007_create_draws_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('draw_no', 32)->unique(); + $table->date('business_date'); + $table->unsignedInteger('sequence_no'); + $table->string('status', 32); + $table->timestamp('start_time')->nullable(); + $table->timestamp('close_time')->nullable(); + $table->timestamp('draw_time')->nullable(); + $table->timestamp('cooling_end_time')->nullable(); + $table->string('result_source', 16)->nullable()->comment('rng|manual'); + $table->unsignedInteger('current_result_version')->default(0); + $table->unsignedInteger('settle_version')->default(0); + $table->boolean('is_reopened')->default(false); + $table->timestamps(); + + $table->index(['status', 'draw_time'], 'idx_draws_status_draw_time'); + }); + } + + public function down(): void + { + Schema::dropIfExists('draws'); + } +}; diff --git a/database/migrations/2026_05_08_100008_create_draw_result_batches_table.php b/database/migrations/2026_05_08_100008_create_draw_result_batches_table.php new file mode 100644 index 0000000..681e064 --- /dev/null +++ b/database/migrations/2026_05_08_100008_create_draw_result_batches_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('draw_id')->constrained('draws')->cascadeOnDelete(); + $table->unsignedInteger('result_version'); + $table->string('source_type', 16)->comment('rng|manual'); + $table->string('rng_seed_hash', 128)->nullable(); + $table->text('raw_seed_encrypted')->nullable(); + $table->string('status', 32); + $table->foreignId('created_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->foreignId('confirmed_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->timestamp('confirmed_at')->nullable(); + $table->timestamps(); + + $table->unique(['draw_id', 'result_version'], 'uk_draw_result_batches_draw_version'); + }); + } + + public function down(): void + { + Schema::dropIfExists('draw_result_batches'); + } +}; diff --git a/database/migrations/2026_05_08_100009_create_draw_result_items_table.php b/database/migrations/2026_05_08_100009_create_draw_result_items_table.php new file mode 100644 index 0000000..6902f3d --- /dev/null +++ b/database/migrations/2026_05_08_100009_create_draw_result_items_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('draw_id')->constrained('draws')->cascadeOnDelete(); + $table->foreignId('result_batch_id')->constrained('draw_result_batches')->cascadeOnDelete(); + $table->string('prize_type', 32); + $table->unsignedInteger('prize_index')->default(0); + $table->char('number_4d', 4); + $table->char('suffix_3d', 3)->nullable(); + $table->char('suffix_2d', 2)->nullable(); + $table->unsignedTinyInteger('head_digit')->nullable(); + $table->unsignedTinyInteger('tail_digit')->nullable(); + $table->timestamp('created_at')->useCurrent(); + + $table->index(['draw_id', 'prize_type', 'prize_index'], 'idx_draw_result_items_draw_prize'); + $table->index(['draw_id', 'number_4d'], 'idx_draw_result_items_draw_number'); + }); + } + + public function down(): void + { + Schema::dropIfExists('draw_result_items'); + } +}; diff --git a/database/migrations/2026_05_08_120000_drop_laravel_default_users_and_password_reset_tables.php b/database/migrations/2026_05_08_120000_drop_laravel_default_users_and_password_reset_tables.php new file mode 100644 index 0000000..ae3391a --- /dev/null +++ b/database/migrations/2026_05_08_120000_drop_laravel_default_users_and_password_reset_tables.php @@ -0,0 +1,37 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } +}; diff --git a/database/migrations/2026_05_08_130000_create_play_types_table.php b/database/migrations/2026_05_08_130000_create_play_types_table.php new file mode 100644 index 0000000..7b43e92 --- /dev/null +++ b/database/migrations/2026_05_08_130000_create_play_types_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('play_code', 32)->unique(); + $table->string('category', 16); + $table->unsignedTinyInteger('dimension')->nullable()->comment('2/3/4'); + $table->string('bet_mode', 32)->nullable(); + $table->string('display_name_zh', 64)->nullable(); + $table->string('display_name_en', 64)->nullable(); + $table->string('display_name_ne', 64)->nullable(); + $table->boolean('is_enabled')->default(true); + $table->integer('sort_order')->default(0); + $table->boolean('supports_multi_number')->default(false); + $table->json('reserved_rule_json')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('play_types'); + } +}; diff --git a/database/migrations/2026_05_08_130001_create_play_config_versions_and_items_tables.php b/database/migrations/2026_05_08_130001_create_play_config_versions_and_items_tables.php new file mode 100644 index 0000000..fd0a5b0 --- /dev/null +++ b/database/migrations/2026_05_08_130001_create_play_config_versions_and_items_tables.php @@ -0,0 +1,44 @@ +id(); + $table->unsignedInteger('version_no'); + $table->string('status', 16); + $table->timestamp('effective_at')->nullable(); + $table->foreignId('updated_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->string('reason', 255)->nullable(); + $table->timestamps(); + }); + + Schema::create('play_config_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('version_id')->constrained('play_config_versions')->cascadeOnDelete(); + $table->string('play_code', 32); + $table->boolean('is_enabled')->default(true); + $table->bigInteger('min_bet_amount')->default(0); + $table->bigInteger('max_bet_amount')->default(0); + $table->integer('display_order')->default(0); + $table->text('rule_text_zh')->nullable(); + $table->text('rule_text_en')->nullable(); + $table->text('rule_text_ne')->nullable(); + $table->json('extra_config_json')->nullable(); + $table->timestamps(); + + $table->unique(['version_id', 'play_code'], 'uk_play_config_items_version_play'); + }); + } + + public function down(): void + { + Schema::dropIfExists('play_config_items'); + Schema::dropIfExists('play_config_versions'); + } +}; diff --git a/database/migrations/2026_05_08_130002_create_odds_versions_and_items_tables.php b/database/migrations/2026_05_08_130002_create_odds_versions_and_items_tables.php new file mode 100644 index 0000000..1a90134 --- /dev/null +++ b/database/migrations/2026_05_08_130002_create_odds_versions_and_items_tables.php @@ -0,0 +1,46 @@ +id(); + $table->unsignedInteger('version_no'); + $table->string('status', 16); + $table->timestamp('effective_at')->nullable(); + $table->foreignId('updated_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->string('reason', 255)->nullable(); + $table->timestamps(); + }); + + Schema::create('odds_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('version_id')->constrained('odds_versions')->cascadeOnDelete(); + $table->string('play_code', 32); + $table->string('prize_scope', 32); + $table->bigInteger('odds_value')->default(0); + $table->decimal('rebate_rate', 8, 4)->default(0); + $table->decimal('commission_rate', 8, 4)->default(0); + $table->string('currency_code', 16); + $table->json('extra_config_json')->nullable(); + $table->timestamps(); + + $table->index(['version_id', 'play_code'], 'idx_odds_items_version_play'); + $table->unique( + ['version_id', 'play_code', 'prize_scope', 'currency_code'], + 'uk_odds_items_version_play_prize_currency' + ); + }); + } + + public function down(): void + { + Schema::dropIfExists('odds_items'); + Schema::dropIfExists('odds_versions'); + } +}; diff --git a/database/migrations/2026_05_08_130003_create_risk_cap_versions_and_items_tables.php b/database/migrations/2026_05_08_130003_create_risk_cap_versions_and_items_tables.php new file mode 100644 index 0000000..1f771a5 --- /dev/null +++ b/database/migrations/2026_05_08_130003_create_risk_cap_versions_and_items_tables.php @@ -0,0 +1,39 @@ +id(); + $table->unsignedInteger('version_no'); + $table->string('status', 16); + $table->timestamp('effective_at')->nullable(); + $table->foreignId('updated_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->string('reason', 255)->nullable(); + $table->timestamps(); + }); + + Schema::create('risk_cap_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('version_id')->constrained('risk_cap_versions')->cascadeOnDelete(); + $table->foreignId('draw_id')->nullable()->constrained('draws')->nullOnDelete(); + $table->char('normalized_number', 4); + $table->bigInteger('cap_amount'); + $table->string('cap_type', 16); + $table->timestamps(); + + $table->index(['version_id', 'draw_id', 'normalized_number'], 'idx_risk_cap_items_lookup'); + }); + } + + public function down(): void + { + Schema::dropIfExists('risk_cap_items'); + Schema::dropIfExists('risk_cap_versions'); + } +}; diff --git a/database/migrations/2026_05_08_130004_create_ticket_orders_table.php b/database/migrations/2026_05_08_130004_create_ticket_orders_table.php new file mode 100644 index 0000000..2cf10ad --- /dev/null +++ b/database/migrations/2026_05_08_130004_create_ticket_orders_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('order_no', 64)->unique(); + $table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); + $table->foreignId('draw_id')->constrained('draws')->cascadeOnDelete(); + $table->string('currency_code', 16); + $table->bigInteger('total_bet_amount')->default(0); + $table->bigInteger('total_rebate_amount')->default(0); + $table->bigInteger('total_actual_deduct')->default(0); + $table->bigInteger('total_estimated_payout')->default(0); + $table->string('status', 32); + $table->string('submit_source', 16)->default('h5'); + $table->string('client_trace_id', 64)->nullable(); + $table->timestamps(); + + $table->index(['player_id', 'draw_id'], 'idx_ticket_orders_player_draw'); + }); + } + + public function down(): void + { + Schema::dropIfExists('ticket_orders'); + } +}; diff --git a/database/migrations/2026_05_08_130005_create_ticket_items_table.php b/database/migrations/2026_05_08_130005_create_ticket_items_table.php new file mode 100644 index 0000000..e7009c2 --- /dev/null +++ b/database/migrations/2026_05_08_130005_create_ticket_items_table.php @@ -0,0 +1,51 @@ +id(); + $table->string('ticket_no', 64)->unique(); + $table->foreignId('order_id')->constrained('ticket_orders')->cascadeOnDelete(); + $table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); + $table->foreignId('draw_id')->constrained('draws')->cascadeOnDelete(); + $table->string('original_number', 32)->nullable(); + $table->char('normalized_number', 4); + $table->string('play_code', 32); + $table->unsignedTinyInteger('dimension')->nullable()->comment('2/3/4'); + $table->unsignedTinyInteger('digit_slot')->nullable()->comment('千百十个位,领域字典'); + $table->string('bet_mode', 32)->nullable(); + $table->bigInteger('unit_bet_amount')->default(0); + $table->bigInteger('total_bet_amount')->default(0); + $table->decimal('rebate_rate_snapshot', 8, 4)->default(0); + $table->decimal('commission_rate_snapshot', 8, 4)->default(0); + $table->bigInteger('actual_deduct_amount')->default(0); + $table->json('odds_snapshot_json')->nullable(); + $table->json('rule_snapshot_json')->nullable(); + $table->unsignedInteger('combination_count')->default(1); + $table->bigInteger('estimated_max_payout')->default(0); + $table->bigInteger('risk_locked_amount')->default(0); + $table->string('status', 32); + $table->string('fail_reason_code', 32)->nullable(); + $table->string('fail_reason_text', 255)->nullable(); + $table->bigInteger('win_amount')->default(0); + $table->bigInteger('jackpot_win_amount')->default(0); + $table->timestamp('settled_at')->nullable(); + $table->timestamps(); + + $table->index(['player_id', 'draw_id'], 'idx_ticket_items_player_draw'); + $table->index(['draw_id', 'status'], 'idx_ticket_items_draw_status'); + $table->index(['draw_id', 'normalized_number'], 'idx_ticket_items_draw_number'); + }); + } + + public function down(): void + { + Schema::dropIfExists('ticket_items'); + } +}; diff --git a/database/migrations/2026_05_08_130006_create_ticket_combinations_table.php b/database/migrations/2026_05_08_130006_create_ticket_combinations_table.php new file mode 100644 index 0000000..41a7d38 --- /dev/null +++ b/database/migrations/2026_05_08_130006_create_ticket_combinations_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('ticket_item_id')->constrained('ticket_items')->cascadeOnDelete(); + $table->unsignedInteger('combination_no')->default(0); + $table->char('number_4d', 4); + $table->bigInteger('bet_amount')->default(0); + $table->bigInteger('estimated_payout')->default(0); + $table->timestamp('created_at')->useCurrent(); + + $table->index('ticket_item_id', 'idx_ticket_combinations_item'); + $table->index('number_4d', 'idx_ticket_combinations_number'); + }); + } + + public function down(): void + { + Schema::dropIfExists('ticket_combinations'); + } +}; diff --git a/database/migrations/2026_05_08_130007_create_risk_pools_and_lock_logs_tables.php b/database/migrations/2026_05_08_130007_create_risk_pools_and_lock_logs_tables.php new file mode 100644 index 0000000..e39dd0e --- /dev/null +++ b/database/migrations/2026_05_08_130007_create_risk_pools_and_lock_logs_tables.php @@ -0,0 +1,45 @@ +id(); + $table->foreignId('draw_id')->constrained('draws')->cascadeOnDelete(); + $table->char('normalized_number', 4); + $table->bigInteger('total_cap_amount')->default(0); + $table->bigInteger('locked_amount')->default(0); + $table->bigInteger('remaining_amount')->default(0); + $table->unsignedTinyInteger('sold_out_status')->default(0); + $table->unsignedBigInteger('version')->default(0); + $table->timestamps(); + + $table->unique(['draw_id', 'normalized_number'], 'uk_risk_pools_draw_number'); + $table->index(['draw_id', 'sold_out_status'], 'idx_risk_pools_draw_soldout'); + }); + + Schema::create('risk_pool_lock_logs', function (Blueprint $table) { + $table->id(); + $table->foreignId('draw_id')->constrained('draws')->cascadeOnDelete(); + $table->char('normalized_number', 4); + $table->foreignId('ticket_item_id')->nullable()->constrained('ticket_items')->nullOnDelete(); + $table->string('action_type', 16); + $table->bigInteger('amount')->default(0); + $table->string('source_reason', 32)->nullable(); + $table->timestamp('created_at')->useCurrent(); + + $table->index(['draw_id', 'normalized_number'], 'idx_risk_lock_logs_draw_number'); + }); + } + + public function down(): void + { + Schema::dropIfExists('risk_pool_lock_logs'); + Schema::dropIfExists('risk_pools'); + } +}; diff --git a/database/migrations/2026_05_08_130008_create_settlement_and_jackpot_tables.php b/database/migrations/2026_05_08_130008_create_settlement_and_jackpot_tables.php new file mode 100644 index 0000000..240c745 --- /dev/null +++ b/database/migrations/2026_05_08_130008_create_settlement_and_jackpot_tables.php @@ -0,0 +1,93 @@ +id(); + $table->foreignId('draw_id')->constrained('draws')->cascadeOnDelete(); + $table->foreignId('result_batch_id')->constrained('draw_result_batches')->cascadeOnDelete(); + $table->unsignedInteger('settle_version')->default(1); + $table->string('status', 32); + $table->unsignedInteger('total_ticket_count')->default(0); + $table->unsignedInteger('total_win_count')->default(0); + $table->bigInteger('total_payout_amount')->default(0); + $table->bigInteger('total_jackpot_payout_amount')->default(0); + $table->string('review_status', 32)->default('pending'); + $table->foreignId('reviewed_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->timestamp('reviewed_at')->nullable(); + $table->string('review_remark', 255)->nullable(); + $table->timestamp('paid_at')->nullable(); + $table->timestamp('started_at')->nullable(); + $table->timestamp('finished_at')->nullable(); + $table->timestamps(); + + $table->index(['draw_id', 'settle_version'], 'idx_settlement_batches_draw_version'); + }); + + Schema::create('ticket_settlement_details', function (Blueprint $table) { + $table->id(); + $table->foreignId('settlement_batch_id')->constrained('settlement_batches')->cascadeOnDelete(); + $table->foreignId('ticket_item_id')->constrained('ticket_items')->cascadeOnDelete(); + $table->string('matched_prize_tier', 32)->nullable(); + $table->bigInteger('win_amount')->default(0); + $table->bigInteger('jackpot_allocation_amount')->default(0); + $table->json('match_detail_json')->nullable(); + $table->timestamps(); + + $table->unique(['settlement_batch_id', 'ticket_item_id'], 'uk_ticket_settlement_batch_ticket'); + }); + + Schema::create('jackpot_pools', function (Blueprint $table) { + $table->id(); + $table->string('currency_code', 16)->unique(); + $table->bigInteger('current_amount')->default(0); + $table->decimal('contribution_rate', 8, 4)->default(0); + $table->bigInteger('trigger_threshold')->default(0); + $table->decimal('payout_rate', 8, 4)->default(0); + $table->unsignedInteger('force_trigger_draw_gap')->default(0); + $table->bigInteger('min_bet_amount')->default(0); + $table->unsignedTinyInteger('status')->default(0)->comment('0=off,1=on'); + $table->foreignId('last_trigger_draw_id')->nullable()->constrained('draws')->nullOnDelete(); + $table->timestamps(); + }); + + Schema::create('jackpot_contributions', function (Blueprint $table) { + $table->id(); + $table->foreignId('jackpot_pool_id')->constrained('jackpot_pools')->cascadeOnDelete(); + $table->foreignId('draw_id')->constrained('draws')->cascadeOnDelete(); + $table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); + $table->foreignId('ticket_item_id')->nullable()->constrained('ticket_items')->nullOnDelete(); + $table->bigInteger('contribution_amount')->default(0); + $table->string('currency_code', 16); + $table->timestamps(); + + $table->index(['draw_id', 'player_id'], 'idx_jackpot_contrib_draw_player'); + }); + + Schema::create('jackpot_payout_logs', function (Blueprint $table) { + $table->id(); + $table->foreignId('draw_id')->constrained('draws')->cascadeOnDelete(); + $table->foreignId('jackpot_pool_id')->constrained('jackpot_pools')->cascadeOnDelete(); + $table->string('trigger_type', 32); + $table->bigInteger('total_payout_amount')->default(0); + $table->unsignedInteger('winner_count')->default(0); + $table->json('trigger_snapshot_json')->nullable(); + $table->timestamp('created_at')->useCurrent(); + }); + } + + public function down(): void + { + Schema::dropIfExists('jackpot_payout_logs'); + Schema::dropIfExists('jackpot_contributions'); + Schema::dropIfExists('jackpot_pools'); + Schema::dropIfExists('ticket_settlement_details'); + Schema::dropIfExists('settlement_batches'); + } +}; diff --git a/database/migrations/2026_05_08_130009_create_report_audit_reconcile_tables.php b/database/migrations/2026_05_08_130009_create_report_audit_reconcile_tables.php new file mode 100644 index 0000000..60ea9a3 --- /dev/null +++ b/database/migrations/2026_05_08_130009_create_report_audit_reconcile_tables.php @@ -0,0 +1,87 @@ +id(); + $table->string('job_no', 64)->unique(); + $table->foreignId('admin_user_id')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->string('report_type', 64); + $table->string('export_format', 16)->default('csv'); + $table->json('filter_json')->nullable(); + $table->string('status', 32); + $table->string('output_path', 512)->nullable(); + $table->text('error_message')->nullable(); + $table->timestamp('finished_at')->nullable(); + $table->timestamps(); + }); + + Schema::create('audit_logs', function (Blueprint $table) { + $table->id(); + $table->string('operator_type', 16); + $table->unsignedBigInteger('operator_id')->default(0); + $table->string('module_code', 32)->nullable(); + $table->string('action_code', 32)->nullable(); + $table->string('target_type', 32)->nullable(); + $table->string('target_id', 64)->nullable(); + $table->json('before_json')->nullable(); + $table->json('after_json')->nullable(); + $table->string('ip', 64)->nullable(); + $table->string('user_agent', 255)->nullable(); + $table->timestamp('created_at')->useCurrent(); + + $table->index(['operator_type', 'operator_id', 'created_at'], 'idx_audit_logs_operator_time'); + $table->index(['module_code', 'action_code'], 'idx_audit_logs_module_action'); + }); + + Schema::create('system_jobs', function (Blueprint $table) { + $table->id(); + $table->string('job_key', 128)->unique(); + $table->string('name', 128); + $table->string('schedule_cron', 64)->nullable(); + $table->boolean('is_enabled')->default(true); + $table->timestamp('last_started_at')->nullable(); + $table->timestamp('last_finished_at')->nullable(); + $table->string('last_status', 32)->nullable(); + $table->timestamps(); + }); + + Schema::create('reconcile_jobs', function (Blueprint $table) { + $table->id(); + $table->string('job_no', 64)->unique(); + $table->string('reconcile_type', 32); + $table->string('status', 32); + $table->timestamp('period_start')->nullable(); + $table->timestamp('period_end')->nullable(); + $table->json('summary_json')->nullable(); + $table->timestamp('finished_at')->nullable(); + $table->timestamps(); + }); + + Schema::create('reconcile_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('reconcile_job_id')->constrained('reconcile_jobs')->cascadeOnDelete(); + $table->string('side_a_ref', 128)->nullable(); + $table->string('side_b_ref', 128)->nullable(); + $table->bigInteger('difference_amount')->default(0); + $table->string('status', 32); + $table->timestamp('resolved_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('reconcile_items'); + Schema::dropIfExists('reconcile_jobs'); + Schema::dropIfExists('system_jobs'); + Schema::dropIfExists('audit_logs'); + Schema::dropIfExists('report_jobs'); + } +}; diff --git a/database/migrations/2026_05_08_140000_create_lottery_settings_table.php b/database/migrations/2026_05_08_140000_create_lottery_settings_table.php new file mode 100644 index 0000000..361e68d --- /dev/null +++ b/database/migrations/2026_05_08_140000_create_lottery_settings_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('setting_key', 160)->unique(); + $table->json('value_json'); + $table->string('group_name', 64)->default('general')->comment('控制台分组展示用'); + $table->string('description_zh')->nullable()->comment('运维说明'); + $table->timestamps(); + + $table->index('group_name', 'idx_lottery_settings_group'); + }); + } + + public function down(): void + { + Schema::dropIfExists('lottery_settings'); + } +}; diff --git a/database/migrations/2026_05_09_023835_create_personal_access_tokens_table.php b/database/migrations/2026_05_09_023835_create_personal_access_tokens_table.php new file mode 100644 index 0000000..628697e --- /dev/null +++ b/database/migrations/2026_05_09_023835_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/database/migrations/2026_05_09_119999_rename_duplicate_migration_filenames_in_table.php b/database/migrations/2026_05_09_119999_rename_duplicate_migration_filenames_in_table.php new file mode 100644 index 0000000..b038b31 --- /dev/null +++ b/database/migrations/2026_05_09_119999_rename_duplicate_migration_filenames_in_table.php @@ -0,0 +1,40 @@ + */ + private const RENAMES = [ + '2026_05_09_120000_add_username_and_nullable_email_to_admin_users' => '2026_05_09_120001_add_username_and_nullable_email_to_admin_users', + '2026_05_09_120000_migrate_draw_status_to_domain_dict' => '2026_05_09_120002_migrate_draw_status_to_domain_dict', + '2026_05_25_120000_consolidate_play_display_name_columns' => '2026_05_25_120001_consolidate_play_display_name_columns', + '2026_05_25_120000_expand_audit_logs_target_type' => '2026_05_25_120002_expand_audit_logs_target_type', + '2026_05_25_120000_refine_admin_permission_granularity' => '2026_05_25_120003_refine_admin_permission_granularity', + ]; + + public function up(): void + { + foreach (self::RENAMES as $from => $to) { + DB::table('migrations')->where('migration', $from)->update(['migration' => $to]); + } + } + + public function down(): void + { + foreach (self::RENAMES as $from => $to) { + DB::table('migrations')->where('migration', $to)->update(['migration' => $from]); + } + } +}; diff --git a/database/migrations/2026_05_09_120001_add_username_and_nullable_email_to_admin_users.php b/database/migrations/2026_05_09_120001_add_username_and_nullable_email_to_admin_users.php new file mode 100644 index 0000000..5e0e470 --- /dev/null +++ b/database/migrations/2026_05_09_120001_add_username_and_nullable_email_to_admin_users.php @@ -0,0 +1,79 @@ +string('username', 64)->nullable()->after('id'); + }); + + $this->backfillUsernames(); + + Schema::table('admin_users', function (Blueprint $table) { + $table->string('username', 64)->nullable(false)->change(); + $table->unique('username'); + }); + + Schema::table('admin_users', function (Blueprint $table) { + $table->dropUnique(['email']); + }); + + Schema::table('admin_users', function (Blueprint $table) { + $table->string('email')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('admin_users', function (Blueprint $table) { + $table->dropUnique(['username']); + }); + + Schema::table('admin_users', function (Blueprint $table) { + $table->dropColumn('username'); + }); + + Schema::table('admin_users', function (Blueprint $table) { + $table->string('email')->nullable(false)->change(); + }); + + Schema::table('admin_users', function (Blueprint $table) { + $table->unique('email'); + }); + } + + private function backfillUsernames(): void + { + $reserved = []; + + foreach (DB::table('admin_users')->orderBy('id')->cursor() as $row) { + $email = (string) $row->email; + $local = Str::lower(Str::before($email, '@')); + $slug = preg_replace('/[^a-z0-9._-]/', '', $local); + $base = Str::substr($slug !== '' ? $slug : 'admin'.(string) $row->id, 0, 50); + if ($base === '') { + $base = 'admin'.(string) $row->id; + } + + $candidate = $base; + $n = 0; + while (in_array($candidate, $reserved, true) + || DB::table('admin_users')->where('username', $candidate)->where('id', '!=', $row->id)->exists()) { + $n++; + $suffix = '_'.$n; + $candidate = Str::substr($base, 0, 64 - strlen($suffix)).$suffix; + } + + $reserved[] = $candidate; + + DB::table('admin_users')->where('id', $row->id)->update(['username' => $candidate]); + } + } +}; diff --git a/database/migrations/2026_05_09_120002_migrate_draw_status_to_domain_dict.php b/database/migrations/2026_05_09_120002_migrate_draw_status_to_domain_dict.php new file mode 100644 index 0000000..412a786 --- /dev/null +++ b/database/migrations/2026_05_09_120002_migrate_draw_status_to_domain_dict.php @@ -0,0 +1,22 @@ +where('status', 'pending_review')->update(['status' => 'review']); + DB::table('draws')->where('status', 'published')->update(['status' => 'cooldown']); + } + + public function down(): void + { + DB::table('draws')->where('status', 'review')->update(['status' => 'pending_review']); + DB::table('draws')->where('status', 'cooldown')->update(['status' => 'published']); + } +}; diff --git a/database/migrations/2026_05_11_120000_add_admin_user_id_to_reconcile_jobs_table.php b/database/migrations/2026_05_11_120000_add_admin_user_id_to_reconcile_jobs_table.php new file mode 100644 index 0000000..aa185df --- /dev/null +++ b/database/migrations/2026_05_11_120000_add_admin_user_id_to_reconcile_jobs_table.php @@ -0,0 +1,25 @@ +foreignId('admin_user_id') + ->nullable() + ->constrained('admin_users') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('reconcile_jobs', function (Blueprint $table): void { + $table->dropConstrainedForeignId('admin_user_id'); + }); + } +}; diff --git a/database/migrations/2026_05_11_173000_create_admin_user_permissions_table.php b/database/migrations/2026_05_11_173000_create_admin_user_permissions_table.php new file mode 100644 index 0000000..c40fce4 --- /dev/null +++ b/database/migrations/2026_05_11_173000_create_admin_user_permissions_table.php @@ -0,0 +1,22 @@ +foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete(); + $table->foreignId('permission_id')->constrained('admin_permissions')->cascadeOnDelete(); + $table->primary(['admin_user_id', 'permission_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('admin_user_permissions'); + } +}; diff --git a/database/migrations/2026_05_13_100000_rebuild_admin_authorization_system.php b/database/migrations/2026_05_13_100000_rebuild_admin_authorization_system.php new file mode 100644 index 0000000..c841a57 --- /dev/null +++ b/database/migrations/2026_05_13_100000_rebuild_admin_authorization_system.php @@ -0,0 +1,728 @@ +createTables(); + $this->seedInitialData(); + $this->migrateLegacyAssignments(); + $this->dropLegacyTables(); + } + + public function down(): void + { + $this->recreateLegacyTables(); + $this->migrateBackToLegacyTables(); + + Schema::dropIfExists('admin_user_site_roles'); + Schema::dropIfExists('admin_user_menu_actions'); + Schema::dropIfExists('admin_user_data_scopes'); + Schema::dropIfExists('admin_role_menu_actions'); + Schema::dropIfExists('admin_role_api_resources'); + Schema::dropIfExists('admin_role_menus'); + Schema::dropIfExists('admin_role_data_scopes'); + Schema::dropIfExists('admin_api_resource_bindings'); + Schema::dropIfExists('admin_api_resources'); + Schema::dropIfExists('admin_menu_actions'); + Schema::dropIfExists('admin_action_catalog'); + Schema::dropIfExists('admin_menus'); + Schema::dropIfExists('admin_data_scopes'); + Schema::dropIfExists('admin_sites'); + } + + private function createTables(): void + { + Schema::create('admin_sites', function (Blueprint $table): void { + $table->id(); + $table->string('code', 64)->unique(); + $table->string('name', 128); + $table->string('currency_code', 16)->default('NPR'); + $table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled'); + $table->boolean('is_default')->default(false); + $table->json('extra_json')->nullable(); + $table->timestamps(); + }); + + Schema::table('admin_roles', function (Blueprint $table): void { + $table->string('code', 64)->nullable()->after('id'); + $table->text('description')->nullable()->after('name'); + $table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled')->after('description'); + $table->boolean('is_system')->default(false)->after('status'); + $table->unsignedInteger('sort_order')->default(0)->after('is_system'); + }); + + DB::table('admin_roles')->update([ + 'code' => DB::raw('slug'), + 'status' => 1, + 'is_system' => true, + 'sort_order' => 0, + ]); + + Schema::table('admin_roles', function (Blueprint $table): void { + $table->string('code', 64)->nullable(false)->change(); + $table->unique('code'); + }); + + Schema::create('admin_menus', function (Blueprint $table): void { + $table->id(); + $table->foreignId('parent_id')->nullable()->constrained('admin_menus')->nullOnDelete(); + $table->string('menu_type', 24)->comment('directory|menu|page'); + $table->string('code', 128)->unique(); + $table->string('name', 128); + $table->string('path', 255)->nullable(); + $table->string('route_name', 255)->nullable(); + $table->string('component', 255)->nullable(); + $table->string('icon', 128)->nullable(); + $table->string('active_menu_code', 128)->nullable(); + $table->unsignedInteger('sort_order')->default(0); + $table->boolean('is_visible')->default(true); + $table->boolean('is_cache')->default(false); + $table->boolean('is_external')->default(false); + $table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled'); + $table->json('meta_json')->nullable(); + $table->timestamps(); + + $table->index(['parent_id', 'sort_order'], 'idx_admin_menus_parent_sort'); + }); + + Schema::create('admin_action_catalog', function (Blueprint $table): void { + $table->id(); + $table->string('code', 64)->unique(); + $table->string('name', 64); + $table->unsignedInteger('sort_order')->default(0); + $table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled'); + $table->timestamps(); + }); + + Schema::create('admin_menu_actions', function (Blueprint $table): void { + $table->id(); + $table->foreignId('menu_id')->constrained('admin_menus')->cascadeOnDelete(); + $table->foreignId('action_id')->constrained('admin_action_catalog')->cascadeOnDelete(); + $table->string('permission_code', 128)->unique(); + $table->string('name', 128); + $table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled'); + $table->timestamps(); + + $table->unique(['menu_id', 'action_id'], 'uk_admin_menu_actions_menu_action'); + $table->index(['menu_id', 'status'], 'idx_admin_menu_actions_menu_status'); + }); + + Schema::create('admin_api_resources', function (Blueprint $table): void { + $table->id(); + $table->string('code', 128)->unique(); + $table->string('module_code', 64); + $table->string('name', 128); + $table->string('http_method', 16); + $table->string('uri_pattern', 255); + $table->string('route_name', 255)->nullable(); + $table->string('auth_mode', 24)->default('permission_required')->comment('login_only|permission_required|internal_only'); + $table->boolean('is_audit_required')->default(false); + $table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled'); + $table->json('meta_json')->nullable(); + $table->timestamps(); + + $table->index(['module_code', 'status'], 'idx_admin_api_resources_module_status'); + }); + + Schema::create('admin_api_resource_bindings', function (Blueprint $table): void { + $table->id(); + $table->foreignId('api_resource_id')->constrained('admin_api_resources')->cascadeOnDelete(); + $table->foreignId('menu_action_id')->constrained('admin_menu_actions')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['api_resource_id', 'menu_action_id'], 'uk_admin_api_bindings_api_action'); + }); + + Schema::create('admin_data_scopes', function (Blueprint $table): void { + $table->id(); + $table->string('code', 64)->unique(); + $table->string('name', 128); + $table->string('scope_type', 32)->comment('all_sites|site_only|site_all_data|site_single_player|self_only'); + $table->string('module_code', 64)->nullable(); + $table->text('description')->nullable(); + $table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled'); + $table->timestamps(); + }); + + Schema::create('admin_role_menus', function (Blueprint $table): void { + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->foreignId('menu_id')->constrained('admin_menus')->cascadeOnDelete(); + $table->primary(['role_id', 'menu_id']); + }); + + Schema::create('admin_role_menu_actions', function (Blueprint $table): void { + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->foreignId('menu_action_id')->constrained('admin_menu_actions')->cascadeOnDelete(); + $table->primary(['role_id', 'menu_action_id']); + }); + + Schema::create('admin_role_api_resources', function (Blueprint $table): void { + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->foreignId('api_resource_id')->constrained('admin_api_resources')->cascadeOnDelete(); + $table->primary(['role_id', 'api_resource_id']); + }); + + Schema::create('admin_role_data_scopes', function (Blueprint $table): void { + $table->id(); + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->foreignId('site_id')->nullable()->constrained('admin_sites')->nullOnDelete(); + $table->foreignId('data_scope_id')->constrained('admin_data_scopes')->cascadeOnDelete(); + $table->string('module_code', 64)->nullable(); + $table->json('constraint_json')->nullable(); + $table->timestamps(); + + $table->unique(['role_id', 'site_id', 'data_scope_id', 'module_code'], 'uk_admin_role_data_scopes'); + }); + + Schema::create('admin_user_site_roles', function (Blueprint $table): void { + $table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete(); + $table->foreignId('site_id')->constrained('admin_sites')->cascadeOnDelete(); + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->timestamp('granted_at')->nullable(); + $table->primary(['admin_user_id', 'site_id', 'role_id'], 'pk_admin_user_site_roles'); + }); + + Schema::create('admin_user_menu_actions', function (Blueprint $table): void { + $table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete(); + $table->foreignId('site_id')->nullable()->constrained('admin_sites')->nullOnDelete(); + $table->foreignId('menu_action_id')->constrained('admin_menu_actions')->cascadeOnDelete(); + $table->timestamp('granted_at')->nullable(); + $table->primary(['admin_user_id', 'site_id', 'menu_action_id'], 'pk_admin_user_menu_actions'); + }); + + Schema::create('admin_user_data_scopes', function (Blueprint $table): void { + $table->id(); + $table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete(); + $table->foreignId('site_id')->nullable()->constrained('admin_sites')->nullOnDelete(); + $table->foreignId('data_scope_id')->constrained('admin_data_scopes')->cascadeOnDelete(); + $table->string('module_code', 64)->nullable(); + $table->json('constraint_json')->nullable(); + $table->timestamps(); + + $table->unique(['admin_user_id', 'site_id', 'data_scope_id', 'module_code'], 'uk_admin_user_data_scopes'); + }); + } + + private function seedInitialData(): void + { + $now = Carbon::now(); + + DB::table('admin_sites')->insert([ + 'code' => 'default_site', + 'name' => '默认站点', + 'currency_code' => 'NPR', + 'status' => 1, + 'is_default' => true, + 'extra_json' => json_encode(['source' => 'migration'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'created_at' => $now, + 'updated_at' => $now, + ]); + + DB::table('admin_action_catalog')->insert([ + ['code' => 'view', 'name' => '查看', 'sort_order' => 10, 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'create', 'name' => '新增', 'sort_order' => 20, 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'update', 'name' => '编辑', 'sort_order' => 30, 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'delete', 'name' => '删除', 'sort_order' => 40, 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'review', 'name' => '审核', 'sort_order' => 50, 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'publish', 'name' => '发布', 'sort_order' => 60, 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'export', 'name' => '导出', 'sort_order' => 70, 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'manage', 'name' => '管理', 'sort_order' => 80, 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ]); + + DB::table('admin_data_scopes')->insert([ + ['code' => 'all_sites', 'name' => '全站点', 'scope_type' => 'all_sites', 'module_code' => null, 'description' => '可访问所有站点数据', 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'site_only', 'name' => '指定站点', 'scope_type' => 'site_only', 'module_code' => null, 'description' => '仅限授权站点登录和访问', 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'site_all_data', 'name' => '站点内全部数据', 'scope_type' => 'site_all_data', 'module_code' => null, 'description' => '可访问站点内全部业务数据', 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'site_single_player', 'name' => '站点内单玩家', 'scope_type' => 'site_single_player', 'module_code' => 'player_service', 'description' => '仅限按指定玩家处理客诉与查单', 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ['code' => 'self_only', 'name' => '仅本人相关', 'scope_type' => 'self_only', 'module_code' => 'audit', 'description' => '仅可查看与自身相关的数据', 'status' => 1, 'created_at' => $now, 'updated_at' => $now], + ]); + + $this->seedMenuTree($now); + $this->seedApiResources($now); + } + + private function seedMenuTree(Carbon $now): void + { + $menus = [ + ['parent_code' => null, 'menu_type' => 'menu', 'code' => 'dashboard', 'name' => '仪表盘', 'path' => '/admin', 'route_name' => 'admin.dashboard', 'component' => 'dashboard/index', 'icon' => 'layout-dashboard', 'sort_order' => 10], + ['parent_code' => null, 'menu_type' => 'directory', 'code' => 'draw', 'name' => '开奖管理', 'path' => '/admin/draws', 'route_name' => null, 'component' => null, 'icon' => 'dice-5', 'sort_order' => 20], + ['parent_code' => 'draw', 'menu_type' => 'page', 'code' => 'draw.results', 'name' => '开奖结果', 'path' => '/admin/draws', 'route_name' => 'admin.draws.index', 'component' => 'draw/results', 'icon' => null, 'sort_order' => 10], + ['parent_code' => 'draw', 'menu_type' => 'page', 'code' => 'draw.review', 'name' => '开奖审核', 'path' => '/admin/draws/review', 'route_name' => 'admin.draws.review', 'component' => 'draw/review', 'icon' => null, 'sort_order' => 20], + ['parent_code' => null, 'menu_type' => 'directory', 'code' => 'config', 'name' => '运营配置', 'path' => '/admin/config', 'route_name' => null, 'component' => null, 'icon' => 'sliders-horizontal', 'sort_order' => 30], + ['parent_code' => 'config', 'menu_type' => 'page', 'code' => 'config.play', 'name' => '玩法开关', 'path' => '/admin/config/play-switches', 'route_name' => 'admin.config.play', 'component' => 'config/play', 'icon' => null, 'sort_order' => 10], + ['parent_code' => 'config', 'menu_type' => 'page', 'code' => 'config.odds', 'name' => '赔率配置', 'path' => '/admin/config/odds', 'route_name' => 'admin.config.odds', 'component' => 'config/odds', 'icon' => null, 'sort_order' => 20], + ['parent_code' => 'config', 'menu_type' => 'page', 'code' => 'config.risk_cap', 'name' => '封顶配置', 'path' => '/admin/config/play-limits', 'route_name' => 'admin.config.risk_cap', 'component' => 'config/risk-cap', 'icon' => null, 'sort_order' => 30], + ['parent_code' => 'config', 'menu_type' => 'page', 'code' => 'config.jackpot', 'name' => 'Jackpot 配置', 'path' => '/admin/jackpot/pools', 'route_name' => 'admin.jackpot.pools', 'component' => 'config/jackpot', 'icon' => null, 'sort_order' => 40], + ['parent_code' => null, 'menu_type' => 'page', 'code' => 'risk.monitor', 'name' => '风控监控', 'path' => '/admin/risk', 'route_name' => 'admin.risk.monitor', 'component' => 'risk/monitor', 'icon' => 'shield-alert', 'sort_order' => 40], + ['parent_code' => null, 'menu_type' => 'page', 'code' => 'settlement.batch', 'name' => '结算批次', 'path' => '/admin/settlement-batches', 'route_name' => 'admin.settlement.batches', 'component' => 'settlement/batches', 'icon' => 'receipt-text', 'sort_order' => 50], + ['parent_code' => null, 'menu_type' => 'directory', 'code' => 'service', 'name' => '客服财务', 'path' => '/admin/service-desk', 'route_name' => null, 'component' => null, 'icon' => 'hand-helping', 'sort_order' => 60], + ['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.players', 'name' => '玩家查询', 'path' => '/admin/players', 'route_name' => 'admin.players.index', 'component' => 'service/players', 'icon' => null, 'sort_order' => 10], + ['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.tickets', 'name' => '玩家注单', 'path' => '/admin/tickets', 'route_name' => 'admin.tickets.index', 'component' => 'service/tickets', 'icon' => null, 'sort_order' => 20], + ['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.wallet', 'name' => '钱包流水', 'path' => '/admin/wallet/transactions', 'route_name' => 'admin.wallet.transactions', 'component' => 'service/wallet', 'icon' => null, 'sort_order' => 30], + ['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.reconcile', 'name' => '对账管理', 'path' => '/admin/reconcile', 'route_name' => 'admin.reconcile.index', 'component' => 'service/reconcile', 'icon' => null, 'sort_order' => 40], + ['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.audit', 'name' => '审计日志', 'path' => '/admin/audit-logs', 'route_name' => 'admin.audit.index', 'component' => 'service/audit', 'icon' => null, 'sort_order' => 60], + ['parent_code' => null, 'menu_type' => 'page', 'code' => 'system.admin_user', 'name' => '管理员权限', 'path' => '/admin/admin-users', 'route_name' => 'admin.system.admin-users', 'component' => 'system/admin-users', 'icon' => 'users-round', 'sort_order' => 70], + ['parent_code' => null, 'menu_type' => 'page', 'code' => 'system.admin_role', 'name' => '角色管理', 'path' => '/admin/admin-roles', 'route_name' => 'admin.system.admin-roles', 'component' => 'system/admin-roles', 'icon' => 'shield-check', 'sort_order' => 71], + ]; + + $menuIds = []; + foreach ($menus as $menu) { + $menuIds[$menu['code']] = DB::table('admin_menus')->insertGetId([ + 'parent_id' => $menu['parent_code'] === null ? null : $menuIds[$menu['parent_code']], + 'menu_type' => $menu['menu_type'], + 'code' => $menu['code'], + 'name' => $menu['name'], + 'path' => $menu['path'], + 'route_name' => $menu['route_name'], + 'component' => $menu['component'], + 'icon' => $menu['icon'], + 'active_menu_code' => null, + 'sort_order' => $menu['sort_order'], + 'is_visible' => true, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $actionIds = DB::table('admin_action_catalog')->pluck('id', 'code'); + $menuActions = [ + ['menu_code' => 'draw.results', 'action_code' => 'view', 'permission_code' => 'draw.results.view', 'name' => '开奖结果查看'], + ['menu_code' => 'draw.review', 'action_code' => 'review', 'permission_code' => 'draw.review.review', 'name' => '开奖审核'], + ['menu_code' => 'draw.review', 'action_code' => 'publish', 'permission_code' => 'draw.review.publish', 'name' => '开奖发布'], + ['menu_code' => 'config.play', 'action_code' => 'manage', 'permission_code' => 'config.play.manage', 'name' => '玩法开关管理'], + ['menu_code' => 'config.odds', 'action_code' => 'manage', 'permission_code' => 'config.odds.manage', 'name' => '赔率配置管理'], + ['menu_code' => 'config.risk_cap', 'action_code' => 'view', 'permission_code' => 'config.risk_cap.view', 'name' => '封顶配置查看'], + ['menu_code' => 'config.risk_cap', 'action_code' => 'manage', 'permission_code' => 'config.risk_cap.manage', 'name' => '封顶配置管理'], + ['menu_code' => 'config.jackpot', 'action_code' => 'view', 'permission_code' => 'config.jackpot.view', 'name' => 'Jackpot 查看'], + ['menu_code' => 'config.jackpot', 'action_code' => 'manage', 'permission_code' => 'config.jackpot.manage', 'name' => 'Jackpot 管理'], + ['menu_code' => 'risk.monitor', 'action_code' => 'view', 'permission_code' => 'risk.monitor.view', 'name' => '风控监控查看'], + ['menu_code' => 'settlement.batch', 'action_code' => 'view', 'permission_code' => 'settlement.batch.view', 'name' => '结算查看'], + ['menu_code' => 'settlement.batch', 'action_code' => 'review', 'permission_code' => 'settlement.batch.review', 'name' => '结算审核'], + ['menu_code' => 'settlement.batch', 'action_code' => 'manage', 'permission_code' => 'settlement.batch.manage', 'name' => '结算执行'], + ['menu_code' => 'service.players', 'action_code' => 'view', 'permission_code' => 'service.players.view', 'name' => '玩家查询查看'], + ['menu_code' => 'service.players', 'action_code' => 'manage', 'permission_code' => 'service.players.manage', 'name' => '玩家查询管理'], + ['menu_code' => 'service.players', 'action_code' => 'update', 'permission_code' => 'service.players.freeze', 'name' => '冻结解冻玩家'], + ['menu_code' => 'service.tickets', 'action_code' => 'view', 'permission_code' => 'service.tickets.view', 'name' => '玩家注单查看'], + ['menu_code' => 'service.wallet', 'action_code' => 'view', 'permission_code' => 'service.wallet.view', 'name' => '钱包流水查看'], + ['menu_code' => 'service.wallet', 'action_code' => 'manage', 'permission_code' => 'service.wallet.manage', 'name' => '钱包流水管理'], + ['menu_code' => 'service.reconcile', 'action_code' => 'view', 'permission_code' => 'service.reconcile.view', 'name' => '对账查看'], + ['menu_code' => 'service.reconcile', 'action_code' => 'manage', 'permission_code' => 'service.reconcile.manage', 'name' => '对账管理'], + ['menu_code' => 'service.audit', 'action_code' => 'view', 'permission_code' => 'service.audit.view', 'name' => '审计查看'], + ['menu_code' => 'system.admin_user', 'action_code' => 'manage', 'permission_code' => 'system.admin_user.manage', 'name' => '管理员权限管理'], + ['menu_code' => 'system.admin_role', 'action_code' => 'manage', 'permission_code' => 'system.admin_role.manage', 'name' => '角色权限管理'], + ]; + + foreach ($menuActions as $row) { + DB::table('admin_menu_actions')->insert([ + 'menu_id' => $menuIds[$row['menu_code']], + 'action_id' => $actionIds[$row['action_code']], + 'permission_code' => $row['permission_code'], + 'name' => $row['name'], + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + private function seedApiResources(Carbon $now): void + { + $resources = [ + ['code' => 'admin.dashboard', 'module_code' => 'dashboard', 'name' => '后台仪表盘', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/dashboard', 'route_name' => 'api.v1.admin.dashboard', 'auth_mode' => 'login_only', 'is_audit_required' => false, 'permission_codes' => []], + ['code' => 'admin.draws.index', 'module_code' => 'draw', 'name' => '期开奖列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws', 'route_name' => 'api.v1.admin.draws.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['draw.results.view']], + ['code' => 'admin.draws.show', 'module_code' => 'draw', 'name' => '期开奖详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}', 'route_name' => 'api.v1.admin.draws.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['draw.results.view']], + ['code' => 'admin.draws.publish', 'module_code' => 'draw', 'name' => '开奖发布', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/result-batches/{batch}/publish', 'route_name' => 'api.v1.admin.draws.result-batches.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['draw.review.publish']], + ['code' => 'admin.draws.settlement.run', 'module_code' => 'settlement', 'name' => '执行结算', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/settlement/run', 'route_name' => 'api.v1.admin.draws.settlement.run', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.batch.manage', 'settlement.batch.review']], + ['code' => 'admin.wallet.transactions', 'module_code' => 'wallet', 'name' => '钱包流水查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/wallet/transactions', 'route_name' => 'api.v1.admin.wallet.transactions', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.wallet.view', 'service.wallet.manage']], + ['code' => 'admin.wallet.transfer-orders', 'module_code' => 'wallet', 'name' => '转账单查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/wallet/transfer-orders', 'route_name' => 'api.v1.admin.wallet.transfer-orders', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.wallet.view', 'service.wallet.manage']], + ['code' => 'admin.players.wallets', 'module_code' => 'player_service', 'name' => '玩家钱包查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/wallets', 'route_name' => 'api.v1.admin.players.wallets', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.wallet.view']], + ['code' => 'admin.players.ticket-items', 'module_code' => 'player_service', 'name' => '玩家注单查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/ticket-items', 'route_name' => 'api.v1.admin.players.ticket-items.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.tickets.view']], + ['code' => 'admin.reconcile.index', 'module_code' => 'reconcile', 'name' => '对账任务列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reconcile-jobs', 'route_name' => 'api.v1.admin.reconcile-jobs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.reconcile.view', 'service.reconcile.manage']], + ['code' => 'admin.reconcile.store', 'module_code' => 'reconcile', 'name' => '创建对账任务', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/reconcile-jobs', 'route_name' => 'api.v1.admin.reconcile-jobs.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.reconcile.manage']], + ['code' => 'admin.audit.index', 'module_code' => 'audit', 'name' => '审计日志查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/audit-logs', 'route_name' => 'api.v1.admin.audit-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.audit.view']], + ['code' => 'admin.admin-users.index', 'module_code' => 'system', 'name' => '管理员列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/admin-users', 'route_name' => 'api.v1.admin.admin-users.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['system.admin_user.manage']], + ['code' => 'admin.admin-users.permission-catalog', 'module_code' => 'system', 'name' => '管理员权限目录', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/admin-user-permission-catalog', 'route_name' => 'api.v1.admin.admin-users.permission-catalog', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['system.admin_user.manage']], + ['code' => 'admin.admin-users.permissions.sync', 'module_code' => 'system', 'name' => '管理员权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-users/{admin_user}/permissions', 'route_name' => 'api.v1.admin.admin-users.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['system.admin_user.manage']], + ]; + + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources')->insertGetId([ + 'code' => $resource['code'], + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + foreach ($resource['permission_codes'] as $permissionCode) { + if (! isset($menuActionIds[$permissionCode])) { + continue; + } + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => $resourceId, + 'menu_action_id' => $menuActionIds[$permissionCode], + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + private function migrateLegacyAssignments(): void + { + $now = Carbon::now(); + $defaultSiteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); + + $legacyRoles = DB::table('admin_roles') + ->select('id', 'code', 'slug', 'name') + ->get(); + + $legacyRoleAssignments = DB::table('admin_user_roles')->get(); + foreach ($legacyRoleAssignments as $row) { + $legacyRoleId = (int) $row->role_id; + DB::table('admin_user_site_roles')->insert([ + 'admin_user_id' => (int) $row->admin_user_id, + 'site_id' => $defaultSiteId, + 'role_id' => $legacyRoleId, + 'granted_at' => $now, + ]); + } + + $legacyPermissionById = DB::table('admin_permissions')->pluck('slug', 'id'); + $legacyRolePermissions = DB::table('admin_role_permissions')->get()->groupBy('role_id'); + $legacyUserPermissions = DB::table('admin_user_permissions')->get()->groupBy('admin_user_id'); + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + $apiResourceIdsByPermission = DB::table('admin_api_resource_bindings') + ->join('admin_menu_actions', 'admin_menu_actions.id', '=', 'admin_api_resource_bindings.menu_action_id') + ->select('admin_menu_actions.permission_code', 'admin_api_resource_bindings.api_resource_id') + ->get() + ->groupBy('permission_code') + ->map(static fn ($rows) => $rows->pluck('api_resource_id')->all()); + $menuIdsByPermission = DB::table('admin_menu_actions') + ->join('admin_menus', 'admin_menus.id', '=', 'admin_menu_actions.menu_id') + ->pluck('admin_menus.id', 'admin_menu_actions.permission_code'); + + $legacyToNewPermissionMap = [ + 'prd.users.manage' => ['service.players.manage'], + 'prd.users.view_finance' => ['service.players.view', 'service.wallet.view'], + 'prd.users.view_cs' => ['service.players.view', 'service.tickets.view'], + 'prd.play_switch.manage' => ['config.play.manage'], + 'prd.odds.manage' => ['config.odds.manage'], + 'prd.risk_cap.manage' => ['config.risk_cap.manage'], + 'prd.risk_cap.view' => ['config.risk_cap.view'], + 'prd.rebate.manage' => ['config.odds.manage'], + 'prd.rebate.view' => ['config.odds.manage'], + 'prd.jackpot.manage' => ['config.jackpot.manage'], + 'prd.jackpot.view' => ['config.jackpot.view'], + 'prd.draw_result.manage' => ['draw.results.view', 'draw.review.review', 'draw.review.publish', 'risk.monitor.view'], + 'prd.draw_result.view' => ['draw.results.view', 'risk.monitor.view'], + 'prd.payout.manage' => ['settlement.batch.manage', 'settlement.batch.view'], + 'prd.payout.review' => ['settlement.batch.review', 'settlement.batch.view'], + 'prd.payout.view' => ['settlement.batch.view'], + 'prd.wallet_reconcile.manage' => ['service.wallet.manage', 'service.reconcile.manage'], + 'prd.wallet_reconcile.view' => ['service.wallet.view', 'service.reconcile.view'], + 'prd.wallet_reconcile.view_cs' => ['service.wallet.view', 'service.reconcile.view'], + 'prd.audit.all' => ['service.audit.view'], + 'prd.audit.self' => ['service.audit.view'], + 'prd.audit.finance' => ['service.audit.view'], + 'prd.admin_user.manage' => ['system.admin_user.manage'], + 'prd.player_freeze.manage' => ['service.players.freeze'], + 'prd.wallet_adjust.manage' => ['service.wallet.manage'], + 'prd.draw_reopen.manage' => ['draw.review.publish'], + ]; + + foreach ($legacyRoles as $role) { + $roleId = (int) $role->id; + + $grantedPermissions = []; + foreach ($legacyRolePermissions->get((int) $role->id, collect()) as $pivot) { + $permissionId = (int) $pivot->permission_id; + $legacySlug = $legacyPermissionById[$permissionId] ?? null; + if (! is_string($legacySlug)) { + continue; + } + foreach ($legacyToNewPermissionMap[$legacySlug] ?? [] as $permissionCode) { + $grantedPermissions[$permissionCode] = true; + } + } + + foreach (array_keys($grantedPermissions) as $permissionCode) { + if (isset($menuIdsByPermission[$permissionCode])) { + $this->grantMenuWithAncestors($roleId, (int) $menuIdsByPermission[$permissionCode]); + } + if (isset($menuActionIds[$permissionCode])) { + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => $roleId, + 'menu_action_id' => (int) $menuActionIds[$permissionCode], + ]); + } + foreach ($apiResourceIdsByPermission[$permissionCode] ?? [] as $apiResourceId) { + DB::table('admin_role_api_resources')->updateOrInsert([ + 'role_id' => $roleId, + 'api_resource_id' => (int) $apiResourceId, + ]); + } + } + + $roleCode = (string) ($role->code ?: $role->slug); + $this->assignRoleDataScopes($roleId, $roleCode, $defaultSiteId, $now); + } + + foreach ($legacyUserPermissions as $adminUserId => $pivots) { + $grantedPermissions = []; + foreach ($pivots as $pivot) { + $permissionId = (int) $pivot->permission_id; + $legacySlug = $legacyPermissionById[$permissionId] ?? null; + if (! is_string($legacySlug)) { + continue; + } + foreach ($legacyToNewPermissionMap[$legacySlug] ?? [] as $permissionCode) { + $grantedPermissions[$permissionCode] = true; + } + } + + foreach (array_keys($grantedPermissions) as $permissionCode) { + if (! isset($menuActionIds[$permissionCode])) { + continue; + } + DB::table('admin_user_menu_actions')->updateOrInsert([ + 'admin_user_id' => (int) $adminUserId, + 'site_id' => $defaultSiteId, + 'menu_action_id' => (int) $menuActionIds[$permissionCode], + ], [ + 'granted_at' => $now, + ]); + } + } + } + + private function assignRoleDataScopes(int $roleId, string $roleCode, int $siteId, Carbon $now): void + { + $dataScopeIds = DB::table('admin_data_scopes')->pluck('id', 'code'); + + $rows = match ($roleCode) { + 'super_admin' => [ + ['scope_code' => 'all_sites', 'module_code' => null], + ['scope_code' => 'site_all_data', 'module_code' => null], + ], + 'risk_operator' => [ + ['scope_code' => 'site_only', 'module_code' => null], + ['scope_code' => 'site_all_data', 'module_code' => 'risk'], + ['scope_code' => 'self_only', 'module_code' => 'audit'], + ], + 'finance' => [ + ['scope_code' => 'site_only', 'module_code' => null], + ['scope_code' => 'site_all_data', 'module_code' => 'wallet'], + ['scope_code' => 'site_all_data', 'module_code' => 'settlement'], + ['scope_code' => 'site_all_data', 'module_code' => 'report'], + ['scope_code' => 'site_all_data', 'module_code' => 'reconcile'], + ], + 'customer_service' => [ + ['scope_code' => 'site_only', 'module_code' => null], + ['scope_code' => 'site_single_player', 'module_code' => 'player_service'], + ], + default => [ + ['scope_code' => 'site_only', 'module_code' => null], + ], + }; + + foreach ($rows as $row) { + $scopeId = $dataScopeIds[$row['scope_code']] ?? null; + if ($scopeId === null) { + continue; + } + DB::table('admin_role_data_scopes')->insert([ + 'role_id' => $roleId, + 'site_id' => $row['scope_code'] === 'all_sites' ? null : $siteId, + 'data_scope_id' => (int) $scopeId, + 'module_code' => $row['module_code'], + 'constraint_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + private function grantMenuWithAncestors(int $roleId, int $menuId): void + { + $currentMenuId = $menuId; + + while ($currentMenuId > 0) { + DB::table('admin_role_menus')->updateOrInsert([ + 'role_id' => $roleId, + 'menu_id' => $currentMenuId, + ]); + + $parentId = DB::table('admin_menus')->where('id', $currentMenuId)->value('parent_id'); + $currentMenuId = $parentId === null ? 0 : (int) $parentId; + } + } + + private function dropLegacyTables(): void + { + Schema::dropIfExists('admin_user_permissions'); + Schema::dropIfExists('admin_user_roles'); + Schema::dropIfExists('admin_role_permissions'); + Schema::dropIfExists('admin_permissions'); + } + + private function recreateLegacyTables(): void + { + Schema::table('admin_roles', function (Blueprint $table): void { + $table->dropUnique(['code']); + $table->dropColumn(['code', 'description', 'status', 'is_system', 'sort_order']); + }); + + Schema::create('admin_permissions', function (Blueprint $table): void { + $table->id(); + $table->string('slug', 128)->unique(); + $table->string('name', 128); + $table->timestamps(); + }); + + Schema::create('admin_role_permissions', function (Blueprint $table): void { + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->foreignId('permission_id')->constrained('admin_permissions')->cascadeOnDelete(); + $table->primary(['role_id', 'permission_id']); + }); + + Schema::create('admin_user_roles', function (Blueprint $table): void { + $table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete(); + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->primary(['admin_user_id', 'role_id']); + }); + + Schema::create('admin_user_permissions', function (Blueprint $table): void { + $table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete(); + $table->foreignId('permission_id')->constrained('admin_permissions')->cascadeOnDelete(); + $table->primary(['admin_user_id', 'permission_id']); + }); + } + + private function migrateBackToLegacyTables(): void + { + $now = Carbon::now(); + + $legacyPermissions = [ + ['slug' => 'prd.users.manage', 'name' => '用户管理·可管理'], + ['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看'], + ['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户'], + ['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理'], + ['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理'], + ['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理'], + ['slug' => 'prd.risk_cap.view', 'name' => '封顶配置·查看'], + ['slug' => 'prd.rebate.manage', 'name' => '佣金/回水·可管理'], + ['slug' => 'prd.rebate.view', 'name' => '佣金/回水·查看'], + ['slug' => 'prd.jackpot.manage', 'name' => 'Jackpot 配置·可管理'], + ['slug' => 'prd.jackpot.view', 'name' => 'Jackpot 配置·查看'], + ['slug' => 'prd.draw_result.manage', 'name' => '开奖结果录入·可管理'], + ['slug' => 'prd.draw_result.view', 'name' => '开奖结果·查看'], + ['slug' => 'prd.draw_reopen.manage', 'name' => '开奖结果重开·可管理'], + ['slug' => 'prd.payout.manage', 'name' => '派彩确认·可管理'], + ['slug' => 'prd.payout.review', 'name' => '派彩确认·可审核'], + ['slug' => 'prd.payout.view', 'name' => '派彩确认·查看'], + ['slug' => 'prd.wallet_reconcile.manage', 'name' => '钱包对账·可管理'], + ['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看'], + ['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户'], + ['slug' => 'prd.wallet_adjust.manage', 'name' => '补单/冲正·可管理'], + ['slug' => 'prd.audit.all', 'name' => '审计日志·全部'], + ['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关'], + ['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关'], + ['slug' => 'prd.player_freeze.manage', 'name' => '冻结/解冻玩家·可管理'], + ['slug' => 'prd.admin_user.manage', 'name' => '后台用户权限管理·可管理'], + ]; + + foreach ($legacyPermissions as $permission) { + DB::table('admin_permissions')->insert([ + 'slug' => $permission['slug'], + 'name' => $permission['name'], + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $permissionIds = DB::table('admin_permissions')->pluck('id', 'slug'); + $roleCodeMap = DB::table('admin_roles')->pluck('id', 'slug'); + + $rolePermissionMap = [ + 'super_admin' => array_keys($permissionIds->all()), + 'risk_operator' => [ + 'prd.play_switch.manage', + 'prd.odds.manage', + 'prd.risk_cap.manage', + 'prd.rebate.manage', + 'prd.jackpot.manage', + 'prd.draw_result.manage', + 'prd.payout.review', + 'prd.wallet_reconcile.view', + 'prd.audit.self', + 'prd.player_freeze.manage', + ], + 'finance' => [ + 'prd.users.view_finance', + 'prd.risk_cap.view', + 'prd.rebate.view', + 'prd.jackpot.view', + 'prd.draw_result.view', + 'prd.payout.view', + 'prd.wallet_reconcile.manage', + 'prd.wallet_adjust.manage', + 'prd.audit.finance', + ], + 'customer_service' => [ + 'prd.users.view_cs', + 'prd.draw_result.view', + 'prd.wallet_reconcile.view_cs', + ], + ]; + + foreach ($rolePermissionMap as $roleCode => $permissionSlugs) { + $roleId = $roleCodeMap[$roleCode] ?? null; + if ($roleId === null) { + continue; + } + + foreach ($permissionSlugs as $slug) { + $permissionId = $permissionIds[$slug] ?? null; + if ($permissionId === null) { + continue; + } + DB::table('admin_role_permissions')->insert([ + 'role_id' => (int) $roleId, + 'permission_id' => (int) $permissionId, + ]); + } + } + + $userRoles = DB::table('admin_user_site_roles') + ->select('admin_user_id', 'role_id') + ->distinct() + ->get(); + + foreach ($userRoles as $row) { + DB::table('admin_user_roles')->insert([ + 'admin_user_id' => (int) $row->admin_user_id, + 'role_id' => (int) $row->role_id, + ]); + } + } +}; diff --git a/database/migrations/2026_05_16_000100_add_snapshot_columns_to_play_config_items_table.php b/database/migrations/2026_05_16_000100_add_snapshot_columns_to_play_config_items_table.php new file mode 100644 index 0000000..b27cf94 --- /dev/null +++ b/database/migrations/2026_05_16_000100_add_snapshot_columns_to_play_config_items_table.php @@ -0,0 +1,79 @@ +string('category', 16)->nullable()->after('play_code'); + $table->unsignedTinyInteger('dimension')->nullable()->after('category'); + $table->string('bet_mode', 32)->nullable()->after('dimension'); + $table->string('display_name_zh', 64)->nullable()->after('bet_mode'); + $table->string('display_name_en', 64)->nullable()->after('display_name_zh'); + $table->string('display_name_ne', 64)->nullable()->after('display_name_en'); + $table->boolean('supports_multi_number')->default(false)->after('display_name_ne'); + $table->json('reserved_rule_json')->nullable()->after('supports_multi_number'); + }); + + $playTypes = DB::table('play_types') + ->select([ + 'play_code', + 'category', + 'dimension', + 'bet_mode', + 'display_name_zh', + 'display_name_en', + 'display_name_ne', + 'supports_multi_number', + 'reserved_rule_json', + ]) + ->get() + ->keyBy('play_code'); + + DB::table('play_config_items') + ->select(['id', 'play_code']) + ->orderBy('id') + ->chunkById(200, function ($rows) use ($playTypes): void { + foreach ($rows as $row) { + $pt = $playTypes->get($row->play_code); + if ($pt === null) { + continue; + } + + DB::table('play_config_items') + ->where('id', $row->id) + ->update([ + 'category' => $pt->category, + 'dimension' => $pt->dimension, + 'bet_mode' => $pt->bet_mode, + 'display_name_zh' => $pt->display_name_zh, + 'display_name_en' => $pt->display_name_en, + 'display_name_ne' => $pt->display_name_ne, + 'supports_multi_number' => (bool) $pt->supports_multi_number, + 'reserved_rule_json' => $pt->reserved_rule_json, + ]); + } + }, 'id'); + } + + public function down(): void + { + Schema::table('play_config_items', function (Blueprint $table): void { + $table->dropColumn([ + 'category', + 'dimension', + 'bet_mode', + 'display_name_zh', + 'display_name_en', + 'display_name_ne', + 'supports_multi_number', + 'reserved_rule_json', + ]); + }); + } +}; diff --git a/database/migrations/2026_05_18_000001_add_combo_trigger_to_jackpot_pools_table.php b/database/migrations/2026_05_18_000001_add_combo_trigger_to_jackpot_pools_table.php new file mode 100644 index 0000000..f6b9f70 --- /dev/null +++ b/database/migrations/2026_05_18_000001_add_combo_trigger_to_jackpot_pools_table.php @@ -0,0 +1,22 @@ +json('combo_trigger_play_codes')->nullable()->after('min_bet_amount'); + }); + } + + public function down(): void + { + Schema::table('jackpot_pools', function (Blueprint $table): void { + $table->dropColumn('combo_trigger_play_codes'); + }); + } +}; diff --git a/database/migrations/2026_05_18_090000_add_config_version_snapshots_to_ticket_orders.php b/database/migrations/2026_05_18_090000_add_config_version_snapshots_to_ticket_orders.php new file mode 100644 index 0000000..f005089 --- /dev/null +++ b/database/migrations/2026_05_18_090000_add_config_version_snapshots_to_ticket_orders.php @@ -0,0 +1,28 @@ +unsignedInteger('play_config_version_no')->default(0)->after('client_trace_id'); + $table->unsignedInteger('odds_version_no')->default(0)->after('play_config_version_no'); + $table->unsignedInteger('risk_cap_version_no')->default(0)->after('odds_version_no'); + }); + } + + public function down(): void + { + Schema::table('ticket_orders', function (Blueprint $table): void { + $table->dropColumn([ + 'play_config_version_no', + 'odds_version_no', + 'risk_cap_version_no', + ]); + }); + } +}; diff --git a/database/migrations/2026_05_18_120000_sync_complete_admin_api_resources.php b/database/migrations/2026_05_18_120000_sync_complete_admin_api_resources.php new file mode 100644 index 0000000..9966635 --- /dev/null +++ b/database/migrations/2026_05_18_120000_sync_complete_admin_api_resources.php @@ -0,0 +1,83 @@ +pluck('id', 'permission_code'); + + foreach (AdminAuthorizationRegistry::resources() as $resource) { + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + DB::table('admin_role_api_resources')->delete(); + + $roleResourceRows = DB::table('admin_role_menu_actions as rma') + ->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id') + ->select('rma.role_id', 'arb.api_resource_id') + ->distinct() + ->get(); + + foreach ($roleResourceRows as $row) { + DB::table('admin_role_api_resources')->insert([ + 'role_id' => (int) $row->role_id, + 'api_resource_id' => (int) $row->api_resource_id, + ]); + } + } + + public function down(): void + { + // 保持数据升级可逆风险最低:不在 down 中尝试删除资源,避免误删线上已使用授权关系。 + } +}; diff --git a/database/migrations/2026_05_19_112752_seed_default_jackpot_pools.php b/database/migrations/2026_05_19_112752_seed_default_jackpot_pools.php new file mode 100644 index 0000000..1c7b063 --- /dev/null +++ b/database/migrations/2026_05_19_112752_seed_default_jackpot_pools.php @@ -0,0 +1,51 @@ +where('is_enabled', true) + ->where('is_bettable', true) + ->pluck('code') + ->filter(static fn ($code): bool => is_string($code) && trim($code) !== '') + ->map(static fn (string $code): string => strtoupper($code)) + ->unique() + ->values(); + + if ($currencyCodes->isEmpty()) { + $currencyCodes = collect([strtoupper((string) config('lottery.default_currency', 'NPR'))]); + } + + foreach ($currencyCodes as $currencyCode) { + $exists = DB::table('jackpot_pools')->where('currency_code', $currencyCode)->exists(); + if ($exists) { + continue; + } + + DB::table('jackpot_pools')->insert([ + 'currency_code' => $currencyCode, + 'current_amount' => 0, + 'contribution_rate' => '0.0200', + 'trigger_threshold' => 100_000_000, + 'payout_rate' => '0.5000', + 'force_trigger_draw_gap' => 100, + 'min_bet_amount' => 100, + 'status' => 0, + 'last_trigger_draw_id' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + public function down(): void + { + // 保留奖池配置与水位,避免回滚误删运营数据。 + } +}; diff --git a/database/migrations/2026_05_19_120000_create_admin_role_legacy_permissions_table.php b/database/migrations/2026_05_19_120000_create_admin_role_legacy_permissions_table.php new file mode 100644 index 0000000..e824128 --- /dev/null +++ b/database/migrations/2026_05_19_120000_create_admin_role_legacy_permissions_table.php @@ -0,0 +1,48 @@ +foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->string('permission_slug', 128); + $table->timestamps(); + + $table->primary(['role_id', 'permission_slug'], 'pk_admin_role_legacy_permissions'); + }); + + $now = now(); + $roleCodes = DB::table('admin_role_menu_actions as rma') + ->join('admin_menu_actions as ma', 'ma.id', '=', 'rma.menu_action_id') + ->where('ma.status', 1) + ->select('rma.role_id', 'ma.permission_code') + ->get() + ->groupBy('role_id'); + + foreach ($roleCodes as $roleId => $rows) { + $slugs = AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes( + $rows->pluck('permission_code')->all(), + ); + foreach ($slugs as $slug) { + DB::table('admin_role_legacy_permissions')->insert([ + 'role_id' => (int) $roleId, + 'permission_slug' => $slug, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + public function down(): void + { + Schema::dropIfExists('admin_role_legacy_permissions'); + } +}; diff --git a/database/migrations/2026_05_19_121000_sync_admin_role_manage_permission.php b/database/migrations/2026_05_19_121000_sync_admin_role_manage_permission.php new file mode 100644 index 0000000..4d50fed --- /dev/null +++ b/database/migrations/2026_05_19_121000_sync_admin_role_manage_permission.php @@ -0,0 +1,124 @@ +where('code', 'manage')->value('id'); + $adminUserMenuId = (int) DB::table('admin_menus')->where('code', 'system.admin_user')->value('id'); + + if ($adminUserMenuId > 0) { + $adminUserMenu = DB::table('admin_menus')->where('id', $adminUserMenuId)->first(); + DB::table('admin_menus')->updateOrInsert( + ['code' => 'system.admin_role'], + [ + 'parent_id' => $adminUserMenu->parent_id, + 'menu_type' => 'page', + 'name' => '角色管理', + 'path' => '/admin/admin-roles', + 'route_name' => 'admin.system.admin-roles', + 'component' => 'system/admin-roles', + 'icon' => 'shield-check', + 'active_menu_code' => null, + 'sort_order' => ((int) $adminUserMenu->sort_order) + 1, + 'is_visible' => true, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + } + + $menuId = (int) DB::table('admin_menus')->where('code', 'system.admin_role')->value('id'); + + if ($actionCatalogId > 0 && $menuId > 0) { + DB::table('admin_menu_actions')->updateOrInsert( + ['permission_code' => 'system.admin_role.manage'], + [ + 'menu_id' => $menuId, + 'action_id' => $actionCatalogId, + 'name' => '角色权限管理', + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + } + + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + foreach (AdminAuthorizationRegistry::resources() as $resource) { + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + if ($resourceId === null) { + continue; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $adminRoleSlug = 'prd.admin_role.manage'; + $adminUserSlug = 'prd.admin_user.manage'; + $roleIds = DB::table('admin_role_legacy_permissions') + ->where('permission_slug', $adminUserSlug) + ->pluck('role_id') + ->all(); + + foreach ($roleIds as $roleId) { + DB::table('admin_role_legacy_permissions')->updateOrInsert( + [ + 'role_id' => (int) $roleId, + 'permission_slug' => $adminRoleSlug, + ], + [ + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + + foreach (AdminPermissionBridge::menuActionCodesForLegacy($adminRoleSlug) as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $roleId, + 'menu_action_id' => (int) $menuActionId, + ]); + } + } + } + + public function down(): void + { + // 不回滚授权数据,避免删除线上已经显式授予的角色管理权限。 + } +}; diff --git a/database/migrations/2026_05_19_122000_sync_player_permission_resource_bindings.php b/database/migrations/2026_05_19_122000_sync_player_permission_resource_bindings.php new file mode 100644 index 0000000..9ecf7ca --- /dev/null +++ b/database/migrations/2026_05_19_122000_sync_player_permission_resource_bindings.php @@ -0,0 +1,82 @@ +where('code', 'service.players')->value('id'); + $updateActionId = (int) DB::table('admin_action_catalog')->where('code', 'update')->value('id'); + + if ($playersMenuId > 0 && $updateActionId > 0) { + DB::table('admin_menu_actions')->updateOrInsert( + ['permission_code' => 'service.players.freeze'], + [ + 'menu_id' => $playersMenuId, + 'action_id' => $updateActionId, + 'name' => '冻结解冻玩家', + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + } + + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + + $playerResourceBindings = [ + 'admin.players.index' => ['service.players.manage', 'service.players.view'], + 'admin.players.store' => ['service.players.manage'], + 'admin.players.show' => ['service.players.manage', 'service.players.view'], + 'admin.players.update' => ['service.players.manage'], + 'admin.players.destroy' => ['service.players.manage'], + 'admin.players.freeze' => ['service.players.freeze'], + 'admin.players.unfreeze' => ['service.players.freeze'], + 'admin.players.wallets' => ['service.players.manage', 'service.wallet.view'], + 'admin.players.ticket-items' => ['service.players.manage', 'service.tickets.view'], + ]; + + foreach (AdminAuthorizationRegistry::resources() as $resource) { + if (($resource['module_code'] ?? null) !== 'player_service') { + continue; + } + + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + if ($resourceId === null) { + continue; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + $permissionCodes = $playerResourceBindings[$resource['code']] ?? $resource['permission_codes']; + foreach ($permissionCodes as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + public function down(): void + { + // 不回滚授权绑定,避免误删线上已调整的资源权限关系。 + } +}; diff --git a/database/migrations/2026_05_20_000001_add_admin_ticket_items_api_resource.php b/database/migrations/2026_05_20_000001_add_admin_ticket_items_api_resource.php new file mode 100644 index 0000000..a22f34f --- /dev/null +++ b/database/migrations/2026_05_20_000001_add_admin_ticket_items_api_resource.php @@ -0,0 +1,71 @@ +where('code', 'admin.tickets.index') + ->value('id'); + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId([ + 'code' => 'admin.tickets.index', + 'module_code' => 'ticket', + 'name' => '后台注单列表', + 'http_method' => 'GET', + 'uri_pattern' => '/api/v1/admin/tickets', + 'route_name' => 'api.v1.admin.tickets.index', + 'auth_mode' => 'permission_required', + 'is_audit_required' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $menuActionId = DB::table('admin_menu_actions') + ->where('permission_code', 'service.tickets.view') + ->value('id'); + + if ($menuActionId !== null) { + $exists = DB::table('admin_api_resource_bindings') + ->where('api_resource_id', $resourceId) + ->where('menu_action_id', $menuActionId) + ->exists(); + + if (! $exists) { + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => $resourceId, + 'menu_action_id' => $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + public function down(): void + { + $resourceId = DB::table('admin_api_resources') + ->where('code', 'admin.tickets.index') + ->value('id'); + + if ($resourceId !== null) { + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', $resourceId) + ->delete(); + + DB::table('admin_api_resources') + ->where('id', $resourceId) + ->delete(); + } + } +}; diff --git a/database/migrations/2026_05_21_000002_add_admin_currency_api_resources.php b/database/migrations/2026_05_21_000002_add_admin_currency_api_resources.php new file mode 100644 index 0000000..e6f01da --- /dev/null +++ b/database/migrations/2026_05_21_000002_add_admin_currency_api_resources.php @@ -0,0 +1,109 @@ +pluck('id', 'permission_code'); + + $resources = array_values(array_filter( + AdminAuthorizationRegistry::resources(), + static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.currencies.'), + )); + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $roleResourceRows = DB::table('admin_role_menu_actions as rma') + ->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id') + ->join('admin_api_resources as ar', 'ar.id', '=', 'arb.api_resource_id') + ->whereIn('ar.code', array_column($resources, 'code')) + ->select('rma.role_id', 'arb.api_resource_id') + ->distinct() + ->get(); + + foreach ($roleResourceRows as $row) { + DB::table('admin_role_api_resources')->updateOrInsert([ + 'role_id' => (int) $row->role_id, + 'api_resource_id' => (int) $row->api_resource_id, + ], []); + } + } + + public function down(): void + { + $resourceCodes = ['admin.currencies.index', 'admin.currencies.store', 'admin.currencies.update']; + + $resourceIds = DB::table('admin_api_resources') + ->whereIn('code', $resourceCodes) + ->pluck('id') + ->all(); + + if ($resourceIds === []) { + return; + } + + DB::table('admin_role_api_resources') + ->whereIn('api_resource_id', $resourceIds) + ->delete(); + + DB::table('admin_api_resource_bindings') + ->whereIn('api_resource_id', $resourceIds) + ->delete(); + + DB::table('admin_api_resources') + ->whereIn('id', $resourceIds) + ->delete(); + } +}; diff --git a/database/migrations/2026_05_21_093141_add_dimension_to_odds_items_table.php b/database/migrations/2026_05_21_093141_add_dimension_to_odds_items_table.php new file mode 100644 index 0000000..51f5fb1 --- /dev/null +++ b/database/migrations/2026_05_21_093141_add_dimension_to_odds_items_table.php @@ -0,0 +1,49 @@ +unsignedTinyInteger('dimension')->nullable()->after('prize_scope')->comment('2/3/4 维度,佣金按维度配置'); + + // 删除旧的唯一约束 + $table->dropUnique('uk_odds_items_version_play_prize_currency'); + + // 添加新的唯一约束:佣金按 dimension + currency_code 配置 + // 赔率仍按 play_code + prize_scope + currency_code 配置 + $table->unique( + ['version_id', 'play_code', 'prize_scope', 'currency_code', 'dimension'], + 'uk_odds_items_version_play_prize_currency_dimension' + ); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('odds_items', function (Blueprint $table) { + // 删除新的唯一约束 + $table->dropUnique('uk_odds_items_version_play_prize_currency_dimension'); + + // 恢复旧的唯一约束 + $table->unique( + ['version_id', 'play_code', 'prize_scope', 'currency_code'], + 'uk_odds_items_version_play_prize_currency' + ); + + // 删除 dimension 字段 + $table->dropColumn('dimension'); + }); + } +}; diff --git a/database/migrations/2026_05_21_150000_add_admin_currency_destroy_api_resource.php b/database/migrations/2026_05_21_150000_add_admin_currency_destroy_api_resource.php new file mode 100644 index 0000000..654a433 --- /dev/null +++ b/database/migrations/2026_05_21_150000_add_admin_currency_destroy_api_resource.php @@ -0,0 +1,105 @@ +pluck('id', 'permission_code'); + + $resource = collect(AdminAuthorizationRegistry::resources()) + ->first(static fn (array $item): bool => $item['code'] === 'admin.currencies.destroy'); + + if ($resource === null) { + return; + } + + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $roleResourceRows = DB::table('admin_role_menu_actions as rma') + ->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id') + ->where('arb.api_resource_id', (int) $resourceId) + ->select('rma.role_id') + ->distinct() + ->get(); + + foreach ($roleResourceRows as $row) { + DB::table('admin_role_api_resources')->updateOrInsert([ + 'role_id' => (int) $row->role_id, + 'api_resource_id' => (int) $resourceId, + ], []); + } + } + + public function down(): void + { + $resourceId = DB::table('admin_api_resources') + ->where('code', 'admin.currencies.destroy') + ->value('id'); + + if ($resourceId === null) { + return; + } + + DB::table('admin_role_api_resources') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->delete(); + } +}; diff --git a/database/migrations/2026_05_21_160000_add_currency_manage_legacy_permission.php b/database/migrations/2026_05_21_160000_add_currency_manage_legacy_permission.php new file mode 100644 index 0000000..613f6a5 --- /dev/null +++ b/database/migrations/2026_05_21_160000_add_currency_manage_legacy_permission.php @@ -0,0 +1,143 @@ +where('code', 'service')->value('id'); + $manageActionId = (int) DB::table('admin_action_catalog')->where('code', 'manage')->value('id'); + + if ($serviceMenuId > 0) { + DB::table('admin_menus')->updateOrInsert( + ['code' => 'service.currency'], + [ + 'parent_id' => $serviceMenuId, + 'menu_type' => 'page', + 'name' => '币种管理', + 'path' => '/admin/settings/currencies', + 'route_name' => 'admin.settings.currencies', + 'component' => 'settings/currencies', + 'icon' => null, + 'active_menu_code' => null, + 'sort_order' => 70, + 'is_visible' => false, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + } + + $currencyMenuId = (int) DB::table('admin_menus')->where('code', 'service.currency')->value('id'); + if ($currencyMenuId > 0 && $manageActionId > 0) { + DB::table('admin_menu_actions')->updateOrInsert( + ['permission_code' => 'service.currency.manage'], + [ + 'menu_id' => $currencyMenuId, + 'action_id' => $manageActionId, + 'name' => '币种管理', + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + } + + if (Schema::hasTable('admin_permissions')) { + DB::table('admin_permissions')->updateOrInsert( + ['slug' => 'prd.currency.manage'], + [ + 'name' => '币种管理·可管理', + 'updated_at' => $now, + 'created_at' => $now, + ], + ); + } + + $currencyActionId = DB::table('admin_menu_actions') + ->where('permission_code', 'service.currency.manage') + ->value('id'); + + if ($currencyActionId === null) { + return; + } + + $roleIds = DB::table('admin_role_legacy_permissions') + ->where('permission_slug', 'prd.users.manage') + ->pluck('role_id') + ->all(); + + foreach ($roleIds as $roleId) { + DB::table('admin_role_legacy_permissions')->updateOrInsert( + [ + 'role_id' => (int) $roleId, + 'permission_slug' => 'prd.currency.manage', + ], + [ + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + + DB::table('admin_role_menu_actions')->updateOrInsert( + [ + 'role_id' => (int) $roleId, + 'menu_action_id' => (int) $currencyActionId, + ], + [], + ); + } + + $currencyResourceIds = DB::table('admin_api_resources') + ->whereIn('code', [ + 'admin.currencies.index', + 'admin.currencies.store', + 'admin.currencies.update', + 'admin.currencies.destroy', + ]) + ->pluck('id') + ->all(); + + foreach ($currencyResourceIds as $resourceId) { + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $currencyActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $roleResourceRows = DB::table('admin_role_menu_actions as rma') + ->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id') + ->whereIn('arb.api_resource_id', $currencyResourceIds) + ->select('rma.role_id', 'arb.api_resource_id') + ->distinct() + ->get(); + + foreach ($roleResourceRows as $row) { + DB::table('admin_role_api_resources')->updateOrInsert([ + 'role_id' => (int) $row->role_id, + 'api_resource_id' => (int) $row->api_resource_id, + ], []); + } + } + + public function down(): void + { + // 不自动回滚线上角色与资源绑定,避免误删已调整的授权。 + } +}; diff --git a/database/migrations/2026_05_21_170000_move_currency_menu_to_top_level_route.php b/database/migrations/2026_05_21_170000_move_currency_menu_to_top_level_route.php new file mode 100644 index 0000000..e196fa6 --- /dev/null +++ b/database/migrations/2026_05_21_170000_move_currency_menu_to_top_level_route.php @@ -0,0 +1,29 @@ +where('code', 'service.currency') + ->update([ + 'path' => '/admin/currencies', + 'route_name' => 'admin.currencies', + 'updated_at' => now(), + ]); + } + + public function down(): void + { + DB::table('admin_menus') + ->where('code', 'service.currency') + ->update([ + 'path' => '/admin/settings/currencies', + 'route_name' => 'admin.settings.currencies', + 'updated_at' => now(), + ]); + } +}; diff --git a/database/migrations/2026_05_22_100000_add_admin_report_module.php b/database/migrations/2026_05_22_100000_add_admin_report_module.php new file mode 100644 index 0000000..b7f9894 --- /dev/null +++ b/database/migrations/2026_05_22_100000_add_admin_report_module.php @@ -0,0 +1,160 @@ +where('code', 'view')->value('id'); + if ($actionViewId === null) { + return; + } + + $serviceMenuId = DB::table('admin_menus')->where('code', 'service')->value('id'); + if ($serviceMenuId === null) { + return; + } + + $reportMenuId = DB::table('admin_menus')->where('code', 'service.report')->value('id'); + if ($reportMenuId === null) { + $reportMenuId = DB::table('admin_menus')->insertGetId([ + 'parent_id' => $serviceMenuId, + 'menu_type' => 'page', + 'code' => 'service.report', + 'name' => '报表中心', + 'path' => '/admin/reports', + 'route_name' => 'admin.reports.index', + 'component' => 'service/reports', + 'icon' => null, + 'active_menu_code' => null, + 'sort_order' => 50, + 'is_visible' => true, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + if (! isset($menuActionIds['service.report.view'])) { + DB::table('admin_menu_actions')->insert([ + 'menu_id' => (int) $reportMenuId, + 'action_id' => (int) $actionViewId, + 'permission_code' => 'service.report.view', + 'name' => '报表中心查看', + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + } + + $reportResourceCodes = [ + 'admin.reports.daily-profit', + 'admin.reports.player-win-loss', + 'admin.reports.play-dimension', + 'admin.reports.rebate-commission', + 'admin.report-jobs.index', + 'admin.report-jobs.store', + 'admin.report-jobs.show', + 'admin.report-jobs.download', + ]; + + foreach (AdminAuthorizationRegistry::resources() as $resource) { + if (! in_array($resource['code'], $reportResourceCodes, true)) { + continue; + } + + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id'); + if ($superRoleId !== null && isset($menuActionIds['service.report.view'])) { + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $superRoleId, + 'menu_action_id' => (int) $menuActionIds['service.report.view'], + ]); + } + + } + + public function down(): void + { + $codes = [ + 'admin.reports.daily-profit', + 'admin.reports.player-win-loss', + 'admin.reports.play-dimension', + 'admin.reports.rebate-commission', + 'admin.report-jobs.index', + 'admin.report-jobs.store', + 'admin.report-jobs.show', + 'admin.report-jobs.download', + ]; + + $resourceIds = DB::table('admin_api_resources')->whereIn('code', $codes)->pluck('id'); + foreach ($resourceIds as $resourceId) { + DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete(); + DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete(); + } + + $menuActionId = DB::table('admin_menu_actions')->where('permission_code', 'service.report.view')->value('id'); + if ($menuActionId !== null) { + DB::table('admin_role_menu_actions')->where('menu_action_id', (int) $menuActionId)->delete(); + DB::table('admin_menu_actions')->where('id', (int) $menuActionId)->delete(); + } + + DB::table('admin_menus')->where('code', 'service.report')->delete(); + } +}; diff --git a/database/migrations/2026_05_22_110000_fix_admin_report_authorization.php b/database/migrations/2026_05_22_110000_fix_admin_report_authorization.php new file mode 100644 index 0000000..9f9ff46 --- /dev/null +++ b/database/migrations/2026_05_22_110000_fix_admin_report_authorization.php @@ -0,0 +1,122 @@ + */ + private const STALE_RESOURCE_CODES = [ + 'admin.reports.index', + 'admin.reports.store', + 'admin.reconcile.index', + 'admin.reconcile.store', + 'admin.draws.publish', + ]; + + /** @var list */ + private const REPORT_RESOURCE_CODES = [ + 'admin.reports.daily-profit', + 'admin.reports.player-win-loss', + 'admin.reports.play-dimension', + 'admin.reports.rebate-commission', + 'admin.report-jobs.index', + 'admin.report-jobs.store', + 'admin.report-jobs.show', + 'admin.report-jobs.download', + ]; + + public function up(): void + { + $now = Carbon::now(); + + $this->deleteStaleApiResources(); + $this->ensureReportViewOnRolesWithReportLegacy(); + $this->syncReportResourceBindings($now); + } + + public function down(): void + { + // 绑定收紧与角色补权为数据修复,不回滚以免再现漂移。 + } + + private function deleteStaleApiResources(): void + { + $resourceIds = DB::table('admin_api_resources') + ->whereIn('code', self::STALE_RESOURCE_CODES) + ->pluck('id'); + + foreach ($resourceIds as $resourceId) { + $id = (int) $resourceId; + DB::table('admin_api_resource_bindings')->where('api_resource_id', $id)->delete(); + DB::table('admin_api_resources')->where('id', $id)->delete(); + } + } + + private function ensureReportViewOnRolesWithReportLegacy(): void + { + $menuActionId = DB::table('admin_menu_actions') + ->where('permission_code', 'service.report.view') + ->where('status', 1) + ->value('id'); + + if ($menuActionId === null) { + return; + } + + $reportSlugs = ['prd.report.view', 'prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']; + $roleIds = DB::table('admin_role_legacy_permissions') + ->whereIn('permission_slug', $reportSlugs) + ->distinct() + ->pluck('role_id'); + + foreach ($roleIds as $roleId) { + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $roleId, + 'menu_action_id' => (int) $menuActionId, + ]); + } + } + + private function syncReportResourceBindings(Carbon $now): void + { + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + $registryByCode = []; + foreach (AdminAuthorizationRegistry::resources() as $resource) { + $registryByCode[$resource['code']] = $resource; + } + + foreach (self::REPORT_RESOURCE_CODES as $code) { + $resource = $registryByCode[$code] ?? null; + if ($resource === null) { + continue; + } + + $resourceId = DB::table('admin_api_resources')->where('code', $code)->value('id'); + if ($resourceId === null) { + continue; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + +}; diff --git a/database/migrations/2026_05_22_120000_drop_redundant_admin_and_system_tables.php b/database/migrations/2026_05_22_120000_drop_redundant_admin_and_system_tables.php new file mode 100644 index 0000000..267f379 --- /dev/null +++ b/database/migrations/2026_05_22_120000_drop_redundant_admin_and_system_tables.php @@ -0,0 +1,29 @@ +pluck('id'); + + foreach ($roleIds as $roleId) { + $legacySlugs = DB::table('admin_role_legacy_permissions') + ->where('role_id', (int) $roleId) + ->pluck('permission_slug') + ->all(); + + $slugs = AdminPermissionBridge::normalizeCanonicalLegacySlugs( + is_array($legacySlugs) ? $legacySlugs : [], + ); + + if ($slugs === []) { + continue; + } + + $role = AdminRole::query()->find((int) $roleId); + if ($role !== null) { + $role->syncLegacyPermissionSlugs($slugs); + } + } + + Schema::dropIfExists('admin_role_legacy_permissions'); + } + + public function down(): void + { + // 单向清理:slug 已合并,权限以 admin_role_menu_actions 为准。 + } +}; diff --git a/database/migrations/2026_05_22_140000_add_frontend_play_rules_html_i18n_settings.php b/database/migrations/2026_05_22_140000_add_frontend_play_rules_html_i18n_settings.php new file mode 100644 index 0000000..f5df7fa --- /dev/null +++ b/database/migrations/2026_05_22_140000_add_frontend_play_rules_html_i18n_settings.php @@ -0,0 +1,50 @@ + '玩家端玩法规则页 HTML(中文)', + 'frontend.play_rules_html_en' => '玩家端玩法规则页 HTML(English)', + 'frontend.play_rules_html_ne' => '玩家端玩法规则页 HTML(नेपाली)', + ]; + + public function up(): void + { + $legacyRow = LotterySetting::query()->where('setting_key', self::LEGACY_KEY)->first(); + $legacyValue = $legacyRow?->value_json; + + foreach (self::I18N_KEYS as $key => $description) { + if (LotterySetting::query()->where('setting_key', $key)->exists()) { + continue; + } + + $value = ''; + if ($key === 'frontend.play_rules_html_zh' && is_string($legacyValue) && trim($legacyValue) !== '') { + $value = $legacyValue; + } + + LotterySetting::query()->create([ + 'setting_key' => $key, + 'value_json' => $value, + 'group_name' => 'frontend', + 'description_zh' => $description, + ]); + } + } + + public function down(): void + { + LotterySetting::query() + ->whereIn('setting_key', array_keys(self::I18N_KEYS)) + ->delete(); + } +}; diff --git a/database/migrations/2026_05_25_120001_consolidate_play_display_name_columns.php b/database/migrations/2026_05_25_120001_consolidate_play_display_name_columns.php new file mode 100644 index 0000000..300db05 --- /dev/null +++ b/database/migrations/2026_05_25_120001_consolidate_play_display_name_columns.php @@ -0,0 +1,54 @@ +string('display_name', 64)->nullable()->after('bet_mode'); + }); + + DB::table($table)->update([ + 'display_name' => DB::raw( + "COALESCE( + NULLIF(TRIM(display_name_zh), ''), + NULLIF(TRIM(display_name_en), ''), + NULLIF(TRIM(display_name_ne), ''), + play_code + )", + ), + ]); + + Schema::table($table, function (Blueprint $blueprint): void { + $blueprint->dropColumn(['display_name_zh', 'display_name_en', 'display_name_ne']); + }); + } + } + + public function down(): void + { + foreach (['play_types', 'play_config_items'] as $table) { + Schema::table($table, function (Blueprint $blueprint): void { + $blueprint->string('display_name_zh', 64)->nullable()->after('bet_mode'); + $blueprint->string('display_name_en', 64)->nullable()->after('display_name_zh'); + $blueprint->string('display_name_ne', 64)->nullable()->after('display_name_en'); + }); + + DB::table($table)->update([ + 'display_name_zh' => DB::raw('display_name'), + 'display_name_en' => DB::raw('display_name'), + 'display_name_ne' => DB::raw('display_name'), + ]); + + Schema::table($table, function (Blueprint $blueprint): void { + $blueprint->dropColumn('display_name'); + }); + } + } +}; diff --git a/database/migrations/2026_05_25_120002_expand_audit_logs_target_type.php b/database/migrations/2026_05_25_120002_expand_audit_logs_target_type.php new file mode 100644 index 0000000..f86a6e4 --- /dev/null +++ b/database/migrations/2026_05_25_120002_expand_audit_logs_target_type.php @@ -0,0 +1,25 @@ +string('target_type', 128)->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('audit_logs', function (Blueprint $table): void { + $table->string('target_type', 32)->nullable()->change(); + }); + } +}; diff --git a/database/migrations/2026_05_25_120003_refine_admin_permission_granularity.php b/database/migrations/2026_05_25_120003_refine_admin_permission_granularity.php new file mode 100644 index 0000000..96ec8e2 --- /dev/null +++ b/database/migrations/2026_05_25_120003_refine_admin_permission_granularity.php @@ -0,0 +1,62 @@ +pluck('id', 'code'); + $menuIds = DB::table('admin_menus')->pluck('id', 'code'); + + $walletMenuId = $menuIds['service.wallet'] ?? null; + $updateActionId = $actionIds['update'] ?? null; + if ($walletMenuId !== null && $updateActionId !== null) { + $exists = DB::table('admin_menu_actions') + ->where('permission_code', 'service.wallet.adjust') + ->exists(); + + if (! $exists) { + DB::table('admin_menu_actions')->insert([ + 'menu_id' => $walletMenuId, + 'action_id' => $updateActionId, + 'permission_code' => 'service.wallet.adjust', + 'name' => '钱包补单/冲正', + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $oddsMenuId = $menuIds['config.odds'] ?? null; + $viewActionId = $actionIds['view'] ?? null; + if ($oddsMenuId !== null && $viewActionId !== null) { + $exists = DB::table('admin_menu_actions') + ->where('permission_code', 'config.odds.view') + ->exists(); + + if (! $exists) { + DB::table('admin_menu_actions')->insert([ + 'menu_id' => $oddsMenuId, + 'action_id' => $viewActionId, + 'permission_code' => 'config.odds.view', + 'name' => '赔率配置查看', + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + public function down(): void + { + DB::table('admin_menu_actions') + ->whereIn('permission_code', ['service.wallet.adjust', 'config.odds.view']) + ->delete(); + } +}; diff --git a/database/migrations/2026_05_25_130000_remove_stale_admin_menu_actions.php b/database/migrations/2026_05_25_130000_remove_stale_admin_menu_actions.php new file mode 100644 index 0000000..5a55b0f --- /dev/null +++ b/database/migrations/2026_05_25_130000_remove_stale_admin_menu_actions.php @@ -0,0 +1,48 @@ + */ + private const STALE_PERMISSION_CODES = [ + 'dashboard.view', + 'service.reports.view', + 'service.reports.export', + ]; + + public function up(): void + { + $menuActionIds = DB::table('admin_menu_actions') + ->whereIn('permission_code', self::STALE_PERMISSION_CODES) + ->pluck('id'); + + if ($menuActionIds->isNotEmpty()) { + DB::table('admin_menu_actions') + ->whereIn('id', $menuActionIds->all()) + ->delete(); + } + + $staleReportMenuId = DB::table('admin_menus') + ->where('code', 'service.reports') + ->value('id'); + + if ($staleReportMenuId !== null) { + $hasActions = DB::table('admin_menu_actions') + ->where('menu_id', (int) $staleReportMenuId) + ->exists(); + + if (! $hasActions) { + DB::table('admin_menus') + ->where('id', (int) $staleReportMenuId) + ->delete(); + } + } + } + + public function down(): void + { + // 数据清理迁移,不回滚以免再现僵尸 permission_code。 + } +}; diff --git a/database/migrations/2026_05_25_140000_add_admin_dashboard_analytics_resource.php b/database/migrations/2026_05_25_140000_add_admin_dashboard_analytics_resource.php new file mode 100644 index 0000000..1641cb0 --- /dev/null +++ b/database/migrations/2026_05_25_140000_add_admin_dashboard_analytics_resource.php @@ -0,0 +1,87 @@ +firstWhere('code', 'admin.dashboard.analytics'); + + if ($resource === null) { + return; + } + + $resourceId = DB::table('admin_api_resources') + ->where('code', 'admin.dashboard.analytics') + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => 'admin.dashboard.analytics', + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $actionId = DB::table('admin_menu_actions') + ->where('permission_code', $permissionCode) + ->value('id'); + if ($actionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $actionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + public function down(): void + { + $resourceId = DB::table('admin_api_resources') + ->where('code', 'admin.dashboard.analytics') + ->value('id'); + + if ($resourceId === null) { + return; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->delete(); + } +}; diff --git a/database/migrations/2026_05_25_180000_add_settlement_batch_review_columns.php b/database/migrations/2026_05_25_180000_add_settlement_batch_review_columns.php new file mode 100644 index 0000000..3d0882e --- /dev/null +++ b/database/migrations/2026_05_25_180000_add_settlement_batch_review_columns.php @@ -0,0 +1,51 @@ +string('review_status', 32)->default('pending')->after('total_jackpot_payout_amount'); + } + if (! Schema::hasColumn('settlement_batches', 'reviewed_by')) { + $table->foreignId('reviewed_by')->nullable()->after('review_status')->constrained('admin_users')->nullOnDelete(); + } + if (! Schema::hasColumn('settlement_batches', 'reviewed_at')) { + $table->timestamp('reviewed_at')->nullable()->after('reviewed_by'); + } + if (! Schema::hasColumn('settlement_batches', 'review_remark')) { + $table->string('review_remark', 255)->nullable()->after('reviewed_at'); + } + if (! Schema::hasColumn('settlement_batches', 'paid_at')) { + $table->timestamp('paid_at')->nullable()->after('review_remark'); + } + }); + } + + public function down(): void + { + Schema::table('settlement_batches', function (Blueprint $table): void { + if (Schema::hasColumn('settlement_batches', 'paid_at')) { + $table->dropColumn('paid_at'); + } + if (Schema::hasColumn('settlement_batches', 'review_remark')) { + $table->dropColumn('review_remark'); + } + if (Schema::hasColumn('settlement_batches', 'reviewed_at')) { + $table->dropColumn('reviewed_at'); + } + if (Schema::hasColumn('settlement_batches', 'reviewed_by')) { + $table->dropForeign(['reviewed_by']); + $table->dropColumn('reviewed_by'); + } + if (Schema::hasColumn('settlement_batches', 'review_status')) { + $table->dropColumn('review_status'); + } + }); + } +}; diff --git a/database/migrations/2026_05_26_100000_expand_admin_permission_granularity.php b/database/migrations/2026_05_26_100000_expand_admin_permission_granularity.php new file mode 100644 index 0000000..cd36f62 --- /dev/null +++ b/database/migrations/2026_05_26_100000_expand_admin_permission_granularity.php @@ -0,0 +1,216 @@ + */ + private const NEW_MENU_ACTIONS = [ + ['menu_code' => 'dashboard', 'action_code' => 'view', 'permission_code' => 'dashboard.view', 'name' => '仪表盘查看'], + ['menu_code' => 'service.report', 'action_code' => 'export', 'permission_code' => 'service.report.export', 'name' => '报表导出'], + ['menu_code' => 'risk.monitor', 'action_code' => 'manage', 'permission_code' => 'risk.monitor.manage', 'name' => '风控监控管理'], + ]; + + public function up(): void + { + $now = Carbon::now(); + $menuIds = DB::table('admin_menus')->pluck('id', 'code'); + $actionIds = DB::table('admin_action_catalog')->pluck('id', 'code'); + + $menuActionIds = []; + foreach (self::NEW_MENU_ACTIONS as $row) { + $menuId = $menuIds[$row['menu_code']] ?? null; + $actionId = $actionIds[$row['action_code']] ?? null; + if ($menuId === null || $actionId === null) { + continue; + } + + $exists = DB::table('admin_menu_actions') + ->where('permission_code', $row['permission_code']) + ->exists(); + + if ($exists) { + $menuActionIds[$row['permission_code']] = (int) DB::table('admin_menu_actions') + ->where('permission_code', $row['permission_code']) + ->value('id'); + + continue; + } + + $menuActionIds[$row['permission_code']] = (int) DB::table('admin_menu_actions')->insertGetId([ + 'menu_id' => (int) $menuId, + 'action_id' => (int) $actionId, + 'permission_code' => $row['permission_code'], + 'name' => $row['name'], + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $this->grantMenuActionsToAllRoles($menuActionIds, $now); + $this->grantReportExportToReportViewRoles($menuActionIds['service.report.export'] ?? null, $now); + $this->grantTicketsViewToLegacyRoles($menuActionIds, $now); + } + + public function down(): void + { + $codes = array_column(self::NEW_MENU_ACTIONS, 'permission_code'); + $ids = DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->pluck('id'); + foreach ($ids as $id) { + DB::table('admin_role_menu_actions')->where('menu_action_id', (int) $id)->delete(); + DB::table('admin_user_menu_actions')->where('menu_action_id', (int) $id)->delete(); + DB::table('admin_api_resource_bindings')->where('menu_action_id', (int) $id)->delete(); + DB::table('admin_menu_actions')->where('id', (int) $id)->delete(); + } + } + + /** + * @param array $menuActionIds + */ + private function grantMenuActionsToAllRoles(array $menuActionIds, Carbon $now): void + { + $dashboardId = $menuActionIds['dashboard.view'] ?? null; + if ($dashboardId === null) { + return; + } + + $roleIds = DB::table('admin_roles')->pluck('id'); + foreach ($roleIds as $roleId) { + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $roleId, + 'menu_action_id' => $dashboardId, + ]); + } + } + + private function grantReportExportToReportViewRoles(?int $exportMenuActionId, Carbon $now): void + { + if ($exportMenuActionId === null) { + return; + } + + $viewMenuActionId = DB::table('admin_menu_actions') + ->where('permission_code', 'service.report.view') + ->value('id'); + + if ($viewMenuActionId === null) { + return; + } + + $roleIds = DB::table('admin_role_menu_actions') + ->where('menu_action_id', (int) $viewMenuActionId) + ->distinct() + ->pluck('role_id'); + + foreach ($roleIds as $roleId) { + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $roleId, + 'menu_action_id' => $exportMenuActionId, + ]); + } + } + + /** + * 原注单入口依赖多种 prd.*;迁移为独立的 prd.tickets.view。 + * + * @param array $menuActionIds + */ + private function grantTicketsViewToLegacyRoles(array $menuActionIds, Carbon $now): void + { + $ticketsViewId = DB::table('admin_menu_actions') + ->where('permission_code', 'service.tickets.view') + ->value('id'); + + if ($ticketsViewId === null) { + return; + } + + $legacySlugs = [ + 'prd.users.view_cs', + 'prd.users.manage', + 'prd.users.view_finance', + 'prd.draw_result.view', + 'prd.draw_result.manage', + 'prd.payout.view', + 'prd.payout.review', + 'prd.payout.manage', + ]; + + $roleIds = $this->roleIdsWithAnyLegacySlug($legacySlugs); + + foreach ($roleIds as $roleId) { + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $roleId, + 'menu_action_id' => (int) $ticketsViewId, + ]); + } + + $riskViewId = DB::table('admin_menu_actions') + ->where('permission_code', 'risk.monitor.view') + ->value('id'); + $riskManageId = $menuActionIds['risk.monitor.manage'] ?? null; + + if ($riskManageId === null) { + return; + } + + $riskRoleIds = $this->roleIdsWithAnyLegacySlug([ + 'prd.draw_result.manage', + 'prd.draw_result.view', + 'prd.risk.manage', + 'prd.risk.view', + ]); + + foreach ($riskRoleIds as $roleId) { + if ($riskViewId !== null) { + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $roleId, + 'menu_action_id' => (int) $riskViewId, + ]); + } + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $roleId, + 'menu_action_id' => (int) $riskManageId, + ]); + } + } + + /** + * 通过角色已授权的 menu_action 反推曾拥有指定 prd.* 的角色(legacy 表已废弃)。 + * + * @param list $legacySlugs + * @return list + */ + private function roleIdsWithAnyLegacySlug(array $legacySlugs): array + { + $codes = []; + foreach ($legacySlugs as $slug) { + $codes = array_merge($codes, AdminPermissionBridge::menuActionCodesForLegacy($slug)); + } + $codes = array_values(array_unique($codes)); + + if ($codes === []) { + return []; + } + + $menuActionIds = DB::table('admin_menu_actions') + ->whereIn('permission_code', $codes) + ->where('status', 1) + ->pluck('id'); + + if ($menuActionIds->isEmpty()) { + return []; + } + + return DB::table('admin_role_menu_actions') + ->whereIn('menu_action_id', $menuActionIds->map(fn ($id) => (int) $id)->all()) + ->distinct() + ->pluck('role_id') + ->map(fn ($id) => (int) $id) + ->all(); + } +}; diff --git a/database/migrations/2026_05_26_120000_add_unique_client_trace_to_ticket_orders.php b/database/migrations/2026_05_26_120000_add_unique_client_trace_to_ticket_orders.php new file mode 100644 index 0000000..0885f2f --- /dev/null +++ b/database/migrations/2026_05_26_120000_add_unique_client_trace_to_ticket_orders.php @@ -0,0 +1,25 @@ +unique( + ['player_id', 'draw_id', 'client_trace_id'], + 'uniq_ticket_orders_player_draw_trace', + ); + }); + } + + public function down(): void + { + Schema::table('ticket_orders', function (Blueprint $table): void { + $table->dropUnique('uniq_ticket_orders_player_draw_trace'); + }); + } +}; diff --git a/database/migrations/2026_05_27_100000_add_jackpot_manual_burst_permission.php b/database/migrations/2026_05_27_100000_add_jackpot_manual_burst_permission.php new file mode 100644 index 0000000..123410c --- /dev/null +++ b/database/migrations/2026_05_27_100000_add_jackpot_manual_burst_permission.php @@ -0,0 +1,112 @@ +where('code', 'manual_burst')->exists()) { + DB::table('admin_action_catalog')->insert([ + 'code' => 'manual_burst', + 'name' => '手动爆池', + 'sort_order' => 85, + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $jackpotMenuId = (int) DB::table('admin_menus')->where('code', 'config.jackpot')->value('id'); + $manualBurstActionId = (int) DB::table('admin_action_catalog')->where('code', 'manual_burst')->value('id'); + + if ($jackpotMenuId <= 0 || $manualBurstActionId <= 0) { + return; + } + + DB::table('admin_menu_actions')->updateOrInsert( + ['permission_code' => 'jackpot.pool.manual_burst'], + [ + 'menu_id' => $jackpotMenuId, + 'action_id' => $manualBurstActionId, + 'name' => 'Jackpot 手动爆池', + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + + if (Schema::hasTable('admin_permissions')) { + DB::table('admin_permissions')->updateOrInsert( + ['slug' => 'prd.jackpot.manual_burst'], + [ + 'name' => 'Jackpot 手动爆池·仅超管', + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + } + + $menuActionId = (int) DB::table('admin_menu_actions') + ->where('permission_code', 'jackpot.pool.manual_burst') + ->value('id'); + + $superRoleId = (int) DB::table('admin_roles')->where('slug', 'super_admin')->value('id'); + if ($superRoleId > 0 && $menuActionId > 0) { + if (Schema::hasTable('admin_role_legacy_permissions')) { + DB::table('admin_role_legacy_permissions')->updateOrInsert( + [ + 'role_id' => $superRoleId, + 'permission_slug' => 'prd.jackpot.manual_burst', + ], + [ + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + } + + DB::table('admin_role_menu_actions')->updateOrInsert( + [ + 'role_id' => $superRoleId, + 'menu_action_id' => $menuActionId, + ], + [], + ); + } + + $resourceId = DB::table('admin_api_resources') + ->where('code', 'admin.jackpot.pools.manual-burst') + ->value('id'); + + if ($resourceId !== null && $menuActionId > 0) { + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + if ($superRoleId > 0 && Schema::hasTable('admin_role_api_resources')) { + DB::table('admin_role_api_resources')->updateOrInsert([ + 'role_id' => $superRoleId, + 'api_resource_id' => (int) $resourceId, + ], []); + } + } + } + + public function down(): void + { + // 避免误删线上已调整的授权绑定。 + } +}; diff --git a/database/migrations/2026_05_27_140000_add_integration_fields_to_admin_sites.php b/database/migrations/2026_05_27_140000_add_integration_fields_to_admin_sites.php new file mode 100644 index 0000000..355b446 --- /dev/null +++ b/database/migrations/2026_05_27_140000_add_integration_fields_to_admin_sites.php @@ -0,0 +1,237 @@ +string('wallet_api_url', 512)->nullable()->after('extra_json'); + $table->string('wallet_debit_path', 128)->default('/wallet/debit-for-lottery')->after('wallet_api_url'); + $table->string('wallet_credit_path', 128)->default('/wallet/credit-from-lottery')->after('wallet_debit_path'); + $table->string('wallet_balance_path', 128)->default('/wallet/balance')->after('wallet_credit_path'); + $table->text('wallet_api_key_encrypted')->nullable()->after('wallet_balance_path'); + $table->text('sso_jwt_secret_encrypted')->nullable()->after('wallet_api_key_encrypted'); + $table->unsignedSmallInteger('wallet_timeout_seconds')->default(10)->after('sso_jwt_secret_encrypted'); + $table->json('iframe_allowed_origins')->nullable()->after('wallet_timeout_seconds'); + $table->string('lottery_h5_base_url', 512)->nullable()->after('iframe_allowed_origins'); + $table->text('notes')->nullable()->after('lottery_h5_base_url'); + }); + + $this->seedIntegrationMenuActions(); + $this->backfillDefaultSiteFromEnv(); + $this->syncIntegrationApiResources(); + } + + public function down(): void + { + $resourceIds = DB::table('admin_api_resources') + ->where('code', 'like', 'admin.integration-sites.%') + ->pluck('id') + ->all(); + + if ($resourceIds !== []) { + DB::table('admin_role_api_resources')->whereIn('api_resource_id', $resourceIds)->delete(); + DB::table('admin_api_resource_bindings')->whereIn('api_resource_id', $resourceIds)->delete(); + DB::table('admin_api_resources')->whereIn('id', $resourceIds)->delete(); + } + + Schema::table('admin_sites', function (Blueprint $table): void { + $table->dropColumn([ + 'wallet_api_url', + 'wallet_debit_path', + 'wallet_credit_path', + 'wallet_balance_path', + 'wallet_api_key_encrypted', + 'sso_jwt_secret_encrypted', + 'wallet_timeout_seconds', + 'iframe_allowed_origins', + 'lottery_h5_base_url', + 'notes', + ]); + }); + } + + private function seedIntegrationMenuActions(): void + { + $now = Carbon::now(); + $viewActionId = DB::table('admin_action_catalog')->where('code', 'view')->value('id'); + $manageActionId = DB::table('admin_action_catalog')->where('code', 'manage')->value('id'); + if ($viewActionId === null || $manageActionId === null) { + return; + } + + $configMenuId = DB::table('admin_menus')->where('code', 'config')->value('id'); + if ($configMenuId === null) { + return; + } + + $integrationMenuId = DB::table('admin_menus')->where('code', 'config.integration')->value('id'); + if ($integrationMenuId === null) { + $integrationMenuId = DB::table('admin_menus')->insertGetId([ + 'parent_id' => (int) $configMenuId, + 'menu_type' => 'page', + 'code' => 'config.integration', + 'name' => '主站接入站点', + 'path' => '/admin/config/integration-sites', + 'route_name' => 'admin.config.integration', + 'component' => 'config/integration', + 'icon' => null, + 'active_menu_code' => null, + 'sort_order' => 45, + 'is_visible' => true, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + foreach ([ + ['permission_code' => 'integration.site.view', 'action_id' => (int) $viewActionId, 'name' => '接入站点查看'], + ['permission_code' => 'integration.site.manage', 'action_id' => (int) $manageActionId, 'name' => '接入站点管理'], + ] as $row) { + $exists = DB::table('admin_menu_actions') + ->where('permission_code', $row['permission_code']) + ->exists(); + if ($exists) { + continue; + } + + DB::table('admin_menu_actions')->insert([ + 'menu_id' => (int) $integrationMenuId, + 'action_id' => $row['action_id'], + 'permission_code' => $row['permission_code'], + 'name' => $row['name'], + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + private function backfillDefaultSiteFromEnv(): void + { + $siteId = DB::table('admin_sites')->where('is_default', true)->value('id') + ?? DB::table('admin_sites')->orderBy('id')->value('id'); + + if ($siteId === null) { + return; + } + + $walletUrl = env('MAIN_SITE_WALLET_API_URL'); + $ssoSecret = env('MAIN_SITE_SSO_JWT_SECRET'); + $walletKey = env('MAIN_SITE_WALLET_API_KEY'); + + $payload = [ + 'updated_at' => Carbon::now(), + ]; + + if (is_string($walletUrl) && trim($walletUrl) !== '') { + $payload['wallet_api_url'] = rtrim(trim($walletUrl), '/'); + } + + $debitPath = env('MAIN_SITE_WALLET_DEBIT_PATH'); + if (is_string($debitPath) && $debitPath !== '') { + $payload['wallet_debit_path'] = $debitPath; + } + + $creditPath = env('MAIN_SITE_WALLET_CREDIT_PATH'); + if (is_string($creditPath) && $creditPath !== '') { + $payload['wallet_credit_path'] = $creditPath; + } + + $balancePath = env('MAIN_SITE_WALLET_BALANCE_PATH'); + if (is_string($balancePath) && $balancePath !== '') { + $payload['wallet_balance_path'] = $balancePath; + } + + $timeout = env('MAIN_SITE_WALLET_TIMEOUT'); + if (is_numeric($timeout)) { + $payload['wallet_timeout_seconds'] = max(1, (int) $timeout); + } + + if (is_string($ssoSecret) && $ssoSecret !== '') { + $payload['sso_jwt_secret_encrypted'] = encrypt($ssoSecret); + } + + if (is_string($walletKey) && $walletKey !== '') { + $payload['wallet_api_key_encrypted'] = encrypt($walletKey); + } + + if (count($payload) > 1) { + DB::table('admin_sites')->where('id', (int) $siteId)->update($payload); + } + } + + private function syncIntegrationApiResources(): void + { + if (! Schema::hasTable('admin_api_resources')) { + return; + } + + $now = Carbon::now(); + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + + $resources = array_values(array_filter( + AdminAuthorizationRegistry::resources(), + static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.integration-sites.'), + )); + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } +}; diff --git a/database/migrations/2026_05_27_140001_seed_integration_menu_actions.php b/database/migrations/2026_05_27_140001_seed_integration_menu_actions.php new file mode 100644 index 0000000..a394bb6 --- /dev/null +++ b/database/migrations/2026_05_27_140001_seed_integration_menu_actions.php @@ -0,0 +1,105 @@ +where('code', 'view')->value('id'); + $manageActionId = DB::table('admin_action_catalog')->where('code', 'manage')->value('id'); + if ($viewActionId === null || $manageActionId === null) { + return; + } + + $configMenuId = DB::table('admin_menus')->where('code', 'config')->value('id'); + if ($configMenuId === null) { + return; + } + + $integrationMenuId = DB::table('admin_menus')->where('code', 'config.integration')->value('id'); + if ($integrationMenuId === null) { + $integrationMenuId = DB::table('admin_menus')->insertGetId([ + 'parent_id' => (int) $configMenuId, + 'menu_type' => 'page', + 'code' => 'config.integration', + 'name' => '主站接入站点', + 'path' => '/admin/config/integration-sites', + 'route_name' => 'admin.config.integration', + 'component' => 'config/integration', + 'icon' => null, + 'active_menu_code' => null, + 'sort_order' => 45, + 'is_visible' => true, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + foreach ([ + ['permission_code' => 'integration.site.view', 'action_id' => (int) $viewActionId, 'name' => '接入站点查看'], + ['permission_code' => 'integration.site.manage', 'action_id' => (int) $manageActionId, 'name' => '接入站点管理'], + ] as $row) { + if (DB::table('admin_menu_actions')->where('permission_code', $row['permission_code'])->exists()) { + continue; + } + + DB::table('admin_menu_actions')->insert([ + 'menu_id' => (int) $integrationMenuId, + 'action_id' => $row['action_id'], + 'permission_code' => $row['permission_code'], + 'name' => $row['name'], + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + $resources = array_values(array_filter( + AdminAuthorizationRegistry::resources(), + static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.integration-sites.'), + )); + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id'); + if ($resourceId === null) { + continue; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + public function down(): void + { + // 保留 menu_actions / bindings,避免回滚后超管无法管理已创建的接入站点。 + } +}; diff --git a/database/migrations/2026_05_28_100000_resync_admin_api_resources_after_dashboard_view.php b/database/migrations/2026_05_28_100000_resync_admin_api_resources_after_dashboard_view.php new file mode 100644 index 0000000..08f2577 --- /dev/null +++ b/database/migrations/2026_05_28_100000_resync_admin_api_resources_after_dashboard_view.php @@ -0,0 +1,72 @@ +pluck('id', 'permission_code'); + + foreach (AdminAuthorizationRegistry::resources() as $resource) { + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + public function down(): void + { + // 数据修复迁移:不在 down 中回滚 bindings,避免误删线上授权关系。 + } +}; diff --git a/database/migrations/2026_05_29_100000_add_unique_ticket_item_id_to_jackpot_contributions.php b/database/migrations/2026_05_29_100000_add_unique_ticket_item_id_to_jackpot_contributions.php new file mode 100644 index 0000000..19dd2a6 --- /dev/null +++ b/database/migrations/2026_05_29_100000_add_unique_ticket_item_id_to_jackpot_contributions.php @@ -0,0 +1,41 @@ +select('ticket_item_id') + ->whereNotNull('ticket_item_id') + ->groupBy('ticket_item_id') + ->havingRaw('count(*) > 1') + ->pluck('ticket_item_id'); + + foreach ($duplicateIds as $ticketItemId) { + $rows = DB::table('jackpot_contributions') + ->where('ticket_item_id', $ticketItemId) + ->orderByDesc('id') + ->pluck('id'); + $keep = $rows->shift(); + if ($keep !== null && $rows->isNotEmpty()) { + DB::table('jackpot_contributions')->whereIn('id', $rows->all())->delete(); + } + } + + Schema::table('jackpot_contributions', function (Blueprint $table): void { + $table->unique('ticket_item_id', 'uk_jackpot_contributions_ticket_item'); + }); + } + + public function down(): void + { + Schema::table('jackpot_contributions', function (Blueprint $table): void { + $table->dropUnique('uk_jackpot_contributions_ticket_item'); + }); + } +}; diff --git a/database/migrations/2026_05_30_100000_create_jackpot_pool_adjustments_table.php b/database/migrations/2026_05_30_100000_create_jackpot_pool_adjustments_table.php new file mode 100644 index 0000000..bd98382 --- /dev/null +++ b/database/migrations/2026_05_30_100000_create_jackpot_pool_adjustments_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('adjustment_no', 32)->unique(); + $table->foreignId('jackpot_pool_id')->constrained('jackpot_pools')->cascadeOnDelete(); + $table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete(); + $table->bigInteger('amount_delta')->comment('signed minor units; + increase pool'); + $table->bigInteger('balance_before'); + $table->bigInteger('balance_after'); + $table->string('reason', 500); + $table->timestamps(); + + $table->index(['jackpot_pool_id', 'created_at'], 'idx_jackpot_pool_adjustments_pool_created'); + }); + } + + public function down(): void + { + Schema::dropIfExists('jackpot_pool_adjustments'); + } +}; diff --git a/database/migrations/2026_05_30_100001_add_jackpot_pool_adjustment_api_resources.php b/database/migrations/2026_05_30_100001_add_jackpot_pool_adjustment_api_resources.php new file mode 100644 index 0000000..2276798 --- /dev/null +++ b/database/migrations/2026_05_30_100001_add_jackpot_pool_adjustment_api_resources.php @@ -0,0 +1,105 @@ + */ + private const RESOURCE_CODES = [ + 'admin.jackpot.pools.adjustments.index', + 'admin.jackpot.pools.adjustments.store', + ]; + + public function up(): void + { + $now = Carbon::now(); + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + $resources = collect(AdminAuthorizationRegistry::resources()) + ->filter(fn (array $item): bool => in_array($item['code'], self::RESOURCE_CODES, true)) + ->values(); + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $roleResourceRows = DB::table('admin_role_menu_actions as rma') + ->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id') + ->where('arb.api_resource_id', (int) $resourceId) + ->select('rma.role_id') + ->distinct() + ->get(); + + if (Schema::hasTable('admin_role_api_resources')) { + foreach ($roleResourceRows as $row) { + DB::table('admin_role_api_resources')->updateOrInsert([ + 'role_id' => (int) $row->role_id, + 'api_resource_id' => (int) $resourceId, + ], []); + } + } + } + } + + public function down(): void + { + foreach (self::RESOURCE_CODES as $code) { + $resourceId = DB::table('admin_api_resources')->where('code', $code)->value('id'); + if ($resourceId === null) { + continue; + } + + if (Schema::hasTable('admin_role_api_resources')) { + DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete(); + } + DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete(); + DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete(); + } + } +}; diff --git a/database/migrations/2026_05_31_100000_add_query_performance_indexes.php b/database/migrations/2026_05_31_100000_add_query_performance_indexes.php new file mode 100644 index 0000000..09635c1 --- /dev/null +++ b/database/migrations/2026_05_31_100000_add_query_performance_indexes.php @@ -0,0 +1,53 @@ +index('draw_id', 'idx_ticket_orders_draw_id'); + }); + + Schema::table('wallet_txns', function (Blueprint $table): void { + $table->index(['player_id', 'id'], 'idx_wallet_txns_player_id'); + }); + + Schema::table('ticket_items', function (Blueprint $table): void { + $table->index(['player_id', 'id'], 'idx_ticket_items_player_id'); + }); + + Schema::table('draws', function (Blueprint $table): void { + $table->index(['business_date', 'draw_time'], 'idx_draws_business_date_draw_time'); + }); + } + + public function down(): void + { + Schema::table('draws', function (Blueprint $table): void { + $table->dropIndex('idx_draws_business_date_draw_time'); + }); + + Schema::table('ticket_items', function (Blueprint $table): void { + $table->dropIndex('idx_ticket_items_player_id'); + }); + + Schema::table('wallet_txns', function (Blueprint $table): void { + $table->dropIndex('idx_wallet_txns_player_id'); + }); + + Schema::table('ticket_orders', function (Blueprint $table): void { + $table->dropIndex('idx_ticket_orders_draw_id'); + }); + } +}; diff --git a/database/migrations/2026_06_01_100000_add_admin_settings_batch_update_api_resource.php b/database/migrations/2026_06_01_100000_add_admin_settings_batch_update_api_resource.php new file mode 100644 index 0000000..a0737ae --- /dev/null +++ b/database/migrations/2026_06_01_100000_add_admin_settings_batch_update_api_resource.php @@ -0,0 +1,106 @@ +first(fn (array $item): bool => $item['code'] === self::RESOURCE_CODE); + + if ($resource === null) { + return; + } + + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + $sourceResourceId = DB::table('admin_api_resources') + ->where('code', self::CLONE_BINDINGS_FROM) + ->value('id'); + + if ($sourceResourceId !== null) { + $bindings = DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $sourceResourceId) + ->get(['menu_action_id']); + + foreach ($bindings as $binding) { + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $binding->menu_action_id, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + if (Schema::hasTable('admin_role_api_resources')) { + $roleResourceRows = DB::table('admin_role_menu_actions as rma') + ->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id') + ->where('arb.api_resource_id', (int) $resourceId) + ->select('rma.role_id') + ->distinct() + ->get(); + + foreach ($roleResourceRows as $row) { + DB::table('admin_role_api_resources')->updateOrInsert([ + 'role_id' => (int) $row->role_id, + 'api_resource_id' => (int) $resourceId, + ], []); + } + } + } + + public function down(): void + { + $resourceId = DB::table('admin_api_resources')->where('code', self::RESOURCE_CODE)->value('id'); + if ($resourceId === null) { + return; + } + + if (Schema::hasTable('admin_role_api_resources')) { + DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete(); + } + DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete(); + DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete(); + } +}; diff --git a/database/migrations/2026_06_02_100000_create_agent_hierarchy_tables.php b/database/migrations/2026_06_02_100000_create_agent_hierarchy_tables.php new file mode 100644 index 0000000..d416614 --- /dev/null +++ b/database/migrations/2026_06_02_100000_create_agent_hierarchy_tables.php @@ -0,0 +1,132 @@ +id(); + $table->foreignId('admin_site_id')->constrained('admin_sites')->cascadeOnDelete(); + $table->foreignId('parent_id')->nullable()->constrained('agent_nodes')->nullOnDelete(); + $table->string('path', 512); + $table->unsignedSmallInteger('depth')->default(0); + $table->string('code', 64); + $table->string('name', 128); + $table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled'); + $table->foreignId('created_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->json('extra_json')->nullable(); + $table->timestamps(); + + $table->unique(['admin_site_id', 'code'], 'uk_agent_nodes_site_code'); + $table->index(['admin_site_id', 'parent_id'], 'idx_agent_nodes_site_parent'); + $table->index('path', 'idx_agent_nodes_path'); + }); + + Schema::create('admin_user_agents', function (Blueprint $table): void { + $table->foreignId('admin_user_id')->primary()->constrained('admin_users')->cascadeOnDelete(); + $table->foreignId('agent_node_id')->constrained('agent_nodes')->cascadeOnDelete(); + $table->boolean('is_primary')->default(true); + $table->timestamp('granted_at')->nullable(); + }); + + $this->seedRootAgentNodes(); + $this->backfillAdminUserAgents(); + } + + public function down(): void + { + Schema::dropIfExists('admin_user_agents'); + Schema::dropIfExists('agent_nodes'); + } + + private function seedRootAgentNodes(): void + { + $now = Carbon::now(); + $sites = DB::table('admin_sites')->orderBy('id')->get(['id', 'code', 'name']); + + foreach ($sites as $site) { + if (DB::table('agent_nodes')->where('admin_site_id', (int) $site->id)->where('depth', 0)->exists()) { + continue; + } + + $code = 'root-'.(string) $site->code; + $nodeId = DB::table('agent_nodes')->insertGetId([ + 'admin_site_id' => (int) $site->id, + 'parent_id' => null, + 'path' => '/', + 'depth' => 0, + 'code' => $code, + 'name' => (string) $site->name, + 'status' => 1, + 'created_by' => null, + 'extra_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + DB::table('agent_nodes')->where('id', $nodeId)->update([ + 'path' => '/'.$nodeId.'/', + ]); + } + } + + private function backfillAdminUserAgents(): void + { + $superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id'); + $now = Carbon::now(); + + $userIds = DB::table('admin_users')->pluck('id'); + foreach ($userIds as $userId) { + $userId = (int) $userId; + if (DB::table('admin_user_agents')->where('admin_user_id', $userId)->exists()) { + continue; + } + + if ($superRoleId !== null) { + $isSuper = DB::table('admin_user_site_roles') + ->where('admin_user_id', $userId) + ->where('role_id', (int) $superRoleId) + ->exists(); + if ($isSuper) { + continue; + } + } + + $siteId = DB::table('admin_user_site_roles') + ->where('admin_user_id', $userId) + ->orderBy('site_id') + ->value('site_id'); + + if ($siteId === null) { + $siteId = DB::table('admin_sites')->where('is_default', true)->value('id') + ?? DB::table('admin_sites')->orderBy('id')->value('id'); + } + + if ($siteId === null) { + continue; + } + + $rootId = DB::table('agent_nodes') + ->where('admin_site_id', (int) $siteId) + ->where('depth', 0) + ->value('id'); + + if ($rootId === null) { + continue; + } + + DB::table('admin_user_agents')->insert([ + 'admin_user_id' => $userId, + 'agent_node_id' => (int) $rootId, + 'is_primary' => true, + 'granted_at' => $now, + ]); + } + } +}; diff --git a/database/migrations/2026_06_02_100001_seed_agent_node_permissions.php b/database/migrations/2026_06_02_100001_seed_agent_node_permissions.php new file mode 100644 index 0000000..99a2e61 --- /dev/null +++ b/database/migrations/2026_06_02_100001_seed_agent_node_permissions.php @@ -0,0 +1,195 @@ +where('code', 'view')->value('id'); + $manageActionId = DB::table('admin_action_catalog')->where('code', 'manage')->value('id'); + if ($viewActionId === null || $manageActionId === null) { + return; + } + + $systemMenuId = DB::table('admin_menus')->where('code', 'system')->value('id'); + if ($systemMenuId === null) { + $systemMenuId = DB::table('admin_menus')->insertGetId([ + 'parent_id' => null, + 'menu_type' => 'directory', + 'code' => 'system', + 'name' => '系统', + 'path' => null, + 'route_name' => null, + 'component' => null, + 'icon' => null, + 'active_menu_code' => null, + 'sort_order' => 90, + 'is_visible' => true, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $agentMenuId = DB::table('admin_menus')->where('code', 'system.agents')->value('id'); + if ($agentMenuId === null) { + $agentMenuId = DB::table('admin_menus')->insertGetId([ + 'parent_id' => (int) $systemMenuId, + 'menu_type' => 'page', + 'code' => 'system.agents', + 'name' => '代理管理', + 'path' => '/admin/agents', + 'route_name' => 'admin.agents', + 'component' => 'agents/index', + 'icon' => null, + 'active_menu_code' => null, + 'sort_order' => 25, + 'is_visible' => true, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + foreach ([ + ['permission_code' => 'agent.node.view', 'action_id' => (int) $viewActionId, 'name' => '代理节点查看'], + ['permission_code' => 'agent.node.manage', 'action_id' => (int) $manageActionId, 'name' => '代理节点管理'], + ] as $row) { + if (DB::table('admin_menu_actions')->where('permission_code', $row['permission_code'])->exists()) { + continue; + } + + DB::table('admin_menu_actions')->insert([ + 'menu_id' => (int) $agentMenuId, + 'action_id' => $row['action_id'], + 'permission_code' => $row['permission_code'], + 'name' => $row['name'], + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + $resources = array_values(array_filter( + AdminAuthorizationRegistry::resources(), + static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.agent-nodes.'), + )); + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id'); + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources')->where('id', (int) $resourceId)->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + $permissionCodes = $resource['permission_codes'] ?? []; + foreach ($permissionCodes as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + if (! Schema::hasTable('admin_role_api_resources')) { + return; + } + + $superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id'); + if ($superRoleId === null) { + return; + } + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id'); + if ($resourceId === null) { + continue; + } + + DB::table('admin_role_api_resources')->updateOrInsert([ + 'role_id' => (int) $superRoleId, + 'api_resource_id' => (int) $resourceId, + ], []); + } + + $menuActionIdList = DB::table('admin_menu_actions') + ->whereIn('permission_code', ['agent.node.view', 'agent.node.manage']) + ->pluck('id'); + + foreach ($menuActionIdList as $menuActionId) { + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $superRoleId, + 'menu_action_id' => (int) $menuActionId, + ], []); + } + } + + public function down(): void + { + $codes = [ + 'admin.agent-nodes.tree', + 'admin.agent-nodes.store', + 'admin.agent-nodes.show', + 'admin.agent-nodes.update', + 'admin.agent-nodes.children', + ]; + + foreach ($codes as $code) { + $resourceId = DB::table('admin_api_resources')->where('code', $code)->value('id'); + if ($resourceId === null) { + continue; + } + + if (Schema::hasTable('admin_role_api_resources')) { + DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete(); + } + DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete(); + DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete(); + } + + DB::table('admin_menu_actions') + ->whereIn('permission_code', ['agent.node.view', 'agent.node.manage']) + ->delete(); + } +}; diff --git a/database/migrations/2026_06_02_110000_agent_scoped_roles_and_player_agent.php b/database/migrations/2026_06_02_110000_agent_scoped_roles_and_player_agent.php new file mode 100644 index 0000000..9599933 --- /dev/null +++ b/database/migrations/2026_06_02_110000_agent_scoped_roles_and_player_agent.php @@ -0,0 +1,78 @@ +foreignId('owner_agent_id')->nullable()->after('sort_order')->constrained('agent_nodes')->nullOnDelete(); + $table->foreignId('delegated_from_role_id')->nullable()->after('owner_agent_id')->constrained('admin_roles')->nullOnDelete(); + $table->string('scope_type', 16)->default('system')->after('delegated_from_role_id'); + }); + + Schema::create('admin_user_agent_roles', function (Blueprint $table): void { + $table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete(); + $table->foreignId('agent_node_id')->constrained('agent_nodes')->cascadeOnDelete(); + $table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete(); + $table->timestamp('granted_at')->nullable(); + $table->primary(['admin_user_id', 'agent_node_id', 'role_id'], 'pk_admin_user_agent_roles'); + }); + + Schema::table('players', function (Blueprint $table): void { + $table->foreignId('agent_node_id')->nullable()->after('site_code')->constrained('agent_nodes')->nullOnDelete(); + $table->index(['site_code', 'agent_node_id'], 'idx_players_site_agent'); + }); + + DB::table('admin_roles')->update(['scope_type' => 'system']); + + $this->backfillAdminUserAgentRoles(); + } + + public function down(): void + { + Schema::table('players', function (Blueprint $table): void { + $table->dropIndex('idx_players_site_agent'); + $table->dropConstrainedForeignId('agent_node_id'); + }); + + Schema::dropIfExists('admin_user_agent_roles'); + + Schema::table('admin_roles', function (Blueprint $table): void { + $table->dropConstrainedForeignId('delegated_from_role_id'); + $table->dropConstrainedForeignId('owner_agent_id'); + $table->dropColumn('scope_type'); + }); + } + + private function backfillAdminUserAgentRoles(): void + { + $now = Carbon::now(); + $rows = DB::table('admin_user_site_roles as usr') + ->join('admin_user_agents as uaa', 'uaa.admin_user_id', '=', 'usr.admin_user_id') + ->join('agent_nodes as an', static function ($join): void { + $join->on('an.id', '=', 'uaa.agent_node_id') + ->on('an.admin_site_id', '=', 'usr.site_id'); + }) + ->select(['usr.admin_user_id', 'uaa.agent_node_id', 'usr.role_id', 'usr.granted_at']) + ->get(); + + foreach ($rows as $row) { + DB::table('admin_user_agent_roles')->updateOrInsert( + [ + 'admin_user_id' => (int) $row->admin_user_id, + 'agent_node_id' => (int) $row->agent_node_id, + 'role_id' => (int) $row->role_id, + ], + [ + 'granted_at' => $row->granted_at ?? $now, + ], + ); + } + } +}; diff --git a/database/migrations/2026_06_02_110001_seed_agent_role_permissions.php b/database/migrations/2026_06_02_110001_seed_agent_role_permissions.php new file mode 100644 index 0000000..d7a2e8d --- /dev/null +++ b/database/migrations/2026_06_02_110001_seed_agent_role_permissions.php @@ -0,0 +1,116 @@ +pluck('id', 'permission_code'); + + $resources = array_values(array_filter( + AdminAuthorizationRegistry::resources(), + static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.agent-roles.') + || str_starts_with((string) $resource['code'], 'admin.agent-admin-users.'), + )); + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id'); + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources')->where('id', (int) $resourceId)->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] ?? [] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + if (! Schema::hasTable('admin_role_api_resources')) { + return; + } + + $superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id'); + if ($superRoleId === null) { + return; + } + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id'); + if ($resourceId === null) { + continue; + } + + DB::table('admin_role_api_resources')->updateOrInsert([ + 'role_id' => (int) $superRoleId, + 'api_resource_id' => (int) $resourceId, + ], []); + } + } + + public function down(): void + { + $codes = [ + 'admin.agent-roles.update', + 'admin.agent-roles.destroy', + 'admin.agent-roles.permissions.sync', + 'admin.agent-roles.index', + 'admin.agent-roles.store', + 'admin.agent-admin-users.index', + 'admin.agent-admin-users.store', + 'admin.agent-admin-users.roles.sync', + ]; + + foreach ($codes as $code) { + $resourceId = DB::table('admin_api_resources')->where('code', $code)->value('id'); + if ($resourceId === null) { + continue; + } + + if (Schema::hasTable('admin_role_api_resources')) { + DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete(); + } + DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete(); + DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete(); + } + } +}; diff --git a/database/migrations/2026_06_02_120000_create_agent_delegation_grants.php b/database/migrations/2026_06_02_120000_create_agent_delegation_grants.php new file mode 100644 index 0000000..f35f49a --- /dev/null +++ b/database/migrations/2026_06_02_120000_create_agent_delegation_grants.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('parent_agent_id')->constrained('agent_nodes')->cascadeOnDelete(); + $table->foreignId('child_agent_id')->constrained('agent_nodes')->cascadeOnDelete(); + $table->foreignId('menu_action_id')->constrained('admin_menu_actions')->cascadeOnDelete(); + $table->boolean('can_delegate')->default(false); + $table->foreignId('granted_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->timestamp('granted_at')->nullable(); + $table->timestamps(); + + $table->unique(['child_agent_id', 'menu_action_id'], 'uk_agent_delegation_child_action'); + $table->index(['parent_agent_id', 'child_agent_id'], 'idx_agent_delegation_parent_child'); + }); + } + + public function down(): void + { + Schema::dropIfExists('agent_delegation_grants'); + } +}; diff --git a/database/migrations/2026_06_02_130000_backfill_players_agent_node_id.php b/database/migrations/2026_06_02_130000_backfill_players_agent_node_id.php new file mode 100644 index 0000000..2cdd84f --- /dev/null +++ b/database/migrations/2026_06_02_130000_backfill_players_agent_node_id.php @@ -0,0 +1,34 @@ +join('admin_sites as s', 's.id', '=', 'an.admin_site_id') + ->where('an.depth', 0) + ->get(['an.id as root_id', 's.code as site_code']); + + foreach ($roots as $root) { + DB::table('players') + ->where('site_code', (string) $root->site_code) + ->whereNull('agent_node_id') + ->update(['agent_node_id' => (int) $root->root_id]); + } + } + + public function down(): void + { + // 不回滚归属,避免误清空业务绑定。 + } +}; diff --git a/database/migrations/2026_06_03_120000_split_agent_permission_granularity.php b/database/migrations/2026_06_03_120000_split_agent_permission_granularity.php new file mode 100644 index 0000000..a6483db --- /dev/null +++ b/database/migrations/2026_06_03_120000_split_agent_permission_granularity.php @@ -0,0 +1,177 @@ +where('code', 'view')->value('id'); + $manageActionId = DB::table('admin_action_catalog')->where('code', 'manage')->value('id'); + if ($viewActionId === null || $manageActionId === null) { + return; + } + + $agentMenuId = (int) DB::table('admin_menus')->where('code', 'system.agents')->value('id'); + if ($agentMenuId === 0) { + return; + } + + $rolesMenuId = $this->ensureChildMenu($agentMenuId, 'system.agents.roles', '代理角色', $now); + $usersMenuId = $this->ensureChildMenu($agentMenuId, 'system.agents.users', '代理账号', $now); + + $this->ensureMenuAction((int) $rolesMenuId, (int) $viewActionId, 'agent.role.view', '代理角色查看', $now); + $this->ensureMenuAction((int) $rolesMenuId, (int) $manageActionId, 'agent.role.manage', '代理角色管理', $now); + $this->ensureMenuAction((int) $usersMenuId, (int) $viewActionId, 'agent.user.view', '代理账号查看', $now); + $this->ensureMenuAction((int) $usersMenuId, (int) $manageActionId, 'agent.user.manage', '代理账号管理', $now); + + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + $nodeViewId = $menuActionIds['agent.node.view'] ?? null; + $nodeManageId = $menuActionIds['agent.node.manage'] ?? null; + $roleViewId = $menuActionIds['agent.role.view'] ?? null; + $roleManageId = $menuActionIds['agent.role.manage'] ?? null; + $userViewId = $menuActionIds['agent.user.view'] ?? null; + $userManageId = $menuActionIds['agent.user.manage'] ?? null; + + if ($nodeViewId !== null && $roleViewId !== null && $userViewId !== null) { + $roleIdsWithNodeView = DB::table('admin_role_menu_actions') + ->where('menu_action_id', (int) $nodeViewId) + ->pluck('role_id') + ->unique() + ->all(); + + foreach ($roleIdsWithNodeView as $roleId) { + foreach ([$roleViewId, $userViewId] as $actionId) { + $this->attachRoleMenuAction((int) $roleId, (int) $actionId, $now); + } + } + } + + if ($nodeManageId !== null && $roleManageId !== null && $userManageId !== null) { + $roleIdsWithNodeManage = DB::table('admin_role_menu_actions') + ->where('menu_action_id', (int) $nodeManageId) + ->pluck('role_id') + ->unique() + ->all(); + + foreach ($roleIdsWithNodeManage as $roleId) { + foreach ([$roleManageId, $userManageId] as $actionId) { + $this->attachRoleMenuAction((int) $roleId, (int) $actionId, $now); + } + } + } + + $resources = array_values(array_filter( + AdminAuthorizationRegistry::resources(), + static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.agent-') + )); + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id'); + if ($resourceId === null) { + continue; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] ?? [] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + private function ensureChildMenu(int $parentId, string $code, string $name, Carbon $now): int + { + $existing = DB::table('admin_menus')->where('code', $code)->value('id'); + if ($existing !== null) { + return (int) $existing; + } + + return (int) DB::table('admin_menus')->insertGetId([ + 'parent_id' => $parentId, + 'menu_type' => 'button', + 'code' => $code, + 'name' => $name, + 'path' => null, + 'route_name' => null, + 'component' => null, + 'icon' => null, + 'active_menu_code' => 'system.agents', + 'sort_order' => 0, + 'is_visible' => false, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + private function ensureMenuAction(int $menuId, int $actionId, string $permissionCode, string $name, Carbon $now): void + { + if (DB::table('admin_menu_actions')->where('permission_code', $permissionCode)->exists()) { + return; + } + + DB::table('admin_menu_actions')->insert([ + 'menu_id' => $menuId, + 'action_id' => $actionId, + 'permission_code' => $permissionCode, + 'name' => $name, + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + private function attachRoleMenuAction(int $roleId, int $menuActionId, Carbon $now): void + { + $exists = DB::table('admin_role_menu_actions') + ->where('role_id', $roleId) + ->where('menu_action_id', $menuActionId) + ->exists(); + + if ($exists) { + return; + } + + DB::table('admin_role_menu_actions')->insert([ + 'role_id' => $roleId, + 'menu_action_id' => $menuActionId, + ]); + } + + public function down(): void + { + $codes = ['agent.role.view', 'agent.role.manage', 'agent.user.view', 'agent.user.manage']; + $actionIds = DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->pluck('id')->all(); + + if ($actionIds !== []) { + DB::table('admin_role_menu_actions')->whereIn('menu_action_id', $actionIds)->delete(); + DB::table('admin_api_resource_bindings')->whereIn('menu_action_id', $actionIds)->delete(); + DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->delete(); + } + + DB::table('admin_menus')->whereIn('code', ['system.agents.roles', 'system.agents.users'])->delete(); + } +}; diff --git a/database/migrations/2026_06_03_140000_ensure_agent_admin_user_destroy_api_resource.php b/database/migrations/2026_06_03_140000_ensure_agent_admin_user_destroy_api_resource.php new file mode 100644 index 0000000..378a089 --- /dev/null +++ b/database/migrations/2026_06_03_140000_ensure_agent_admin_user_destroy_api_resource.php @@ -0,0 +1,87 @@ +firstWhere('code', self::RESOURCE_CODE); + + if ($resource === null) { + return; + } + + $now = Carbon::now(); + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + + $resourceId = DB::table('admin_api_resources') + ->where('code', self::RESOURCE_CODE) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => self::RESOURCE_CODE, + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + public function down(): void + { + $resourceId = DB::table('admin_api_resources') + ->where('code', self::RESOURCE_CODE) + ->value('id'); + + if ($resourceId === null) { + return; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete(); + } +}; diff --git a/database/migrations/2026_06_03_150000_align_root_agent_codes.php b/database/migrations/2026_06_03_150000_align_root_agent_codes.php new file mode 100644 index 0000000..77d1de4 --- /dev/null +++ b/database/migrations/2026_06_03_150000_align_root_agent_codes.php @@ -0,0 +1,60 @@ +orderBy('id')->get(['id', 'code']); + + foreach ($sites as $site) { + $siteId = (int) $site->id; + $code = (string) $site->code; + $legacyCode = 'root-'.$code; + + $root = DB::table('agent_nodes') + ->where('admin_site_id', $siteId) + ->where('depth', 0) + ->first(['id', 'code']); + + if ($root === null) { + continue; + } + + if ((string) $root->code === $legacyCode) { + $conflict = DB::table('agent_nodes') + ->where('admin_site_id', $siteId) + ->where('code', $code) + ->where('id', '!=', (int) $root->id) + ->exists(); + if (! $conflict) { + DB::table('agent_nodes')->where('id', (int) $root->id)->update([ + 'code' => $code, + 'updated_at' => now(), + ]); + } + } + } + } + + public function down(): void + { + $sites = DB::table('admin_sites')->orderBy('id')->get(['id', 'code']); + + foreach ($sites as $site) { + $siteId = (int) $site->id; + $code = (string) $site->code; + + DB::table('agent_nodes') + ->where('admin_site_id', $siteId) + ->where('depth', 0) + ->where('code', $code) + ->update([ + 'code' => 'root-'.$code, + 'updated_at' => now(), + ]); + } + } +}; diff --git a/database/migrations/2026_06_03_160000_agent_credit_and_settlement_tables.php b/database/migrations/2026_06_03_160000_agent_credit_and_settlement_tables.php new file mode 100644 index 0000000..cc01290 --- /dev/null +++ b/database/migrations/2026_06_03_160000_agent_credit_and_settlement_tables.php @@ -0,0 +1,140 @@ +foreignId('agent_node_id')->primary()->constrained('agent_nodes')->cascadeOnDelete(); + $table->decimal('total_share_rate', 5, 2)->default(0)->comment('总占成 0-100'); + $table->unsignedBigInteger('credit_limit')->default(0); + $table->unsignedBigInteger('allocated_credit')->default(0); + $table->unsignedBigInteger('used_credit')->default(0); + $table->decimal('rebate_limit', 8, 4)->default(0); + $table->decimal('default_player_rebate', 8, 4)->default(0); + $table->string('settlement_cycle', 16)->default('weekly'); + $table->boolean('can_grant_extra_rebate')->default(false); + $table->timestamps(); + }); + + Schema::create('player_credit_accounts', function (Blueprint $table): void { + $table->foreignId('player_id')->primary()->constrained('players')->cascadeOnDelete(); + $table->unsignedBigInteger('credit_limit')->default(0); + $table->unsignedBigInteger('used_credit')->default(0); + $table->unsignedBigInteger('frozen_credit')->default(0); + $table->timestamps(); + }); + + Schema::create('player_rebate_profiles', function (Blueprint $table): void { + $table->id(); + $table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); + $table->string('game_type', 32)->default('*'); + $table->boolean('inherit_from_agent')->default(true); + $table->decimal('rebate_rate', 8, 4)->default(0); + $table->decimal('extra_rebate_rate', 8, 4)->default(0); + $table->timestamps(); + $table->unique(['player_id', 'game_type']); + }); + + Schema::create('settlement_periods', function (Blueprint $table): void { + $table->id(); + $table->foreignId('admin_site_id')->constrained('admin_sites')->cascadeOnDelete(); + $table->timestamp('period_start'); + $table->timestamp('period_end'); + $table->string('status', 16)->default('open'); + $table->timestamps(); + $table->index(['admin_site_id', 'status']); + }); + + Schema::create('settlement_bills', function (Blueprint $table): void { + $table->id(); + $table->foreignId('settlement_period_id')->constrained('settlement_periods')->cascadeOnDelete(); + $table->string('bill_type', 16); + $table->string('owner_type', 16); + $table->unsignedBigInteger('owner_id'); + $table->string('counterparty_type', 16); + $table->unsignedBigInteger('counterparty_id'); + $table->bigInteger('gross_win_loss')->default(0); + $table->bigInteger('rebate_amount')->default(0); + $table->bigInteger('adjustment_amount')->default(0); + $table->bigInteger('net_amount')->default(0); + $table->bigInteger('paid_amount')->default(0); + $table->bigInteger('unpaid_amount')->default(0); + $table->string('status', 16)->default('pending'); + $table->timestamp('confirmed_at')->nullable(); + $table->timestamps(); + $table->index(['settlement_period_id', 'bill_type']); + }); + + Schema::create('rebate_records', function (Blueprint $table): void { + $table->id(); + $table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); + $table->foreignId('settlement_period_id')->nullable()->constrained('settlement_periods')->nullOnDelete(); + $table->string('game_type', 32)->default('*'); + $table->unsignedBigInteger('valid_bet_amount')->default(0); + $table->decimal('rebate_rate', 8, 4)->default(0); + $table->unsignedBigInteger('rebate_amount')->default(0); + $table->string('rebate_type', 16)->default('basic'); + $table->foreignId('owner_agent_id')->nullable()->constrained('agent_nodes')->nullOnDelete(); + $table->string('status', 16)->default('pending'); + $table->timestamps(); + }); + + Schema::create('rebate_allocations', function (Blueprint $table): void { + $table->id(); + $table->foreignId('rebate_record_id')->constrained('rebate_records')->cascadeOnDelete(); + $table->foreignId('settlement_bill_id')->nullable()->constrained('settlement_bills')->nullOnDelete(); + $table->string('participant_type', 16); + $table->unsignedBigInteger('participant_id')->default(0); + $table->decimal('actual_share_rate', 5, 2)->default(0); + $table->bigInteger('allocated_amount')->default(0); + $table->string('allocation_rule', 32)->default('share'); + $table->timestamps(); + }); + + Schema::create('payment_records', function (Blueprint $table): void { + $table->id(); + $table->foreignId('settlement_bill_id')->constrained('settlement_bills')->cascadeOnDelete(); + $table->string('payer_type', 16); + $table->unsignedBigInteger('payer_id'); + $table->string('payee_type', 16); + $table->unsignedBigInteger('payee_id'); + $table->bigInteger('amount'); + $table->string('method', 32)->nullable(); + $table->string('status', 16)->default('pending'); + $table->foreignId('created_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->foreignId('confirmed_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->timestamp('confirmed_at')->nullable(); + $table->timestamps(); + }); + + Schema::create('credit_ledger', function (Blueprint $table): void { + $table->id(); + $table->string('owner_type', 16); + $table->unsignedBigInteger('owner_id'); + $table->bigInteger('amount'); + $table->string('reason', 64); + $table->string('ref_type', 32)->nullable(); + $table->unsignedBigInteger('ref_id')->nullable(); + $table->timestamps(); + $table->index(['owner_type', 'owner_id', 'created_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('credit_ledger'); + Schema::dropIfExists('payment_records'); + Schema::dropIfExists('rebate_allocations'); + Schema::dropIfExists('rebate_records'); + Schema::dropIfExists('settlement_bills'); + Schema::dropIfExists('settlement_periods'); + Schema::dropIfExists('player_rebate_profiles'); + Schema::dropIfExists('player_credit_accounts'); + Schema::dropIfExists('agent_profiles'); + } +}; diff --git a/database/migrations/2026_06_03_170000_seed_agent_settlement_api_resources.php b/database/migrations/2026_06_03_170000_seed_agent_settlement_api_resources.php new file mode 100644 index 0000000..8c15432 --- /dev/null +++ b/database/migrations/2026_06_03_170000_seed_agent_settlement_api_resources.php @@ -0,0 +1,155 @@ + */ + private const RESOURCE_CODE_PREFIXES = [ + 'admin.settlement-bills.', + 'admin.settlement-periods.', + 'admin.settlement-payments.', + 'admin.settlement-adjustments.', + 'admin.settlement-reports.', + 'admin.credit-ledger.', + 'admin.agent-lines.', + 'admin.agent-nodes.profile.', + ]; + + /** @var list */ + private const MENU_ACTION_CODES = [ + 'settlement.agent.view', + 'settlement.agent.manage', + 'agent.line.provision', + 'agent.profile.manage', + ]; + + public function up(): void + { + AdminAgentLineSettlementPermissionMenuActionSync::syncMissing(); + + $now = Carbon::now(); + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + + $resources = array_values(array_filter( + AdminAuthorizationRegistry::resources(), + static fn (array $resource): bool => self::matchesResourceCode((string) $resource['code']), + )); + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $this->grantSuperAdminMenuActions(); + } + + public function down(): void + { + foreach (AdminAuthorizationRegistry::resources() as $resource) { + if (! self::matchesResourceCode((string) $resource['code'])) { + continue; + } + + $resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id'); + if ($resourceId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete(); + DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete(); + } + } + + private static function matchesResourceCode(string $code): bool + { + foreach (self::RESOURCE_CODE_PREFIXES as $prefix) { + if (str_starts_with($code, $prefix)) { + return true; + } + } + + return false; + } + + private function grantSuperAdminMenuActions(): void + { + $superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id'); + if ($superRoleId === null) { + return; + } + + $menuActionIds = DB::table('admin_menu_actions') + ->whereIn('permission_code', self::MENU_ACTION_CODES) + ->pluck('id'); + + foreach ($menuActionIds as $menuActionId) { + DB::table('admin_role_menu_actions')->updateOrInsert([ + 'role_id' => (int) $superRoleId, + 'menu_action_id' => (int) $menuActionId, + ]); + } + + if (! Schema::hasTable('admin_role_legacy_permissions')) { + return; + } + + foreach (['prd.settlement.agent.view', 'prd.settlement.agent.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage'] as $slug) { + DB::table('admin_role_legacy_permissions')->updateOrInsert([ + 'role_id' => (int) $superRoleId, + 'permission_slug' => $slug, + ], []); + } + } +}; diff --git a/database/migrations/2026_06_03_180000_add_agent_profile_capability_flags.php b/database/migrations/2026_06_03_180000_add_agent_profile_capability_flags.php new file mode 100644 index 0000000..1de9a80 --- /dev/null +++ b/database/migrations/2026_06_03_180000_add_agent_profile_capability_flags.php @@ -0,0 +1,38 @@ +boolean('can_create_child_agent')->default(false)->after('can_grant_extra_rebate'); + $table->boolean('can_create_player')->default(true)->after('can_create_child_agent'); + }); + + \Illuminate\Support\Facades\DB::table('agent_profiles')->update([ + 'can_create_child_agent' => true, + 'can_create_player' => true, + ]); + + \App\Support\AgentDefaultRolePermissions::ensurePlatformAgentRole(); + \App\Models\AdminUser::query() + ->whereIn('id', \Illuminate\Support\Facades\DB::table('admin_user_agents')->pluck('admin_user_id')) + ->each(static function (\App\Models\AdminUser $user): void { + $agentNodeId = $user->primaryAgentNodeId(); + if ($agentNodeId !== null) { + $user->syncPrimaryPlatformAgentRole($agentNodeId); + } + }); + } + + public function down(): void + { + Schema::table('agent_profiles', function (Blueprint $table): void { + $table->dropColumn(['can_create_child_agent', 'can_create_player']); + }); + } +}; diff --git a/database/migrations/2026_06_03_190000_fix_agent_primary_admin_user_status.php b/database/migrations/2026_06_03_190000_fix_agent_primary_admin_user_status.php new file mode 100644 index 0000000..4801e2f --- /dev/null +++ b/database/migrations/2026_06_03_190000_fix_agent_primary_admin_user_status.php @@ -0,0 +1,51 @@ +join('agent_nodes as an', 'an.id', '=', 'aua.agent_node_id') + ->join('admin_users as au', 'au.id', '=', 'aua.admin_user_id') + ->where('aua.is_primary', true) + ->where('an.status', 1) + ->where('au.status', 1) + ->pluck('au.id'); + + if ($enabledUserIds->isNotEmpty()) { + DB::table('admin_users') + ->whereIn('id', $enabledUserIds->all()) + ->update(['status' => 0, 'updated_at' => $now]); + } + + $disabledUserIds = DB::table('admin_user_agents as aua') + ->join('agent_nodes as an', 'an.id', '=', 'aua.agent_node_id') + ->join('admin_users as au', 'au.id', '=', 'aua.admin_user_id') + ->where('aua.is_primary', true) + ->where('an.status', 0) + ->where('au.status', 0) + ->pluck('au.id'); + + if ($disabledUserIds->isNotEmpty()) { + DB::table('admin_users') + ->whereIn('id', $disabledUserIds->all()) + ->update(['status' => 1, 'updated_at' => $now]); + } + } + + public function down(): void + { + // 不可逆:无法可靠还原误写前的 admin_users.status + } +}; diff --git a/database/migrations/2026_06_04_100000_agent_game_settlement_ledger.php b/database/migrations/2026_06_04_100000_agent_game_settlement_ledger.php new file mode 100644 index 0000000..44b3b76 --- /dev/null +++ b/database/migrations/2026_06_04_100000_agent_game_settlement_ledger.php @@ -0,0 +1,88 @@ +foreignId('agent_node_id')->nullable()->after('player_id')->constrained('agent_nodes')->nullOnDelete(); + $table->json('share_snapshot')->nullable()->after('rule_snapshot_json'); + $table->decimal('agent_rebate_rate_snapshot', 8, 4)->nullable()->after('share_snapshot'); + $table->timestamp('agent_settled_at')->nullable()->after('settled_at'); + $table->foreignId('agent_settlement_reversal_of_id')->nullable()->after('agent_settled_at') + ->constrained('ticket_items')->nullOnDelete(); + }); + + Schema::create('share_ledger', function (Blueprint $table): void { + $table->id(); + $table->foreignId('ticket_item_id')->constrained('ticket_items')->cascadeOnDelete(); + $table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); + $table->foreignId('agent_node_id')->nullable()->constrained('agent_nodes')->nullOnDelete(); + $table->json('agent_path')->nullable(); + $table->json('share_snapshot')->nullable(); + $table->bigInteger('game_win_loss')->default(0); + $table->bigInteger('basic_rebate')->default(0); + $table->bigInteger('shared_net_win_loss')->default(0); + $table->json('allocations_json')->nullable(); + $table->foreignId('settlement_period_id')->nullable()->constrained('settlement_periods')->nullOnDelete(); + $table->unsignedBigInteger('reversal_of_id')->nullable(); + $table->timestamp('settled_at'); + $table->timestamps(); + $table->index(['settled_at', 'player_id']); + $table->index(['settlement_period_id']); + }); + + Schema::table('share_ledger', function (Blueprint $table): void { + $table->foreign('reversal_of_id')->references('id')->on('share_ledger')->nullOnDelete(); + }); + + Schema::table('rebate_records', function (Blueprint $table): void { + $table->foreignId('ticket_item_id')->nullable()->after('player_id')->constrained('ticket_items')->nullOnDelete(); + $table->foreignId('reversal_of_id')->nullable()->after('ticket_item_id')->constrained('rebate_records')->nullOnDelete(); + }); + + Schema::table('settlement_bills', function (Blueprint $table): void { + $table->timestamp('locked_at')->nullable()->after('confirmed_at'); + $table->foreignId('reversed_bill_id')->nullable()->after('locked_at')->constrained('settlement_bills')->nullOnDelete(); + $table->json('meta_json')->nullable()->after('reversed_bill_id'); + }); + + Schema::create('settlement_adjustments', function (Blueprint $table): void { + $table->id(); + $table->foreignId('settlement_period_id')->nullable()->constrained('settlement_periods')->nullOnDelete(); + $table->foreignId('original_bill_id')->nullable()->constrained('settlement_bills')->nullOnDelete(); + $table->string('adjustment_type', 32); + $table->bigInteger('amount'); + $table->string('reason', 255)->nullable(); + $table->foreignId('created_by')->nullable()->constrained('admin_users')->nullOnDelete(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('settlement_adjustments'); + + Schema::table('settlement_bills', function (Blueprint $table): void { + $table->dropConstrainedForeignId('reversed_bill_id'); + $table->dropColumn(['locked_at', 'meta_json']); + }); + + Schema::table('rebate_records', function (Blueprint $table): void { + $table->dropConstrainedForeignId('reversal_of_id'); + $table->dropConstrainedForeignId('ticket_item_id'); + }); + + Schema::dropIfExists('share_ledger'); + + Schema::table('ticket_items', function (Blueprint $table): void { + $table->dropConstrainedForeignId('agent_settlement_reversal_of_id'); + $table->dropConstrainedForeignId('agent_node_id'); + $table->dropColumn(['share_snapshot', 'agent_rebate_rate_snapshot', 'agent_settled_at']); + }); + } +}; diff --git a/database/migrations/2026_06_04_120000_add_player_auth_and_funding_mode.php b/database/migrations/2026_06_04_120000_add_player_auth_and_funding_mode.php new file mode 100644 index 0000000..df0c65f --- /dev/null +++ b/database/migrations/2026_06_04_120000_add_player_auth_and_funding_mode.php @@ -0,0 +1,43 @@ +string('auth_source', 16)->default('main_site_sso')->after('site_player_id'); + $table->string('funding_mode', 16)->default('wallet')->after('auth_source'); + $table->string('password_hash', 255)->nullable()->after('username'); + $table->unsignedSmallInteger('login_failed_count')->default(0)->after('last_login_at'); + $table->timestamp('login_locked_until')->nullable()->after('login_failed_count'); + }); + + DB::table('players')->update([ + 'auth_source' => 'main_site_sso', + 'funding_mode' => 'wallet', + ]); + + Schema::table('players', function (Blueprint $table): void { + $table->index(['site_code', 'auth_source', 'username'], 'idx_players_site_auth_username'); + }); + } + + public function down(): void + { + Schema::table('players', function (Blueprint $table): void { + $table->dropIndex('idx_players_site_auth_username'); + $table->dropColumn([ + 'auth_source', + 'funding_mode', + 'password_hash', + 'login_failed_count', + 'login_locked_until', + ]); + }); + } +}; diff --git a/database/migrations/2026_06_04_120000_agent_settlement_payment_proof.php b/database/migrations/2026_06_04_120000_agent_settlement_payment_proof.php new file mode 100644 index 0000000..c3bc439 --- /dev/null +++ b/database/migrations/2026_06_04_120000_agent_settlement_payment_proof.php @@ -0,0 +1,23 @@ +text('proof')->nullable()->after('method'); + $table->string('remark', 255)->nullable()->after('proof'); + }); + } + + public function down(): void + { + Schema::table('payment_records', function (Blueprint $table): void { + $table->dropColumn(['proof', 'remark']); + }); + } +}; diff --git a/database/migrations/2026_06_04_120000_resync_agent_owner_role_permissions.php b/database/migrations/2026_06_04_120000_resync_agent_owner_role_permissions.php new file mode 100644 index 0000000..83f1ec8 --- /dev/null +++ b/database/migrations/2026_06_04_120000_resync_agent_owner_role_permissions.php @@ -0,0 +1,30 @@ +each(static function (AgentNode $node): void { + $role = AdminRole::query() + ->where('owner_agent_id', $node->id) + ->where('slug', 'agent_owner_'.$node->id) + ->first(); + + if ($role === null) { + return; + } + + $role->syncLegacyPermissionSlugs(AgentDefaultRolePermissions::ownerSlugsForNode($node)); + }); + } + + public function down(): void + { + // 权限包为产品策略,回滚不恢复旧 slug 集合。 + } +}; diff --git a/database/migrations/2026_06_04_130000_seed_platform_agent_role_and_resync_bindings.php b/database/migrations/2026_06_04_130000_seed_platform_agent_role_and_resync_bindings.php new file mode 100644 index 0000000..cd18738 --- /dev/null +++ b/database/migrations/2026_06_04_130000_seed_platform_agent_role_and_resync_bindings.php @@ -0,0 +1,63 @@ +each(static function (AgentNode $node): void { + $ownerRole = AdminRole::query() + ->where('owner_agent_id', $node->id) + ->where('slug', 'agent_owner_'.$node->id) + ->first(); + + if ($ownerRole !== null) { + $ownerRole->syncLegacyPermissionSlugs(AgentDefaultRolePermissions::ownerSlugsForNode($node)); + } + }); + + $bindings = DB::table('admin_user_agents')->get(['admin_user_id', 'agent_node_id']); + foreach ($bindings as $binding) { + $adminUserId = (int) $binding->admin_user_id; + $agentNodeId = (int) $binding->agent_node_id; + $user = AdminUser::query()->find($adminUserId); + if ($user === null) { + continue; + } + + $agentRoleIds = DB::table('admin_user_agent_roles') + ->where('admin_user_id', $adminUserId) + ->where('agent_node_id', $agentNodeId) + ->pluck('role_id') + ->map(static fn ($id): int => (int) $id) + ->all(); + + if ($agentRoleIds === []) { + $ownerId = (int) (AdminRole::query() + ->where('owner_agent_id', $agentNodeId) + ->where('slug', 'agent_owner_'.$agentNodeId) + ->value('id') ?? 0); + if ($ownerId > 0) { + $agentRoleIds = [$ownerId]; + } + } + + if ($agentRoleIds !== []) { + $user->syncAgentRoleIds($agentNodeId, $agentRoleIds); + } + } + } + + public function down(): void + { + // 不回滚权限与 pivot,避免经营账号失权。 + } +}; diff --git a/database/migrations/2026_06_04_140000_agent_settlement_reports_and_tags.php b/database/migrations/2026_06_04_140000_agent_settlement_reports_and_tags.php new file mode 100644 index 0000000..559a70e --- /dev/null +++ b/database/migrations/2026_06_04_140000_agent_settlement_reports_and_tags.php @@ -0,0 +1,38 @@ +bigInteger('platform_rounding_adjustment')->default(0)->after('adjustment_amount'); + }); + + Schema::table('players', function (Blueprint $table): void { + $table->json('risk_tags')->nullable()->after('status'); + }); + + Schema::table('agent_nodes', function (Blueprint $table): void { + $table->json('risk_tags')->nullable()->after('status'); + }); + } + + public function down(): void + { + Schema::table('agent_nodes', function (Blueprint $table): void { + $table->dropColumn('risk_tags'); + }); + + Schema::table('players', function (Blueprint $table): void { + $table->dropColumn('risk_tags'); + }); + + Schema::table('settlement_bills', function (Blueprint $table): void { + $table->dropColumn('platform_rounding_adjustment'); + }); + } +}; diff --git a/database/migrations/2026_06_04_140000_bind_agents_to_platform_agent_role.php b/database/migrations/2026_06_04_140000_bind_agents_to_platform_agent_role.php new file mode 100644 index 0000000..0fdeea8 --- /dev/null +++ b/database/migrations/2026_06_04_140000_bind_agents_to_platform_agent_role.php @@ -0,0 +1,44 @@ +get(['admin_user_id', 'agent_node_id']) as $binding) { + $user = AdminUser::query()->find((int) $binding->admin_user_id); + if ($user === null) { + continue; + } + + $user->syncPrimaryPlatformAgentRole((int) $binding->agent_node_id); + } + + $ownerRoleIds = AdminRole::query() + ->where('scope_type', AdminRole::SCOPE_AGENT) + ->where('slug', 'like', 'agent_owner_%') + ->pluck('id') + ->all(); + + if ($ownerRoleIds !== []) { + DB::table('admin_user_agent_roles')->whereIn('role_id', $ownerRoleIds)->delete(); + DB::table('admin_user_site_roles')->whereIn('role_id', $ownerRoleIds)->delete(); + DB::table('admin_role_menu_actions')->whereIn('role_id', $ownerRoleIds)->delete(); + AdminRole::query()->whereIn('id', $ownerRoleIds)->delete(); + } + } + + public function down(): void + { + // 不回滚:避免经营账号失权。 + } +}; diff --git a/database/migrations/2026_06_04_150000_ensure_platform_fixed_system_roles.php b/database/migrations/2026_06_04_150000_ensure_platform_fixed_system_roles.php new file mode 100644 index 0000000..72f16ff --- /dev/null +++ b/database/migrations/2026_06_04_150000_ensure_platform_fixed_system_roles.php @@ -0,0 +1,17 @@ + */ + private const RESOURCE_CODES = [ + 'admin.credit-ledger.index', + 'admin.settlement-payments.index', + 'admin.settlement-adjustments.index', + 'admin.settlement-reports.summary', + 'admin.settlement-reports.show', + ]; + + public function up(): void + { + $now = Carbon::now(); + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + $byCode = collect(AdminAuthorizationRegistry::resources())->keyBy('code'); + + foreach (self::RESOURCE_CODES as $code) { + $resource = $byCode->get($code); + if (! is_array($resource)) { + continue; + } + + $resourceId = DB::table('admin_api_resources') + ->where('code', $resource['code']) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => $resource['code'], + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + public function down(): void + { + foreach (self::RESOURCE_CODES as $code) { + $resourceId = DB::table('admin_api_resources')->where('code', $code)->value('id'); + if ($resourceId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete(); + DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete(); + } + } +}; diff --git a/database/migrations/2026_06_09_120000_create_player_credit_accounts_and_credit_ledger.php b/database/migrations/2026_06_09_120000_create_player_credit_accounts_and_credit_ledger.php index 2f75dcf..03d4367 100644 --- a/database/migrations/2026_06_09_120000_create_player_credit_accounts_and_credit_ledger.php +++ b/database/migrations/2026_06_09_120000_create_player_credit_accounts_and_credit_ledger.php @@ -1,7 +1,6 @@ unsignedBigInteger('player_id')->primary(); - $table->bigInteger('credit_limit')->default(0); - $table->bigInteger('used_credit')->default(0); - $table->bigInteger('frozen_credit')->default(0); - $table->timestamps(); - - $table->foreign('player_id') - ->references('id') - ->on('players') - ->cascadeOnDelete(); - }); - } - - if (! Schema::hasTable('credit_ledger')) { - Schema::create('credit_ledger', function (Blueprint $table): void { - $table->id(); - $table->string('owner_type', 16); - $table->unsignedBigInteger('owner_id'); - $table->bigInteger('amount'); - $table->string('reason', 64); - $table->string('ref_type', 32)->nullable(); - $table->unsignedBigInteger('ref_id')->nullable(); - $table->timestamps(); - - $table->index(['owner_type', 'owner_id', 'created_at']); - }); + return; } if (Schema::hasTable('players') && Schema::hasColumn('players', 'funding_mode')) { @@ -71,7 +44,6 @@ return new class extends Migration public function down(): void { - Schema::dropIfExists('credit_ledger'); - Schema::dropIfExists('player_credit_accounts'); + // 兼容回填迁移,不回滚既有表结构。 } }; diff --git a/database/migrations/2026_06_10_120000_align_postgres_indexes_with_live_schema.php b/database/migrations/2026_06_10_120000_align_postgres_indexes_with_live_schema.php new file mode 100644 index 0000000..9d37581 --- /dev/null +++ b/database/migrations/2026_06_10_120000_align_postgres_indexes_with_live_schema.php @@ -0,0 +1,48 @@ +hasIndex('ticket_items', 'idx_ticket_items_order_id')) { + Schema::table('ticket_items', function (Blueprint $table): void { + $table->index('order_id', 'idx_ticket_items_order_id'); + }); + } + + if (! $this->hasIndex('settlement_batches', 'idx_settlement_batches_result_batch_id')) { + Schema::table('settlement_batches', function (Blueprint $table): void { + $table->index('result_batch_id', 'idx_settlement_batches_result_batch_id'); + }); + } + } + + public function down(): void + { + if ($this->hasIndex('settlement_batches', 'idx_settlement_batches_result_batch_id')) { + Schema::table('settlement_batches', function (Blueprint $table): void { + $table->dropIndex('idx_settlement_batches_result_batch_id'); + }); + } + + if ($this->hasIndex('ticket_items', 'idx_ticket_items_order_id')) { + Schema::table('ticket_items', function (Blueprint $table): void { + $table->dropIndex('idx_ticket_items_order_id'); + }); + } + } + + private function hasIndex(string $table, string $indexName): bool + { + return DB::table('pg_indexes') + ->where('schemaname', 'public') + ->where('tablename', $table) + ->where('indexname', $indexName) + ->exists(); + } +}; diff --git a/database/migrations/README.md b/database/migrations/README.md index 71850ce..f10ac15 100644 --- a/database/migrations/README.md +++ b/database/migrations/README.md @@ -1,19 +1,12 @@ # 迁移目录说明 -当前项目已切换为 **schema dump 作为数据库基线** 的维护方式。 +当前项目使用 **纯 migration 链** 维护 PostgreSQL 结构。 -- 最终版 PostgreSQL 结构:[`../schema/pgsql-schema.sql`](../schema/pgsql-schema.sql) -- 新环境初始化:优先加载 schema dump,再执行后续新增 migration -- 旧的历史 migration 已清理,不再作为基线结构来源 +- 新环境初始化:直接执行完整 migration 链 +- 历史 migration 保留,作为唯一结构来源 - 统一初始化入口:`php artisan lottery:db-init` 后续规则: 1. 新增数据库结构变更时,继续正常创建新的 migration 文件放在本目录。 -2. 当结构进入一个新的稳定阶段后,可重新执行: - -```bash -php artisan schema:dump --database=pgsql --prune -``` - -3. 执行 `--prune` 前,确认团队已接受“历史迁移链不再保留”的方式。 +2. 不再依赖 `schema dump` 作为基线;如果有临时导出文件,也不作为部署前提。 diff --git a/database/schema/pgsql-schema.sql b/database/schema/pgsql-schema.sql deleted file mode 100644 index 1dd9476..0000000 --- a/database/schema/pgsql-schema.sql +++ /dev/null @@ -1,4889 +0,0 @@ --- --- PostgreSQL database dump --- - -\restrict rPXEgF1VaYgsz0ptn4X1KcYROWRPYlYb6daN4zAOY961hMNjxCs5gLhsUZO9N0E - --- Dumped from database version 18.3(ServBay) --- Dumped by pg_dump version 18.3(ServBay) - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET transaction_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: admin_action_catalog; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_action_catalog ( - id bigint NOT NULL, - code character varying(64) NOT NULL, - name character varying(64) NOT NULL, - sort_order integer DEFAULT 0 NOT NULL, - status smallint DEFAULT '1'::smallint NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN admin_action_catalog.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.admin_action_catalog.status IS '1=enabled,0=disabled'; - - --- --- Name: admin_action_catalog_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.admin_action_catalog_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: admin_action_catalog_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.admin_action_catalog_id_seq OWNED BY public.admin_action_catalog.id; - - --- --- Name: admin_api_resource_bindings; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_api_resource_bindings ( - id bigint NOT NULL, - api_resource_id bigint NOT NULL, - menu_action_id bigint NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: admin_api_resource_bindings_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.admin_api_resource_bindings_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: admin_api_resource_bindings_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.admin_api_resource_bindings_id_seq OWNED BY public.admin_api_resource_bindings.id; - - --- --- Name: admin_api_resources; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_api_resources ( - id bigint NOT NULL, - code character varying(128) NOT NULL, - module_code character varying(64) NOT NULL, - name character varying(128) NOT NULL, - http_method character varying(16) NOT NULL, - uri_pattern character varying(255) NOT NULL, - route_name character varying(255), - auth_mode character varying(24) DEFAULT 'permission_required'::character varying NOT NULL, - is_audit_required boolean DEFAULT false NOT NULL, - status smallint DEFAULT '1'::smallint NOT NULL, - meta_json json, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN admin_api_resources.auth_mode; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.admin_api_resources.auth_mode IS 'login_only|permission_required|internal_only'; - - --- --- Name: COLUMN admin_api_resources.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.admin_api_resources.status IS '1=enabled,0=disabled'; - - --- --- Name: admin_api_resources_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.admin_api_resources_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: admin_api_resources_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.admin_api_resources_id_seq OWNED BY public.admin_api_resources.id; - - --- --- Name: admin_menu_actions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_menu_actions ( - id bigint NOT NULL, - menu_id bigint NOT NULL, - action_id bigint NOT NULL, - permission_code character varying(128) NOT NULL, - name character varying(128) NOT NULL, - status smallint DEFAULT '1'::smallint NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN admin_menu_actions.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.admin_menu_actions.status IS '1=enabled,0=disabled'; - - --- --- Name: admin_menu_actions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.admin_menu_actions_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: admin_menu_actions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.admin_menu_actions_id_seq OWNED BY public.admin_menu_actions.id; - - --- --- Name: admin_menus; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_menus ( - id bigint NOT NULL, - parent_id bigint, - menu_type character varying(24) NOT NULL, - code character varying(128) NOT NULL, - name character varying(128) NOT NULL, - path character varying(255), - route_name character varying(255), - component character varying(255), - icon character varying(128), - active_menu_code character varying(128), - sort_order integer DEFAULT 0 NOT NULL, - is_visible boolean DEFAULT true NOT NULL, - is_cache boolean DEFAULT false NOT NULL, - is_external boolean DEFAULT false NOT NULL, - status smallint DEFAULT '1'::smallint NOT NULL, - meta_json json, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN admin_menus.menu_type; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.admin_menus.menu_type IS 'directory|menu|page'; - - --- --- Name: COLUMN admin_menus.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.admin_menus.status IS '1=enabled,0=disabled'; - - --- --- Name: admin_menus_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.admin_menus_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: admin_menus_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.admin_menus_id_seq OWNED BY public.admin_menus.id; - - --- --- Name: admin_role_menu_actions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_role_menu_actions ( - role_id bigint NOT NULL, - menu_action_id bigint NOT NULL -); - - --- --- Name: admin_roles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_roles ( - id bigint NOT NULL, - slug character varying(64) NOT NULL, - name character varying(128) NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - code character varying(64) NOT NULL, - description text, - status smallint DEFAULT '1'::smallint NOT NULL, - is_system boolean DEFAULT false NOT NULL, - sort_order integer DEFAULT 0 NOT NULL, - owner_agent_id bigint, - delegated_from_role_id bigint, - scope_type character varying(16) DEFAULT 'system'::character varying NOT NULL -); - - --- --- Name: COLUMN admin_roles.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.admin_roles.status IS '1=enabled,0=disabled'; - - --- --- Name: admin_roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.admin_roles_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: admin_roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.admin_roles_id_seq OWNED BY public.admin_roles.id; - - --- --- Name: admin_sites; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_sites ( - id bigint NOT NULL, - code character varying(64) NOT NULL, - name character varying(128) NOT NULL, - currency_code character varying(16) DEFAULT 'NPR'::character varying NOT NULL, - status smallint DEFAULT '1'::smallint NOT NULL, - is_default boolean DEFAULT false NOT NULL, - extra_json json, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - wallet_api_url character varying(512), - wallet_debit_path character varying(128) DEFAULT '/wallet/debit-for-lottery'::character varying NOT NULL, - wallet_credit_path character varying(128) DEFAULT '/wallet/credit-from-lottery'::character varying NOT NULL, - wallet_balance_path character varying(128) DEFAULT '/wallet/balance'::character varying NOT NULL, - wallet_api_key_encrypted text, - sso_jwt_secret_encrypted text, - wallet_timeout_seconds smallint DEFAULT '10'::smallint NOT NULL, - iframe_allowed_origins json, - lottery_h5_base_url character varying(512), - notes text -); - - --- --- Name: COLUMN admin_sites.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.admin_sites.status IS '1=enabled,0=disabled'; - - --- --- Name: admin_sites_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.admin_sites_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: admin_sites_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.admin_sites_id_seq OWNED BY public.admin_sites.id; - - --- --- Name: admin_user_agent_roles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_user_agent_roles ( - admin_user_id bigint NOT NULL, - agent_node_id bigint NOT NULL, - role_id bigint NOT NULL, - granted_at timestamp(0) without time zone -); - - --- --- Name: admin_user_agents; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_user_agents ( - admin_user_id bigint NOT NULL, - agent_node_id bigint NOT NULL, - is_primary boolean DEFAULT true NOT NULL, - granted_at timestamp(0) without time zone -); - - --- --- Name: admin_user_menu_actions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_user_menu_actions ( - admin_user_id bigint NOT NULL, - site_id bigint NOT NULL, - menu_action_id bigint NOT NULL, - granted_at timestamp(0) without time zone -); - - --- --- Name: admin_user_site_roles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_user_site_roles ( - admin_user_id bigint NOT NULL, - site_id bigint NOT NULL, - role_id bigint NOT NULL, - granted_at timestamp(0) without time zone -); - - --- --- Name: admin_users; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.admin_users ( - id bigint NOT NULL, - name character varying(128) NOT NULL, - email character varying(255), - email_verified_at timestamp(0) without time zone, - password character varying(255) NOT NULL, - status smallint DEFAULT '0'::smallint NOT NULL, - last_login_at timestamp(0) without time zone, - remember_token character varying(100), - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - username character varying(64) NOT NULL -); - - --- --- Name: COLUMN admin_users.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.admin_users.status IS '0=active,1=disabled'; - - --- --- Name: admin_users_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.admin_users_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: admin_users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.admin_users_id_seq OWNED BY public.admin_users.id; - - --- --- Name: agent_delegation_grants; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agent_delegation_grants ( - id bigint NOT NULL, - parent_agent_id bigint NOT NULL, - child_agent_id bigint NOT NULL, - menu_action_id bigint NOT NULL, - can_delegate boolean DEFAULT false NOT NULL, - granted_by bigint, - granted_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: agent_delegation_grants_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.agent_delegation_grants_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: agent_delegation_grants_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.agent_delegation_grants_id_seq OWNED BY public.agent_delegation_grants.id; - - --- --- Name: agent_nodes; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agent_nodes ( - id bigint NOT NULL, - admin_site_id bigint NOT NULL, - parent_id bigint, - path character varying(512) NOT NULL, - depth smallint DEFAULT '0'::smallint NOT NULL, - code character varying(64) NOT NULL, - name character varying(128) NOT NULL, - status smallint DEFAULT '1'::smallint NOT NULL, - created_by bigint, - extra_json json, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - risk_tags json -); - - --- --- Name: COLUMN agent_nodes.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.agent_nodes.status IS '1=enabled,0=disabled'; - - --- --- Name: agent_nodes_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.agent_nodes_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: agent_nodes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.agent_nodes_id_seq OWNED BY public.agent_nodes.id; - - --- --- Name: agent_profiles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agent_profiles ( - agent_node_id bigint NOT NULL, - total_share_rate numeric(5,2) DEFAULT '0'::numeric NOT NULL, - credit_limit bigint DEFAULT '0'::bigint NOT NULL, - allocated_credit bigint DEFAULT '0'::bigint NOT NULL, - used_credit bigint DEFAULT '0'::bigint NOT NULL, - rebate_limit numeric(8,4) DEFAULT '0'::numeric NOT NULL, - default_player_rebate numeric(8,4) DEFAULT '0'::numeric NOT NULL, - settlement_cycle character varying(16) DEFAULT 'weekly'::character varying NOT NULL, - can_grant_extra_rebate boolean DEFAULT false NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - can_create_child_agent boolean DEFAULT false NOT NULL, - can_create_player boolean DEFAULT true NOT NULL -); - - --- --- Name: COLUMN agent_profiles.total_share_rate; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.agent_profiles.total_share_rate IS '总占成 0-100'; - - --- --- Name: audit_logs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.audit_logs ( - id bigint NOT NULL, - operator_type character varying(16) NOT NULL, - operator_id bigint DEFAULT '0'::bigint NOT NULL, - module_code character varying(32), - action_code character varying(32), - target_type character varying(128), - target_id character varying(64), - before_json json, - after_json json, - ip character varying(64), - user_agent character varying(255), - created_at timestamp(0) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL -); - - --- --- Name: audit_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.audit_logs_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: audit_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.audit_logs_id_seq OWNED BY public.audit_logs.id; - - --- --- Name: cache; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.cache ( - key character varying(255) NOT NULL, - value text NOT NULL, - expiration bigint NOT NULL -); - - --- --- Name: cache_locks; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.cache_locks ( - key character varying(255) NOT NULL, - owner character varying(255) NOT NULL, - expiration bigint NOT NULL -); - - --- --- Name: credit_ledger; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.credit_ledger ( - id bigint NOT NULL, - owner_type character varying(16) NOT NULL, - owner_id bigint NOT NULL, - amount bigint NOT NULL, - reason character varying(64) NOT NULL, - ref_type character varying(32), - ref_id bigint, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: credit_ledger_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.credit_ledger_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: credit_ledger_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.credit_ledger_id_seq OWNED BY public.credit_ledger.id; - - --- --- Name: currencies; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.currencies ( - id bigint NOT NULL, - code character varying(16) NOT NULL, - name character varying(64) NOT NULL, - decimal_places smallint DEFAULT '2'::smallint NOT NULL, - is_enabled boolean DEFAULT true NOT NULL, - is_bettable boolean DEFAULT false NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: currencies_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.currencies_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: currencies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.currencies_id_seq OWNED BY public.currencies.id; - - --- --- Name: draw_result_batches; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.draw_result_batches ( - id bigint NOT NULL, - draw_id bigint NOT NULL, - result_version integer NOT NULL, - source_type character varying(16) NOT NULL, - rng_seed_hash character varying(128), - raw_seed_encrypted text, - status character varying(32) NOT NULL, - created_by bigint, - confirmed_by bigint, - confirmed_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN draw_result_batches.source_type; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.draw_result_batches.source_type IS 'rng|manual'; - - --- --- Name: draw_result_batches_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.draw_result_batches_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: draw_result_batches_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.draw_result_batches_id_seq OWNED BY public.draw_result_batches.id; - - --- --- Name: draw_result_items; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.draw_result_items ( - id bigint NOT NULL, - draw_id bigint NOT NULL, - result_batch_id bigint NOT NULL, - prize_type character varying(32) NOT NULL, - prize_index integer DEFAULT 0 NOT NULL, - number_4d character(4) NOT NULL, - suffix_3d character(3), - suffix_2d character(2), - head_digit smallint, - tail_digit smallint, - created_at timestamp(0) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL -); - - --- --- Name: draw_result_items_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.draw_result_items_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: draw_result_items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.draw_result_items_id_seq OWNED BY public.draw_result_items.id; - - --- --- Name: draws; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.draws ( - id bigint NOT NULL, - draw_no character varying(32) NOT NULL, - business_date date NOT NULL, - sequence_no integer NOT NULL, - status character varying(32) NOT NULL, - start_time timestamp(0) without time zone, - close_time timestamp(0) without time zone, - draw_time timestamp(0) without time zone, - cooling_end_time timestamp(0) without time zone, - result_source character varying(16), - current_result_version integer DEFAULT 0 NOT NULL, - settle_version integer DEFAULT 0 NOT NULL, - is_reopened boolean DEFAULT false NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN draws.result_source; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.draws.result_source IS 'rng|manual'; - - --- --- Name: draws_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.draws_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: draws_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.draws_id_seq OWNED BY public.draws.id; - - --- --- Name: failed_jobs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.failed_jobs ( - id bigint NOT NULL, - uuid character varying(255) NOT NULL, - connection text NOT NULL, - queue text NOT NULL, - payload text NOT NULL, - exception text NOT NULL, - failed_at timestamp(0) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL -); - - --- --- Name: failed_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.failed_jobs_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: failed_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.failed_jobs_id_seq OWNED BY public.failed_jobs.id; - - --- --- Name: jackpot_contributions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.jackpot_contributions ( - id bigint NOT NULL, - jackpot_pool_id bigint NOT NULL, - draw_id bigint NOT NULL, - player_id bigint NOT NULL, - ticket_item_id bigint, - contribution_amount bigint DEFAULT '0'::bigint NOT NULL, - currency_code character varying(16) NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: jackpot_contributions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.jackpot_contributions_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: jackpot_contributions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.jackpot_contributions_id_seq OWNED BY public.jackpot_contributions.id; - - --- --- Name: jackpot_payout_logs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.jackpot_payout_logs ( - id bigint NOT NULL, - draw_id bigint NOT NULL, - jackpot_pool_id bigint NOT NULL, - trigger_type character varying(32) NOT NULL, - total_payout_amount bigint DEFAULT '0'::bigint NOT NULL, - winner_count integer DEFAULT 0 NOT NULL, - trigger_snapshot_json json, - created_at timestamp(0) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL -); - - --- --- Name: jackpot_payout_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.jackpot_payout_logs_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: jackpot_payout_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.jackpot_payout_logs_id_seq OWNED BY public.jackpot_payout_logs.id; - - --- --- Name: jackpot_pool_adjustments; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.jackpot_pool_adjustments ( - id bigint NOT NULL, - adjustment_no character varying(32) NOT NULL, - jackpot_pool_id bigint NOT NULL, - admin_user_id bigint NOT NULL, - amount_delta bigint NOT NULL, - balance_before bigint NOT NULL, - balance_after bigint NOT NULL, - reason character varying(500) NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN jackpot_pool_adjustments.amount_delta; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.jackpot_pool_adjustments.amount_delta IS 'signed minor units; + increase pool'; - - --- --- Name: jackpot_pool_adjustments_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.jackpot_pool_adjustments_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: jackpot_pool_adjustments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.jackpot_pool_adjustments_id_seq OWNED BY public.jackpot_pool_adjustments.id; - - --- --- Name: jackpot_pools; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.jackpot_pools ( - id bigint NOT NULL, - currency_code character varying(16) NOT NULL, - current_amount bigint DEFAULT '0'::bigint NOT NULL, - contribution_rate numeric(8,4) DEFAULT '0'::numeric NOT NULL, - trigger_threshold bigint DEFAULT '0'::bigint NOT NULL, - payout_rate numeric(8,4) DEFAULT '0'::numeric NOT NULL, - force_trigger_draw_gap integer DEFAULT 0 NOT NULL, - min_bet_amount bigint DEFAULT '0'::bigint NOT NULL, - status smallint DEFAULT '0'::smallint NOT NULL, - last_trigger_draw_id bigint, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - combo_trigger_play_codes json -); - - --- --- Name: COLUMN jackpot_pools.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.jackpot_pools.status IS '0=off,1=on'; - - --- --- Name: jackpot_pools_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.jackpot_pools_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: jackpot_pools_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.jackpot_pools_id_seq OWNED BY public.jackpot_pools.id; - - --- --- Name: job_batches; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.job_batches ( - id character varying(255) NOT NULL, - name character varying(255) NOT NULL, - total_jobs integer NOT NULL, - pending_jobs integer NOT NULL, - failed_jobs integer NOT NULL, - failed_job_ids text NOT NULL, - options text, - cancelled_at integer, - created_at integer NOT NULL, - finished_at integer -); - - --- --- Name: jobs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.jobs ( - id bigint NOT NULL, - queue character varying(255) NOT NULL, - payload text NOT NULL, - attempts smallint NOT NULL, - reserved_at integer, - available_at integer NOT NULL, - created_at integer NOT NULL -); - - --- --- Name: jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.jobs_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.jobs_id_seq OWNED BY public.jobs.id; - - --- --- Name: lottery_settings; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.lottery_settings ( - id bigint NOT NULL, - setting_key character varying(160) NOT NULL, - value_json json NOT NULL, - group_name character varying(64) DEFAULT 'general'::character varying NOT NULL, - description_zh character varying(255), - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN lottery_settings.group_name; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.lottery_settings.group_name IS '控制台分组展示用'; - - --- --- Name: COLUMN lottery_settings.description_zh; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.lottery_settings.description_zh IS '运维说明'; - - --- --- Name: lottery_settings_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.lottery_settings_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: lottery_settings_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.lottery_settings_id_seq OWNED BY public.lottery_settings.id; - - --- --- Name: migrations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.migrations ( - id integer NOT NULL, - migration character varying(255) NOT NULL, - batch integer NOT NULL -); - - --- --- Name: migrations_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.migrations_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: migrations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.migrations_id_seq OWNED BY public.migrations.id; - - --- --- Name: odds_items; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.odds_items ( - id bigint NOT NULL, - version_id bigint NOT NULL, - play_code character varying(32) NOT NULL, - prize_scope character varying(32) NOT NULL, - odds_value bigint DEFAULT '0'::bigint NOT NULL, - rebate_rate numeric(8,4) DEFAULT '0'::numeric NOT NULL, - commission_rate numeric(8,4) DEFAULT '0'::numeric NOT NULL, - currency_code character varying(16) NOT NULL, - extra_config_json json, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - dimension smallint -); - - --- --- Name: COLUMN odds_items.dimension; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.odds_items.dimension IS '2/3/4 维度,佣金按维度配置'; - - --- --- Name: odds_items_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.odds_items_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: odds_items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.odds_items_id_seq OWNED BY public.odds_items.id; - - --- --- Name: odds_versions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.odds_versions ( - id bigint NOT NULL, - version_no integer NOT NULL, - status character varying(16) NOT NULL, - effective_at timestamp(0) without time zone, - updated_by bigint, - reason character varying(255), - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: odds_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.odds_versions_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: odds_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.odds_versions_id_seq OWNED BY public.odds_versions.id; - - --- --- Name: payment_records; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.payment_records ( - id bigint NOT NULL, - settlement_bill_id bigint NOT NULL, - payer_type character varying(16) NOT NULL, - payer_id bigint NOT NULL, - payee_type character varying(16) NOT NULL, - payee_id bigint NOT NULL, - amount bigint NOT NULL, - method character varying(32), - status character varying(16) DEFAULT 'pending'::character varying NOT NULL, - created_by bigint, - confirmed_by bigint, - confirmed_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - proof text, - remark character varying(255) -); - - --- --- Name: payment_records_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.payment_records_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: payment_records_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.payment_records_id_seq OWNED BY public.payment_records.id; - - --- --- Name: personal_access_tokens; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.personal_access_tokens ( - id bigint NOT NULL, - tokenable_type character varying(255) NOT NULL, - tokenable_id bigint NOT NULL, - name text NOT NULL, - token character varying(64) NOT NULL, - abilities text, - last_used_at timestamp(0) without time zone, - expires_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: personal_access_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.personal_access_tokens_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: personal_access_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.personal_access_tokens_id_seq OWNED BY public.personal_access_tokens.id; - - --- --- Name: play_config_items; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.play_config_items ( - id bigint NOT NULL, - version_id bigint NOT NULL, - play_code character varying(32) NOT NULL, - is_enabled boolean DEFAULT true NOT NULL, - min_bet_amount bigint DEFAULT '0'::bigint NOT NULL, - max_bet_amount bigint DEFAULT '0'::bigint NOT NULL, - display_order integer DEFAULT 0 NOT NULL, - rule_text_zh text, - rule_text_en text, - rule_text_ne text, - extra_config_json json, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - category character varying(16), - dimension smallint, - bet_mode character varying(32), - supports_multi_number boolean DEFAULT false NOT NULL, - reserved_rule_json json, - display_name character varying(64) -); - - --- --- Name: play_config_items_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.play_config_items_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: play_config_items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.play_config_items_id_seq OWNED BY public.play_config_items.id; - - --- --- Name: play_config_versions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.play_config_versions ( - id bigint NOT NULL, - version_no integer NOT NULL, - status character varying(16) NOT NULL, - effective_at timestamp(0) without time zone, - updated_by bigint, - reason character varying(255), - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: play_config_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.play_config_versions_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: play_config_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.play_config_versions_id_seq OWNED BY public.play_config_versions.id; - - --- --- Name: play_types; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.play_types ( - id bigint NOT NULL, - play_code character varying(32) NOT NULL, - category character varying(16) NOT NULL, - dimension smallint, - bet_mode character varying(32), - is_enabled boolean DEFAULT true NOT NULL, - sort_order integer DEFAULT 0 NOT NULL, - supports_multi_number boolean DEFAULT false NOT NULL, - reserved_rule_json json, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - display_name character varying(64) -); - - --- --- Name: COLUMN play_types.dimension; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.play_types.dimension IS '2/3/4'; - - --- --- Name: play_types_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.play_types_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: play_types_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.play_types_id_seq OWNED BY public.play_types.id; - - --- --- Name: player_credit_accounts; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.player_credit_accounts ( - player_id bigint NOT NULL, - credit_limit bigint DEFAULT '0'::bigint NOT NULL, - used_credit bigint DEFAULT '0'::bigint NOT NULL, - frozen_credit bigint DEFAULT '0'::bigint NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: player_rebate_profiles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.player_rebate_profiles ( - id bigint NOT NULL, - player_id bigint NOT NULL, - game_type character varying(32) DEFAULT '*'::character varying NOT NULL, - inherit_from_agent boolean DEFAULT true NOT NULL, - rebate_rate numeric(8,4) DEFAULT '0'::numeric NOT NULL, - extra_rebate_rate numeric(8,4) DEFAULT '0'::numeric NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: player_rebate_profiles_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.player_rebate_profiles_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: player_rebate_profiles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.player_rebate_profiles_id_seq OWNED BY public.player_rebate_profiles.id; - - --- --- Name: player_wallets; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.player_wallets ( - id bigint NOT NULL, - player_id bigint NOT NULL, - wallet_type character varying(32) DEFAULT 'lottery'::character varying NOT NULL, - currency_code character varying(16) NOT NULL, - balance bigint DEFAULT '0'::bigint NOT NULL, - frozen_balance bigint DEFAULT '0'::bigint NOT NULL, - status smallint DEFAULT '0'::smallint NOT NULL, - version bigint DEFAULT '0'::bigint NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN player_wallets.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.player_wallets.status IS '0=active,1=frozen'; - - --- --- Name: player_wallets_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.player_wallets_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: player_wallets_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.player_wallets_id_seq OWNED BY public.player_wallets.id; - - --- --- Name: players; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.players ( - id bigint NOT NULL, - site_code character varying(64) NOT NULL, - site_player_id character varying(128) NOT NULL, - username character varying(128), - nickname character varying(128), - default_currency character varying(16) DEFAULT 'NPR'::character varying NOT NULL, - status smallint DEFAULT '0'::smallint NOT NULL, - last_login_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - agent_node_id bigint, - auth_source character varying(16) DEFAULT 'main_site_sso'::character varying NOT NULL, - funding_mode character varying(16) DEFAULT 'wallet'::character varying NOT NULL, - password_hash character varying(255), - login_failed_count smallint DEFAULT '0'::smallint NOT NULL, - login_locked_until timestamp(0) without time zone, - risk_tags json -); - - --- --- Name: COLUMN players.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.players.status IS '0=active,1=frozen,2=blocked'; - - --- --- Name: players_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.players_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: players_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.players_id_seq OWNED BY public.players.id; - - --- --- Name: rebate_allocations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.rebate_allocations ( - id bigint NOT NULL, - rebate_record_id bigint NOT NULL, - settlement_bill_id bigint, - participant_type character varying(16) NOT NULL, - participant_id bigint DEFAULT '0'::bigint NOT NULL, - actual_share_rate numeric(5,2) DEFAULT '0'::numeric NOT NULL, - allocated_amount bigint DEFAULT '0'::bigint NOT NULL, - allocation_rule character varying(32) DEFAULT 'share'::character varying NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: rebate_allocations_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.rebate_allocations_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: rebate_allocations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.rebate_allocations_id_seq OWNED BY public.rebate_allocations.id; - - --- --- Name: rebate_records; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.rebate_records ( - id bigint NOT NULL, - player_id bigint NOT NULL, - settlement_period_id bigint, - game_type character varying(32) DEFAULT '*'::character varying NOT NULL, - valid_bet_amount bigint DEFAULT '0'::bigint NOT NULL, - rebate_rate numeric(8,4) DEFAULT '0'::numeric NOT NULL, - rebate_amount bigint DEFAULT '0'::bigint NOT NULL, - rebate_type character varying(16) DEFAULT 'basic'::character varying NOT NULL, - owner_agent_id bigint, - status character varying(16) DEFAULT 'pending'::character varying NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - ticket_item_id bigint, - reversal_of_id bigint -); - - --- --- Name: rebate_records_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.rebate_records_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: rebate_records_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.rebate_records_id_seq OWNED BY public.rebate_records.id; - - --- --- Name: reconcile_items; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.reconcile_items ( - id bigint NOT NULL, - reconcile_job_id bigint NOT NULL, - side_a_ref character varying(128), - side_b_ref character varying(128), - difference_amount bigint DEFAULT '0'::bigint NOT NULL, - status character varying(32) NOT NULL, - resolved_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: reconcile_items_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.reconcile_items_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: reconcile_items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.reconcile_items_id_seq OWNED BY public.reconcile_items.id; - - --- --- Name: reconcile_jobs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.reconcile_jobs ( - id bigint NOT NULL, - job_no character varying(64) NOT NULL, - reconcile_type character varying(32) NOT NULL, - status character varying(32) NOT NULL, - period_start timestamp(0) without time zone, - period_end timestamp(0) without time zone, - summary_json json, - finished_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - admin_user_id bigint -); - - --- --- Name: reconcile_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.reconcile_jobs_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: reconcile_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.reconcile_jobs_id_seq OWNED BY public.reconcile_jobs.id; - - --- --- Name: report_jobs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.report_jobs ( - id bigint NOT NULL, - job_no character varying(64) NOT NULL, - admin_user_id bigint, - report_type character varying(64) NOT NULL, - export_format character varying(16) DEFAULT 'csv'::character varying NOT NULL, - filter_json json, - status character varying(32) NOT NULL, - output_path character varying(512), - error_message text, - finished_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: report_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.report_jobs_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: report_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.report_jobs_id_seq OWNED BY public.report_jobs.id; - - --- --- Name: risk_cap_items; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.risk_cap_items ( - id bigint NOT NULL, - version_id bigint NOT NULL, - draw_id bigint, - normalized_number character(4) NOT NULL, - cap_amount bigint NOT NULL, - cap_type character varying(16) NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: risk_cap_items_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.risk_cap_items_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: risk_cap_items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.risk_cap_items_id_seq OWNED BY public.risk_cap_items.id; - - --- --- Name: risk_cap_versions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.risk_cap_versions ( - id bigint NOT NULL, - version_no integer NOT NULL, - status character varying(16) NOT NULL, - effective_at timestamp(0) without time zone, - updated_by bigint, - reason character varying(255), - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: risk_cap_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.risk_cap_versions_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: risk_cap_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.risk_cap_versions_id_seq OWNED BY public.risk_cap_versions.id; - - --- --- Name: risk_pool_lock_logs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.risk_pool_lock_logs ( - id bigint NOT NULL, - draw_id bigint NOT NULL, - normalized_number character(4) NOT NULL, - ticket_item_id bigint, - action_type character varying(16) NOT NULL, - amount bigint DEFAULT '0'::bigint NOT NULL, - source_reason character varying(32), - created_at timestamp(0) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL -); - - --- --- Name: risk_pool_lock_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.risk_pool_lock_logs_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: risk_pool_lock_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.risk_pool_lock_logs_id_seq OWNED BY public.risk_pool_lock_logs.id; - - --- --- Name: risk_pools; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.risk_pools ( - id bigint NOT NULL, - draw_id bigint NOT NULL, - normalized_number character(4) NOT NULL, - total_cap_amount bigint DEFAULT '0'::bigint NOT NULL, - locked_amount bigint DEFAULT '0'::bigint NOT NULL, - remaining_amount bigint DEFAULT '0'::bigint NOT NULL, - sold_out_status smallint DEFAULT '0'::smallint NOT NULL, - version bigint DEFAULT '0'::bigint NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: risk_pools_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.risk_pools_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: risk_pools_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.risk_pools_id_seq OWNED BY public.risk_pools.id; - - --- --- Name: sessions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.sessions ( - id character varying(255) NOT NULL, - user_id bigint, - ip_address character varying(45), - user_agent text, - payload text NOT NULL, - last_activity integer NOT NULL -); - - --- --- Name: settlement_adjustments; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.settlement_adjustments ( - id bigint NOT NULL, - settlement_period_id bigint, - original_bill_id bigint, - adjustment_type character varying(32) NOT NULL, - amount bigint NOT NULL, - reason character varying(255), - created_by bigint, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: settlement_adjustments_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.settlement_adjustments_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: settlement_adjustments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.settlement_adjustments_id_seq OWNED BY public.settlement_adjustments.id; - - --- --- Name: settlement_batches; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.settlement_batches ( - id bigint NOT NULL, - draw_id bigint NOT NULL, - result_batch_id bigint NOT NULL, - settle_version integer DEFAULT 1 NOT NULL, - status character varying(32) NOT NULL, - total_ticket_count integer DEFAULT 0 NOT NULL, - total_win_count integer DEFAULT 0 NOT NULL, - total_payout_amount bigint DEFAULT '0'::bigint NOT NULL, - total_jackpot_payout_amount bigint DEFAULT '0'::bigint NOT NULL, - review_status character varying(32) DEFAULT 'pending'::character varying NOT NULL, - reviewed_by bigint, - reviewed_at timestamp(0) without time zone, - review_remark character varying(255), - paid_at timestamp(0) without time zone, - started_at timestamp(0) without time zone, - finished_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: settlement_batches_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.settlement_batches_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: settlement_batches_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.settlement_batches_id_seq OWNED BY public.settlement_batches.id; - - --- --- Name: settlement_bills; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.settlement_bills ( - id bigint NOT NULL, - settlement_period_id bigint NOT NULL, - bill_type character varying(16) NOT NULL, - owner_type character varying(16) NOT NULL, - owner_id bigint NOT NULL, - counterparty_type character varying(16) NOT NULL, - counterparty_id bigint NOT NULL, - gross_win_loss bigint DEFAULT '0'::bigint NOT NULL, - rebate_amount bigint DEFAULT '0'::bigint NOT NULL, - adjustment_amount bigint DEFAULT '0'::bigint NOT NULL, - net_amount bigint DEFAULT '0'::bigint NOT NULL, - paid_amount bigint DEFAULT '0'::bigint NOT NULL, - unpaid_amount bigint DEFAULT '0'::bigint NOT NULL, - status character varying(16) DEFAULT 'pending'::character varying NOT NULL, - confirmed_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - locked_at timestamp(0) without time zone, - reversed_bill_id bigint, - meta_json json, - platform_rounding_adjustment bigint DEFAULT '0'::bigint NOT NULL -); - - --- --- Name: settlement_bills_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.settlement_bills_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: settlement_bills_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.settlement_bills_id_seq OWNED BY public.settlement_bills.id; - - --- --- Name: settlement_periods; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.settlement_periods ( - id bigint NOT NULL, - admin_site_id bigint NOT NULL, - period_start timestamp(0) without time zone NOT NULL, - period_end timestamp(0) without time zone NOT NULL, - status character varying(16) DEFAULT 'open'::character varying NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: settlement_periods_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.settlement_periods_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: settlement_periods_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.settlement_periods_id_seq OWNED BY public.settlement_periods.id; - - --- --- Name: share_ledger; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.share_ledger ( - id bigint NOT NULL, - ticket_item_id bigint NOT NULL, - player_id bigint NOT NULL, - agent_node_id bigint, - agent_path json, - share_snapshot json, - game_win_loss bigint DEFAULT '0'::bigint NOT NULL, - basic_rebate bigint DEFAULT '0'::bigint NOT NULL, - shared_net_win_loss bigint DEFAULT '0'::bigint NOT NULL, - allocations_json json, - settlement_period_id bigint, - reversal_of_id bigint, - settled_at timestamp(0) without time zone NOT NULL, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: share_ledger_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.share_ledger_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: share_ledger_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.share_ledger_id_seq OWNED BY public.share_ledger.id; - - --- --- Name: ticket_combinations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.ticket_combinations ( - id bigint NOT NULL, - ticket_item_id bigint NOT NULL, - combination_no integer DEFAULT 0 NOT NULL, - number_4d character(4) NOT NULL, - bet_amount bigint DEFAULT '0'::bigint NOT NULL, - estimated_payout bigint DEFAULT '0'::bigint NOT NULL, - created_at timestamp(0) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL -); - - --- --- Name: ticket_combinations_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.ticket_combinations_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: ticket_combinations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.ticket_combinations_id_seq OWNED BY public.ticket_combinations.id; - - --- --- Name: ticket_items; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.ticket_items ( - id bigint NOT NULL, - ticket_no character varying(64) NOT NULL, - order_id bigint NOT NULL, - player_id bigint NOT NULL, - draw_id bigint NOT NULL, - original_number character varying(32), - normalized_number character(4) NOT NULL, - play_code character varying(32) NOT NULL, - dimension smallint, - digit_slot smallint, - bet_mode character varying(32), - unit_bet_amount bigint DEFAULT '0'::bigint NOT NULL, - total_bet_amount bigint DEFAULT '0'::bigint NOT NULL, - rebate_rate_snapshot numeric(8,4) DEFAULT '0'::numeric NOT NULL, - commission_rate_snapshot numeric(8,4) DEFAULT '0'::numeric NOT NULL, - actual_deduct_amount bigint DEFAULT '0'::bigint NOT NULL, - odds_snapshot_json json, - rule_snapshot_json json, - combination_count integer DEFAULT 1 NOT NULL, - estimated_max_payout bigint DEFAULT '0'::bigint NOT NULL, - risk_locked_amount bigint DEFAULT '0'::bigint NOT NULL, - status character varying(32) NOT NULL, - fail_reason_code character varying(32), - fail_reason_text character varying(255), - win_amount bigint DEFAULT '0'::bigint NOT NULL, - jackpot_win_amount bigint DEFAULT '0'::bigint NOT NULL, - settled_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - agent_node_id bigint, - share_snapshot json, - agent_rebate_rate_snapshot numeric(8,4), - agent_settled_at timestamp(0) without time zone, - agent_settlement_reversal_of_id bigint -); - - --- --- Name: COLUMN ticket_items.dimension; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.ticket_items.dimension IS '2/3/4'; - - --- --- Name: COLUMN ticket_items.digit_slot; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.ticket_items.digit_slot IS '千百十个位,领域字典'; - - --- --- Name: ticket_items_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.ticket_items_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: ticket_items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.ticket_items_id_seq OWNED BY public.ticket_items.id; - - --- --- Name: ticket_orders; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.ticket_orders ( - id bigint NOT NULL, - order_no character varying(64) NOT NULL, - player_id bigint NOT NULL, - draw_id bigint NOT NULL, - currency_code character varying(16) NOT NULL, - total_bet_amount bigint DEFAULT '0'::bigint NOT NULL, - total_rebate_amount bigint DEFAULT '0'::bigint NOT NULL, - total_actual_deduct bigint DEFAULT '0'::bigint NOT NULL, - total_estimated_payout bigint DEFAULT '0'::bigint NOT NULL, - status character varying(32) NOT NULL, - submit_source character varying(16) DEFAULT 'h5'::character varying NOT NULL, - client_trace_id character varying(64), - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone, - play_config_version_no integer DEFAULT 0 NOT NULL, - odds_version_no integer DEFAULT 0 NOT NULL, - risk_cap_version_no integer DEFAULT 0 NOT NULL -); - - --- --- Name: ticket_orders_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.ticket_orders_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: ticket_orders_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.ticket_orders_id_seq OWNED BY public.ticket_orders.id; - - --- --- Name: ticket_settlement_details; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.ticket_settlement_details ( - id bigint NOT NULL, - settlement_batch_id bigint NOT NULL, - ticket_item_id bigint NOT NULL, - matched_prize_tier character varying(32), - win_amount bigint DEFAULT '0'::bigint NOT NULL, - jackpot_allocation_amount bigint DEFAULT '0'::bigint NOT NULL, - match_detail_json json, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: ticket_settlement_details_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.ticket_settlement_details_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: ticket_settlement_details_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.ticket_settlement_details_id_seq OWNED BY public.ticket_settlement_details.id; - - --- --- Name: transfer_orders; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.transfer_orders ( - id bigint NOT NULL, - transfer_no character varying(64) NOT NULL, - player_id bigint NOT NULL, - direction character varying(16) NOT NULL, - currency_code character varying(16) NOT NULL, - amount bigint NOT NULL, - idempotent_key character varying(64) NOT NULL, - status character varying(32) NOT NULL, - external_request_payload json, - external_response_payload json, - external_ref_no character varying(64), - fail_reason character varying(255), - finished_at timestamp(0) without time zone, - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: transfer_orders_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.transfer_orders_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: transfer_orders_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.transfer_orders_id_seq OWNED BY public.transfer_orders.id; - - --- --- Name: wallet_txns; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.wallet_txns ( - id bigint NOT NULL, - txn_no character varying(64) NOT NULL, - player_id bigint NOT NULL, - wallet_id bigint NOT NULL, - biz_type character varying(32) NOT NULL, - biz_no character varying(64), - direction smallint NOT NULL, - amount bigint NOT NULL, - balance_before bigint NOT NULL, - balance_after bigint NOT NULL, - status character varying(32) NOT NULL, - external_ref_no character varying(64), - idempotent_key character varying(64), - remark character varying(255), - created_at timestamp(0) without time zone, - updated_at timestamp(0) without time zone -); - - --- --- Name: COLUMN wallet_txns.direction; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.wallet_txns.direction IS '1=in,2=out'; - - --- --- Name: wallet_txns_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.wallet_txns_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: wallet_txns_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.wallet_txns_id_seq OWNED BY public.wallet_txns.id; - - --- --- Name: admin_action_catalog id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_action_catalog ALTER COLUMN id SET DEFAULT nextval('public.admin_action_catalog_id_seq'::regclass); - - --- --- Name: admin_api_resource_bindings id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_api_resource_bindings ALTER COLUMN id SET DEFAULT nextval('public.admin_api_resource_bindings_id_seq'::regclass); - - --- --- Name: admin_api_resources id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_api_resources ALTER COLUMN id SET DEFAULT nextval('public.admin_api_resources_id_seq'::regclass); - - --- --- Name: admin_menu_actions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menu_actions ALTER COLUMN id SET DEFAULT nextval('public.admin_menu_actions_id_seq'::regclass); - - --- --- Name: admin_menus id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menus ALTER COLUMN id SET DEFAULT nextval('public.admin_menus_id_seq'::regclass); - - --- --- Name: admin_roles id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_roles ALTER COLUMN id SET DEFAULT nextval('public.admin_roles_id_seq'::regclass); - - --- --- Name: admin_sites id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_sites ALTER COLUMN id SET DEFAULT nextval('public.admin_sites_id_seq'::regclass); - - --- --- Name: admin_users id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_users ALTER COLUMN id SET DEFAULT nextval('public.admin_users_id_seq'::regclass); - - --- --- Name: agent_delegation_grants id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_delegation_grants ALTER COLUMN id SET DEFAULT nextval('public.agent_delegation_grants_id_seq'::regclass); - - --- --- Name: agent_nodes id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_nodes ALTER COLUMN id SET DEFAULT nextval('public.agent_nodes_id_seq'::regclass); - - --- --- Name: audit_logs id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.audit_logs ALTER COLUMN id SET DEFAULT nextval('public.audit_logs_id_seq'::regclass); - - --- --- Name: credit_ledger id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.credit_ledger ALTER COLUMN id SET DEFAULT nextval('public.credit_ledger_id_seq'::regclass); - - --- --- Name: currencies id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.currencies ALTER COLUMN id SET DEFAULT nextval('public.currencies_id_seq'::regclass); - - --- --- Name: draw_result_batches id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_batches ALTER COLUMN id SET DEFAULT nextval('public.draw_result_batches_id_seq'::regclass); - - --- --- Name: draw_result_items id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_items ALTER COLUMN id SET DEFAULT nextval('public.draw_result_items_id_seq'::regclass); - - --- --- Name: draws id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draws ALTER COLUMN id SET DEFAULT nextval('public.draws_id_seq'::regclass); - - --- --- Name: failed_jobs id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.failed_jobs ALTER COLUMN id SET DEFAULT nextval('public.failed_jobs_id_seq'::regclass); - - --- --- Name: jackpot_contributions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_contributions ALTER COLUMN id SET DEFAULT nextval('public.jackpot_contributions_id_seq'::regclass); - - --- --- Name: jackpot_payout_logs id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_payout_logs ALTER COLUMN id SET DEFAULT nextval('public.jackpot_payout_logs_id_seq'::regclass); - - --- --- Name: jackpot_pool_adjustments id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_pool_adjustments ALTER COLUMN id SET DEFAULT nextval('public.jackpot_pool_adjustments_id_seq'::regclass); - - --- --- Name: jackpot_pools id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_pools ALTER COLUMN id SET DEFAULT nextval('public.jackpot_pools_id_seq'::regclass); - - --- --- Name: jobs id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jobs ALTER COLUMN id SET DEFAULT nextval('public.jobs_id_seq'::regclass); - - --- --- Name: lottery_settings id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.lottery_settings ALTER COLUMN id SET DEFAULT nextval('public.lottery_settings_id_seq'::regclass); - - --- --- Name: migrations id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migrations ALTER COLUMN id SET DEFAULT nextval('public.migrations_id_seq'::regclass); - - --- --- Name: odds_items id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.odds_items ALTER COLUMN id SET DEFAULT nextval('public.odds_items_id_seq'::regclass); - - --- --- Name: odds_versions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.odds_versions ALTER COLUMN id SET DEFAULT nextval('public.odds_versions_id_seq'::regclass); - - --- --- Name: payment_records id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.payment_records ALTER COLUMN id SET DEFAULT nextval('public.payment_records_id_seq'::regclass); - - --- --- Name: personal_access_tokens id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.personal_access_tokens ALTER COLUMN id SET DEFAULT nextval('public.personal_access_tokens_id_seq'::regclass); - - --- --- Name: play_config_items id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_config_items ALTER COLUMN id SET DEFAULT nextval('public.play_config_items_id_seq'::regclass); - - --- --- Name: play_config_versions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_config_versions ALTER COLUMN id SET DEFAULT nextval('public.play_config_versions_id_seq'::regclass); - - --- --- Name: play_types id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_types ALTER COLUMN id SET DEFAULT nextval('public.play_types_id_seq'::regclass); - - --- --- Name: player_rebate_profiles id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_rebate_profiles ALTER COLUMN id SET DEFAULT nextval('public.player_rebate_profiles_id_seq'::regclass); - - --- --- Name: player_wallets id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_wallets ALTER COLUMN id SET DEFAULT nextval('public.player_wallets_id_seq'::regclass); - - --- --- Name: players id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.players ALTER COLUMN id SET DEFAULT nextval('public.players_id_seq'::regclass); - - --- --- Name: rebate_allocations id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_allocations ALTER COLUMN id SET DEFAULT nextval('public.rebate_allocations_id_seq'::regclass); - - --- --- Name: rebate_records id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_records ALTER COLUMN id SET DEFAULT nextval('public.rebate_records_id_seq'::regclass); - - --- --- Name: reconcile_items id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reconcile_items ALTER COLUMN id SET DEFAULT nextval('public.reconcile_items_id_seq'::regclass); - - --- --- Name: reconcile_jobs id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reconcile_jobs ALTER COLUMN id SET DEFAULT nextval('public.reconcile_jobs_id_seq'::regclass); - - --- --- Name: report_jobs id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.report_jobs ALTER COLUMN id SET DEFAULT nextval('public.report_jobs_id_seq'::regclass); - - --- --- Name: risk_cap_items id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_cap_items ALTER COLUMN id SET DEFAULT nextval('public.risk_cap_items_id_seq'::regclass); - - --- --- Name: risk_cap_versions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_cap_versions ALTER COLUMN id SET DEFAULT nextval('public.risk_cap_versions_id_seq'::regclass); - - --- --- Name: risk_pool_lock_logs id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_pool_lock_logs ALTER COLUMN id SET DEFAULT nextval('public.risk_pool_lock_logs_id_seq'::regclass); - - --- --- Name: risk_pools id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_pools ALTER COLUMN id SET DEFAULT nextval('public.risk_pools_id_seq'::regclass); - - --- --- Name: settlement_adjustments id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_adjustments ALTER COLUMN id SET DEFAULT nextval('public.settlement_adjustments_id_seq'::regclass); - - --- --- Name: settlement_batches id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_batches ALTER COLUMN id SET DEFAULT nextval('public.settlement_batches_id_seq'::regclass); - - --- --- Name: settlement_bills id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_bills ALTER COLUMN id SET DEFAULT nextval('public.settlement_bills_id_seq'::regclass); - - --- --- Name: settlement_periods id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_periods ALTER COLUMN id SET DEFAULT nextval('public.settlement_periods_id_seq'::regclass); - - --- --- Name: share_ledger id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.share_ledger ALTER COLUMN id SET DEFAULT nextval('public.share_ledger_id_seq'::regclass); - - --- --- Name: ticket_combinations id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_combinations ALTER COLUMN id SET DEFAULT nextval('public.ticket_combinations_id_seq'::regclass); - - --- --- Name: ticket_items id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_items ALTER COLUMN id SET DEFAULT nextval('public.ticket_items_id_seq'::regclass); - - --- --- Name: ticket_orders id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_orders ALTER COLUMN id SET DEFAULT nextval('public.ticket_orders_id_seq'::regclass); - - --- --- Name: ticket_settlement_details id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_settlement_details ALTER COLUMN id SET DEFAULT nextval('public.ticket_settlement_details_id_seq'::regclass); - - --- --- Name: transfer_orders id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.transfer_orders ALTER COLUMN id SET DEFAULT nextval('public.transfer_orders_id_seq'::regclass); - - --- --- Name: wallet_txns id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.wallet_txns ALTER COLUMN id SET DEFAULT nextval('public.wallet_txns_id_seq'::regclass); - - --- --- Name: admin_action_catalog admin_action_catalog_code_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_action_catalog - ADD CONSTRAINT admin_action_catalog_code_unique UNIQUE (code); - - --- --- Name: admin_action_catalog admin_action_catalog_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_action_catalog - ADD CONSTRAINT admin_action_catalog_pkey PRIMARY KEY (id); - - --- --- Name: admin_api_resource_bindings admin_api_resource_bindings_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_api_resource_bindings - ADD CONSTRAINT admin_api_resource_bindings_pkey PRIMARY KEY (id); - - --- --- Name: admin_api_resources admin_api_resources_code_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_api_resources - ADD CONSTRAINT admin_api_resources_code_unique UNIQUE (code); - - --- --- Name: admin_api_resources admin_api_resources_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_api_resources - ADD CONSTRAINT admin_api_resources_pkey PRIMARY KEY (id); - - --- --- Name: admin_menu_actions admin_menu_actions_permission_code_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menu_actions - ADD CONSTRAINT admin_menu_actions_permission_code_unique UNIQUE (permission_code); - - --- --- Name: admin_menu_actions admin_menu_actions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menu_actions - ADD CONSTRAINT admin_menu_actions_pkey PRIMARY KEY (id); - - --- --- Name: admin_menus admin_menus_code_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menus - ADD CONSTRAINT admin_menus_code_unique UNIQUE (code); - - --- --- Name: admin_menus admin_menus_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menus - ADD CONSTRAINT admin_menus_pkey PRIMARY KEY (id); - - --- --- Name: admin_role_menu_actions admin_role_menu_actions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_role_menu_actions - ADD CONSTRAINT admin_role_menu_actions_pkey PRIMARY KEY (role_id, menu_action_id); - - --- --- Name: admin_roles admin_roles_code_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_roles - ADD CONSTRAINT admin_roles_code_unique UNIQUE (code); - - --- --- Name: admin_roles admin_roles_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_roles - ADD CONSTRAINT admin_roles_pkey PRIMARY KEY (id); - - --- --- Name: admin_roles admin_roles_slug_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_roles - ADD CONSTRAINT admin_roles_slug_unique UNIQUE (slug); - - --- --- Name: admin_sites admin_sites_code_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_sites - ADD CONSTRAINT admin_sites_code_unique UNIQUE (code); - - --- --- Name: admin_sites admin_sites_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_sites - ADD CONSTRAINT admin_sites_pkey PRIMARY KEY (id); - - --- --- Name: admin_user_agent_roles admin_user_agent_roles_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_agent_roles - ADD CONSTRAINT admin_user_agent_roles_pkey PRIMARY KEY (admin_user_id, agent_node_id, role_id); - - --- --- Name: admin_user_agents admin_user_agents_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_agents - ADD CONSTRAINT admin_user_agents_pkey PRIMARY KEY (admin_user_id); - - --- --- Name: admin_user_menu_actions admin_user_menu_actions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_menu_actions - ADD CONSTRAINT admin_user_menu_actions_pkey PRIMARY KEY (admin_user_id, site_id, menu_action_id); - - --- --- Name: admin_user_site_roles admin_user_site_roles_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_site_roles - ADD CONSTRAINT admin_user_site_roles_pkey PRIMARY KEY (admin_user_id, site_id, role_id); - - --- --- Name: admin_users admin_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_users - ADD CONSTRAINT admin_users_pkey PRIMARY KEY (id); - - --- --- Name: admin_users admin_users_username_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_users - ADD CONSTRAINT admin_users_username_unique UNIQUE (username); - - --- --- Name: agent_delegation_grants agent_delegation_grants_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_delegation_grants - ADD CONSTRAINT agent_delegation_grants_pkey PRIMARY KEY (id); - - --- --- Name: agent_nodes agent_nodes_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_nodes - ADD CONSTRAINT agent_nodes_pkey PRIMARY KEY (id); - - --- --- Name: agent_profiles agent_profiles_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_profiles - ADD CONSTRAINT agent_profiles_pkey PRIMARY KEY (agent_node_id); - - --- --- Name: audit_logs audit_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.audit_logs - ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id); - - --- --- Name: cache_locks cache_locks_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.cache_locks - ADD CONSTRAINT cache_locks_pkey PRIMARY KEY (key); - - --- --- Name: cache cache_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.cache - ADD CONSTRAINT cache_pkey PRIMARY KEY (key); - - --- --- Name: credit_ledger credit_ledger_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.credit_ledger - ADD CONSTRAINT credit_ledger_pkey PRIMARY KEY (id); - - --- --- Name: currencies currencies_code_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.currencies - ADD CONSTRAINT currencies_code_unique UNIQUE (code); - - --- --- Name: currencies currencies_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.currencies - ADD CONSTRAINT currencies_pkey PRIMARY KEY (id); - - --- --- Name: draw_result_batches draw_result_batches_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_batches - ADD CONSTRAINT draw_result_batches_pkey PRIMARY KEY (id); - - --- --- Name: draw_result_items draw_result_items_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_items - ADD CONSTRAINT draw_result_items_pkey PRIMARY KEY (id); - - --- --- Name: draws draws_draw_no_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draws - ADD CONSTRAINT draws_draw_no_unique UNIQUE (draw_no); - - --- --- Name: draws draws_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draws - ADD CONSTRAINT draws_pkey PRIMARY KEY (id); - - --- --- Name: failed_jobs failed_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.failed_jobs - ADD CONSTRAINT failed_jobs_pkey PRIMARY KEY (id); - - --- --- Name: failed_jobs failed_jobs_uuid_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.failed_jobs - ADD CONSTRAINT failed_jobs_uuid_unique UNIQUE (uuid); - - --- --- Name: jackpot_contributions jackpot_contributions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_contributions - ADD CONSTRAINT jackpot_contributions_pkey PRIMARY KEY (id); - - --- --- Name: jackpot_payout_logs jackpot_payout_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_payout_logs - ADD CONSTRAINT jackpot_payout_logs_pkey PRIMARY KEY (id); - - --- --- Name: jackpot_pool_adjustments jackpot_pool_adjustments_adjustment_no_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_pool_adjustments - ADD CONSTRAINT jackpot_pool_adjustments_adjustment_no_unique UNIQUE (adjustment_no); - - --- --- Name: jackpot_pool_adjustments jackpot_pool_adjustments_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_pool_adjustments - ADD CONSTRAINT jackpot_pool_adjustments_pkey PRIMARY KEY (id); - - --- --- Name: jackpot_pools jackpot_pools_currency_code_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_pools - ADD CONSTRAINT jackpot_pools_currency_code_unique UNIQUE (currency_code); - - --- --- Name: jackpot_pools jackpot_pools_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_pools - ADD CONSTRAINT jackpot_pools_pkey PRIMARY KEY (id); - - --- --- Name: job_batches job_batches_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.job_batches - ADD CONSTRAINT job_batches_pkey PRIMARY KEY (id); - - --- --- Name: jobs jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jobs - ADD CONSTRAINT jobs_pkey PRIMARY KEY (id); - - --- --- Name: lottery_settings lottery_settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.lottery_settings - ADD CONSTRAINT lottery_settings_pkey PRIMARY KEY (id); - - --- --- Name: lottery_settings lottery_settings_setting_key_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.lottery_settings - ADD CONSTRAINT lottery_settings_setting_key_unique UNIQUE (setting_key); - - --- --- Name: migrations migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migrations - ADD CONSTRAINT migrations_pkey PRIMARY KEY (id); - - --- --- Name: odds_items odds_items_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.odds_items - ADD CONSTRAINT odds_items_pkey PRIMARY KEY (id); - - --- --- Name: odds_versions odds_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.odds_versions - ADD CONSTRAINT odds_versions_pkey PRIMARY KEY (id); - - --- --- Name: payment_records payment_records_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.payment_records - ADD CONSTRAINT payment_records_pkey PRIMARY KEY (id); - - --- --- Name: personal_access_tokens personal_access_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.personal_access_tokens - ADD CONSTRAINT personal_access_tokens_pkey PRIMARY KEY (id); - - --- --- Name: personal_access_tokens personal_access_tokens_token_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.personal_access_tokens - ADD CONSTRAINT personal_access_tokens_token_unique UNIQUE (token); - - --- --- Name: play_config_items play_config_items_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_config_items - ADD CONSTRAINT play_config_items_pkey PRIMARY KEY (id); - - --- --- Name: play_config_versions play_config_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_config_versions - ADD CONSTRAINT play_config_versions_pkey PRIMARY KEY (id); - - --- --- Name: play_types play_types_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_types - ADD CONSTRAINT play_types_pkey PRIMARY KEY (id); - - --- --- Name: play_types play_types_play_code_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_types - ADD CONSTRAINT play_types_play_code_unique UNIQUE (play_code); - - --- --- Name: player_credit_accounts player_credit_accounts_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_credit_accounts - ADD CONSTRAINT player_credit_accounts_pkey PRIMARY KEY (player_id); - - --- --- Name: player_rebate_profiles player_rebate_profiles_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_rebate_profiles - ADD CONSTRAINT player_rebate_profiles_pkey PRIMARY KEY (id); - - --- --- Name: player_rebate_profiles player_rebate_profiles_player_id_game_type_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_rebate_profiles - ADD CONSTRAINT player_rebate_profiles_player_id_game_type_unique UNIQUE (player_id, game_type); - - --- --- Name: player_wallets player_wallets_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_wallets - ADD CONSTRAINT player_wallets_pkey PRIMARY KEY (id); - - --- --- Name: players players_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.players - ADD CONSTRAINT players_pkey PRIMARY KEY (id); - - --- --- Name: rebate_allocations rebate_allocations_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_allocations - ADD CONSTRAINT rebate_allocations_pkey PRIMARY KEY (id); - - --- --- Name: rebate_records rebate_records_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_records - ADD CONSTRAINT rebate_records_pkey PRIMARY KEY (id); - - --- --- Name: reconcile_items reconcile_items_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reconcile_items - ADD CONSTRAINT reconcile_items_pkey PRIMARY KEY (id); - - --- --- Name: reconcile_jobs reconcile_jobs_job_no_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reconcile_jobs - ADD CONSTRAINT reconcile_jobs_job_no_unique UNIQUE (job_no); - - --- --- Name: reconcile_jobs reconcile_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reconcile_jobs - ADD CONSTRAINT reconcile_jobs_pkey PRIMARY KEY (id); - - --- --- Name: report_jobs report_jobs_job_no_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.report_jobs - ADD CONSTRAINT report_jobs_job_no_unique UNIQUE (job_no); - - --- --- Name: report_jobs report_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.report_jobs - ADD CONSTRAINT report_jobs_pkey PRIMARY KEY (id); - - --- --- Name: risk_cap_items risk_cap_items_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_cap_items - ADD CONSTRAINT risk_cap_items_pkey PRIMARY KEY (id); - - --- --- Name: risk_cap_versions risk_cap_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_cap_versions - ADD CONSTRAINT risk_cap_versions_pkey PRIMARY KEY (id); - - --- --- Name: risk_pool_lock_logs risk_pool_lock_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_pool_lock_logs - ADD CONSTRAINT risk_pool_lock_logs_pkey PRIMARY KEY (id); - - --- --- Name: risk_pools risk_pools_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_pools - ADD CONSTRAINT risk_pools_pkey PRIMARY KEY (id); - - --- --- Name: sessions sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.sessions - ADD CONSTRAINT sessions_pkey PRIMARY KEY (id); - - --- --- Name: settlement_adjustments settlement_adjustments_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_adjustments - ADD CONSTRAINT settlement_adjustments_pkey PRIMARY KEY (id); - - --- --- Name: settlement_batches settlement_batches_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_batches - ADD CONSTRAINT settlement_batches_pkey PRIMARY KEY (id); - - --- --- Name: settlement_bills settlement_bills_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_bills - ADD CONSTRAINT settlement_bills_pkey PRIMARY KEY (id); - - --- --- Name: settlement_periods settlement_periods_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_periods - ADD CONSTRAINT settlement_periods_pkey PRIMARY KEY (id); - - --- --- Name: share_ledger share_ledger_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.share_ledger - ADD CONSTRAINT share_ledger_pkey PRIMARY KEY (id); - - --- --- Name: ticket_combinations ticket_combinations_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_combinations - ADD CONSTRAINT ticket_combinations_pkey PRIMARY KEY (id); - - --- --- Name: ticket_items ticket_items_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_items - ADD CONSTRAINT ticket_items_pkey PRIMARY KEY (id); - - --- --- Name: ticket_items ticket_items_ticket_no_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_items - ADD CONSTRAINT ticket_items_ticket_no_unique UNIQUE (ticket_no); - - --- --- Name: ticket_orders ticket_orders_order_no_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_orders - ADD CONSTRAINT ticket_orders_order_no_unique UNIQUE (order_no); - - --- --- Name: ticket_orders ticket_orders_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_orders - ADD CONSTRAINT ticket_orders_pkey PRIMARY KEY (id); - - --- --- Name: ticket_settlement_details ticket_settlement_details_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_settlement_details - ADD CONSTRAINT ticket_settlement_details_pkey PRIMARY KEY (id); - - --- --- Name: transfer_orders transfer_orders_idempotent_key_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.transfer_orders - ADD CONSTRAINT transfer_orders_idempotent_key_unique UNIQUE (idempotent_key); - - --- --- Name: transfer_orders transfer_orders_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.transfer_orders - ADD CONSTRAINT transfer_orders_pkey PRIMARY KEY (id); - - --- --- Name: transfer_orders transfer_orders_transfer_no_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.transfer_orders - ADD CONSTRAINT transfer_orders_transfer_no_unique UNIQUE (transfer_no); - - --- --- Name: admin_api_resource_bindings uk_admin_api_bindings_api_action; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_api_resource_bindings - ADD CONSTRAINT uk_admin_api_bindings_api_action UNIQUE (api_resource_id, menu_action_id); - - --- --- Name: admin_menu_actions uk_admin_menu_actions_menu_action; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menu_actions - ADD CONSTRAINT uk_admin_menu_actions_menu_action UNIQUE (menu_id, action_id); - - --- --- Name: agent_delegation_grants uk_agent_delegation_child_action; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_delegation_grants - ADD CONSTRAINT uk_agent_delegation_child_action UNIQUE (child_agent_id, menu_action_id); - - --- --- Name: agent_nodes uk_agent_nodes_site_code; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_nodes - ADD CONSTRAINT uk_agent_nodes_site_code UNIQUE (admin_site_id, code); - - --- --- Name: draw_result_batches uk_draw_result_batches_draw_version; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_batches - ADD CONSTRAINT uk_draw_result_batches_draw_version UNIQUE (draw_id, result_version); - - --- --- Name: jackpot_contributions uk_jackpot_contributions_ticket_item; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_contributions - ADD CONSTRAINT uk_jackpot_contributions_ticket_item UNIQUE (ticket_item_id); - - --- --- Name: odds_items uk_odds_items_version_play_prize_currency_dimension; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.odds_items - ADD CONSTRAINT uk_odds_items_version_play_prize_currency_dimension UNIQUE (version_id, play_code, prize_scope, currency_code, dimension); - - --- --- Name: play_config_items uk_play_config_items_version_play; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_config_items - ADD CONSTRAINT uk_play_config_items_version_play UNIQUE (version_id, play_code); - - --- --- Name: player_wallets uk_player_wallets_player_type_currency; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_wallets - ADD CONSTRAINT uk_player_wallets_player_type_currency UNIQUE (player_id, wallet_type, currency_code); - - --- --- Name: players uk_players_site_player; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.players - ADD CONSTRAINT uk_players_site_player UNIQUE (site_code, site_player_id); - - --- --- Name: risk_pools uk_risk_pools_draw_number; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_pools - ADD CONSTRAINT uk_risk_pools_draw_number UNIQUE (draw_id, normalized_number); - - --- --- Name: ticket_settlement_details uk_ticket_settlement_batch_ticket; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_settlement_details - ADD CONSTRAINT uk_ticket_settlement_batch_ticket UNIQUE (settlement_batch_id, ticket_item_id); - - --- --- Name: wallet_txns uk_wallet_txns_idempotent_biz; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.wallet_txns - ADD CONSTRAINT uk_wallet_txns_idempotent_biz UNIQUE (idempotent_key, biz_type); - - --- --- Name: ticket_orders uniq_ticket_orders_player_draw_trace; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_orders - ADD CONSTRAINT uniq_ticket_orders_player_draw_trace UNIQUE (player_id, draw_id, client_trace_id); - - --- --- Name: wallet_txns wallet_txns_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.wallet_txns - ADD CONSTRAINT wallet_txns_pkey PRIMARY KEY (id); - - --- --- Name: wallet_txns wallet_txns_txn_no_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.wallet_txns - ADD CONSTRAINT wallet_txns_txn_no_unique UNIQUE (txn_no); - - --- --- Name: cache_expiration_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX cache_expiration_index ON public.cache USING btree (expiration); - - --- --- Name: cache_locks_expiration_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX cache_locks_expiration_index ON public.cache_locks USING btree (expiration); - - --- --- Name: credit_ledger_owner_type_owner_id_created_at_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX credit_ledger_owner_type_owner_id_created_at_index ON public.credit_ledger USING btree (owner_type, owner_id, created_at); - - --- --- Name: idx_admin_api_resources_module_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_admin_api_resources_module_status ON public.admin_api_resources USING btree (module_code, status); - - --- --- Name: idx_admin_menu_actions_menu_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_admin_menu_actions_menu_status ON public.admin_menu_actions USING btree (menu_id, status); - - --- --- Name: idx_admin_menus_parent_sort; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_admin_menus_parent_sort ON public.admin_menus USING btree (parent_id, sort_order); - - --- --- Name: idx_agent_delegation_parent_child; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_agent_delegation_parent_child ON public.agent_delegation_grants USING btree (parent_agent_id, child_agent_id); - - --- --- Name: idx_agent_nodes_path; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_agent_nodes_path ON public.agent_nodes USING btree (path); - - --- --- Name: idx_agent_nodes_site_parent; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_agent_nodes_site_parent ON public.agent_nodes USING btree (admin_site_id, parent_id); - - --- --- Name: idx_audit_logs_module_action; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_audit_logs_module_action ON public.audit_logs USING btree (module_code, action_code); - - --- --- Name: idx_audit_logs_operator_time; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_audit_logs_operator_time ON public.audit_logs USING btree (operator_type, operator_id, created_at); - - --- --- Name: idx_draw_result_items_batch_prize; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_draw_result_items_batch_prize ON public.draw_result_items USING btree (result_batch_id, prize_type, prize_index); - - --- --- Name: idx_draw_result_items_draw_number; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_draw_result_items_draw_number ON public.draw_result_items USING btree (draw_id, number_4d); - - --- --- Name: idx_draw_result_items_draw_prize; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_draw_result_items_draw_prize ON public.draw_result_items USING btree (draw_id, prize_type, prize_index); - - --- --- Name: idx_draws_business_date_draw_time; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_draws_business_date_draw_time ON public.draws USING btree (business_date, draw_time); - - --- --- Name: idx_draws_status_draw_time; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_draws_status_draw_time ON public.draws USING btree (status, draw_time); - - --- --- Name: idx_jackpot_contrib_draw_player; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_jackpot_contrib_draw_player ON public.jackpot_contributions USING btree (draw_id, player_id); - - --- --- Name: idx_jackpot_pool_adjustments_pool_created; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_jackpot_pool_adjustments_pool_created ON public.jackpot_pool_adjustments USING btree (jackpot_pool_id, created_at); - - --- --- Name: idx_lottery_settings_group; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_lottery_settings_group ON public.lottery_settings USING btree (group_name); - - --- --- Name: idx_odds_items_version_play; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_odds_items_version_play ON public.odds_items USING btree (version_id, play_code); - - --- --- Name: idx_players_site_agent; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_players_site_agent ON public.players USING btree (site_code, agent_node_id); - - --- --- Name: idx_players_site_auth_username; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_players_site_auth_username ON public.players USING btree (site_code, auth_source, username); - - --- --- Name: idx_players_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_players_status ON public.players USING btree (status); - - --- --- Name: idx_risk_cap_items_lookup; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_risk_cap_items_lookup ON public.risk_cap_items USING btree (version_id, draw_id, normalized_number); - - --- --- Name: idx_risk_lock_logs_draw_number; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_risk_lock_logs_draw_number ON public.risk_pool_lock_logs USING btree (draw_id, normalized_number); - - --- --- Name: idx_risk_pools_draw_soldout; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_risk_pools_draw_soldout ON public.risk_pools USING btree (draw_id, sold_out_status); - - --- --- Name: idx_settlement_batches_draw_version; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_settlement_batches_draw_version ON public.settlement_batches USING btree (draw_id, settle_version); - - --- --- Name: idx_settlement_batches_result_batch_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_settlement_batches_result_batch_id ON public.settlement_batches USING btree (result_batch_id); - - --- --- Name: idx_ticket_combinations_item; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_combinations_item ON public.ticket_combinations USING btree (ticket_item_id); - - --- --- Name: idx_ticket_combinations_number; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_combinations_number ON public.ticket_combinations USING btree (number_4d); - - --- --- Name: idx_ticket_items_draw_number; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_items_draw_number ON public.ticket_items USING btree (draw_id, normalized_number); - - --- --- Name: idx_ticket_items_draw_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_items_draw_status ON public.ticket_items USING btree (draw_id, status); - - --- --- Name: idx_ticket_items_order_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_items_order_id ON public.ticket_items USING btree (order_id); - - --- --- Name: idx_ticket_items_player_draw; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_items_player_draw ON public.ticket_items USING btree (player_id, draw_id); - - --- --- Name: idx_ticket_items_player_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_items_player_id ON public.ticket_items USING btree (player_id, id); - - --- --- Name: idx_ticket_orders_draw_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_orders_draw_id ON public.ticket_orders USING btree (draw_id); - - --- --- Name: idx_ticket_orders_player_draw; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_orders_player_draw ON public.ticket_orders USING btree (player_id, draw_id); - - --- --- Name: idx_ticket_settlement_details_ticket_item; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ticket_settlement_details_ticket_item ON public.ticket_settlement_details USING btree (ticket_item_id); - - --- --- Name: idx_wallet_txns_biz; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_wallet_txns_biz ON public.wallet_txns USING btree (biz_type, biz_no); - - --- --- Name: idx_wallet_txns_player_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_wallet_txns_player_id ON public.wallet_txns USING btree (player_id, id); - - --- --- Name: idx_wallet_txns_player_time; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_wallet_txns_player_time ON public.wallet_txns USING btree (player_id, created_at); - - --- --- Name: jobs_queue_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX jobs_queue_index ON public.jobs USING btree (queue); - - --- --- Name: personal_access_tokens_expires_at_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX personal_access_tokens_expires_at_index ON public.personal_access_tokens USING btree (expires_at); - - --- --- Name: personal_access_tokens_tokenable_type_tokenable_id_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX personal_access_tokens_tokenable_type_tokenable_id_index ON public.personal_access_tokens USING btree (tokenable_type, tokenable_id); - - --- --- Name: sessions_last_activity_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX sessions_last_activity_index ON public.sessions USING btree (last_activity); - - --- --- Name: sessions_user_id_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX sessions_user_id_index ON public.sessions USING btree (user_id); - - --- --- Name: settlement_bills_settlement_period_id_bill_type_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX settlement_bills_settlement_period_id_bill_type_index ON public.settlement_bills USING btree (settlement_period_id, bill_type); - - --- --- Name: settlement_periods_admin_site_id_status_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX settlement_periods_admin_site_id_status_index ON public.settlement_periods USING btree (admin_site_id, status); - - --- --- Name: share_ledger_settled_at_player_id_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX share_ledger_settled_at_player_id_index ON public.share_ledger USING btree (settled_at, player_id); - - --- --- Name: share_ledger_settlement_period_id_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX share_ledger_settlement_period_id_index ON public.share_ledger USING btree (settlement_period_id); - - --- --- Name: transfer_orders_player_id_created_at_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX transfer_orders_player_id_created_at_index ON public.transfer_orders USING btree (player_id, created_at); - - --- --- Name: admin_api_resource_bindings admin_api_resource_bindings_api_resource_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_api_resource_bindings - ADD CONSTRAINT admin_api_resource_bindings_api_resource_id_foreign FOREIGN KEY (api_resource_id) REFERENCES public.admin_api_resources(id) ON DELETE CASCADE; - - --- --- Name: admin_api_resource_bindings admin_api_resource_bindings_menu_action_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_api_resource_bindings - ADD CONSTRAINT admin_api_resource_bindings_menu_action_id_foreign FOREIGN KEY (menu_action_id) REFERENCES public.admin_menu_actions(id) ON DELETE CASCADE; - - --- --- Name: admin_menu_actions admin_menu_actions_action_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menu_actions - ADD CONSTRAINT admin_menu_actions_action_id_foreign FOREIGN KEY (action_id) REFERENCES public.admin_action_catalog(id) ON DELETE CASCADE; - - --- --- Name: admin_menu_actions admin_menu_actions_menu_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menu_actions - ADD CONSTRAINT admin_menu_actions_menu_id_foreign FOREIGN KEY (menu_id) REFERENCES public.admin_menus(id) ON DELETE CASCADE; - - --- --- Name: admin_menus admin_menus_parent_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_menus - ADD CONSTRAINT admin_menus_parent_id_foreign FOREIGN KEY (parent_id) REFERENCES public.admin_menus(id) ON DELETE SET NULL; - - --- --- Name: admin_role_menu_actions admin_role_menu_actions_menu_action_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_role_menu_actions - ADD CONSTRAINT admin_role_menu_actions_menu_action_id_foreign FOREIGN KEY (menu_action_id) REFERENCES public.admin_menu_actions(id) ON DELETE CASCADE; - - --- --- Name: admin_role_menu_actions admin_role_menu_actions_role_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_role_menu_actions - ADD CONSTRAINT admin_role_menu_actions_role_id_foreign FOREIGN KEY (role_id) REFERENCES public.admin_roles(id) ON DELETE CASCADE; - - --- --- Name: admin_roles admin_roles_delegated_from_role_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_roles - ADD CONSTRAINT admin_roles_delegated_from_role_id_foreign FOREIGN KEY (delegated_from_role_id) REFERENCES public.admin_roles(id) ON DELETE SET NULL; - - --- --- Name: admin_roles admin_roles_owner_agent_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_roles - ADD CONSTRAINT admin_roles_owner_agent_id_foreign FOREIGN KEY (owner_agent_id) REFERENCES public.agent_nodes(id) ON DELETE SET NULL; - - --- --- Name: admin_user_agent_roles admin_user_agent_roles_admin_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_agent_roles - ADD CONSTRAINT admin_user_agent_roles_admin_user_id_foreign FOREIGN KEY (admin_user_id) REFERENCES public.admin_users(id) ON DELETE CASCADE; - - --- --- Name: admin_user_agent_roles admin_user_agent_roles_agent_node_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_agent_roles - ADD CONSTRAINT admin_user_agent_roles_agent_node_id_foreign FOREIGN KEY (agent_node_id) REFERENCES public.agent_nodes(id) ON DELETE CASCADE; - - --- --- Name: admin_user_agent_roles admin_user_agent_roles_role_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_agent_roles - ADD CONSTRAINT admin_user_agent_roles_role_id_foreign FOREIGN KEY (role_id) REFERENCES public.admin_roles(id) ON DELETE CASCADE; - - --- --- Name: admin_user_agents admin_user_agents_admin_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_agents - ADD CONSTRAINT admin_user_agents_admin_user_id_foreign FOREIGN KEY (admin_user_id) REFERENCES public.admin_users(id) ON DELETE CASCADE; - - --- --- Name: admin_user_agents admin_user_agents_agent_node_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_agents - ADD CONSTRAINT admin_user_agents_agent_node_id_foreign FOREIGN KEY (agent_node_id) REFERENCES public.agent_nodes(id) ON DELETE CASCADE; - - --- --- Name: admin_user_menu_actions admin_user_menu_actions_admin_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_menu_actions - ADD CONSTRAINT admin_user_menu_actions_admin_user_id_foreign FOREIGN KEY (admin_user_id) REFERENCES public.admin_users(id) ON DELETE CASCADE; - - --- --- Name: admin_user_menu_actions admin_user_menu_actions_menu_action_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_menu_actions - ADD CONSTRAINT admin_user_menu_actions_menu_action_id_foreign FOREIGN KEY (menu_action_id) REFERENCES public.admin_menu_actions(id) ON DELETE CASCADE; - - --- --- Name: admin_user_menu_actions admin_user_menu_actions_site_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_menu_actions - ADD CONSTRAINT admin_user_menu_actions_site_id_foreign FOREIGN KEY (site_id) REFERENCES public.admin_sites(id) ON DELETE SET NULL; - - --- --- Name: admin_user_site_roles admin_user_site_roles_admin_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_site_roles - ADD CONSTRAINT admin_user_site_roles_admin_user_id_foreign FOREIGN KEY (admin_user_id) REFERENCES public.admin_users(id) ON DELETE CASCADE; - - --- --- Name: admin_user_site_roles admin_user_site_roles_role_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_site_roles - ADD CONSTRAINT admin_user_site_roles_role_id_foreign FOREIGN KEY (role_id) REFERENCES public.admin_roles(id) ON DELETE CASCADE; - - --- --- Name: admin_user_site_roles admin_user_site_roles_site_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.admin_user_site_roles - ADD CONSTRAINT admin_user_site_roles_site_id_foreign FOREIGN KEY (site_id) REFERENCES public.admin_sites(id) ON DELETE CASCADE; - - --- --- Name: agent_delegation_grants agent_delegation_grants_child_agent_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_delegation_grants - ADD CONSTRAINT agent_delegation_grants_child_agent_id_foreign FOREIGN KEY (child_agent_id) REFERENCES public.agent_nodes(id) ON DELETE CASCADE; - - --- --- Name: agent_delegation_grants agent_delegation_grants_granted_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_delegation_grants - ADD CONSTRAINT agent_delegation_grants_granted_by_foreign FOREIGN KEY (granted_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: agent_delegation_grants agent_delegation_grants_menu_action_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_delegation_grants - ADD CONSTRAINT agent_delegation_grants_menu_action_id_foreign FOREIGN KEY (menu_action_id) REFERENCES public.admin_menu_actions(id) ON DELETE CASCADE; - - --- --- Name: agent_delegation_grants agent_delegation_grants_parent_agent_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_delegation_grants - ADD CONSTRAINT agent_delegation_grants_parent_agent_id_foreign FOREIGN KEY (parent_agent_id) REFERENCES public.agent_nodes(id) ON DELETE CASCADE; - - --- --- Name: agent_nodes agent_nodes_admin_site_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_nodes - ADD CONSTRAINT agent_nodes_admin_site_id_foreign FOREIGN KEY (admin_site_id) REFERENCES public.admin_sites(id) ON DELETE CASCADE; - - --- --- Name: agent_nodes agent_nodes_created_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_nodes - ADD CONSTRAINT agent_nodes_created_by_foreign FOREIGN KEY (created_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: agent_nodes agent_nodes_parent_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_nodes - ADD CONSTRAINT agent_nodes_parent_id_foreign FOREIGN KEY (parent_id) REFERENCES public.agent_nodes(id) ON DELETE SET NULL; - - --- --- Name: agent_profiles agent_profiles_agent_node_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_profiles - ADD CONSTRAINT agent_profiles_agent_node_id_foreign FOREIGN KEY (agent_node_id) REFERENCES public.agent_nodes(id) ON DELETE CASCADE; - - --- --- Name: draw_result_batches draw_result_batches_confirmed_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_batches - ADD CONSTRAINT draw_result_batches_confirmed_by_foreign FOREIGN KEY (confirmed_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: draw_result_batches draw_result_batches_created_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_batches - ADD CONSTRAINT draw_result_batches_created_by_foreign FOREIGN KEY (created_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: draw_result_batches draw_result_batches_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_batches - ADD CONSTRAINT draw_result_batches_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE CASCADE; - - --- --- Name: draw_result_items draw_result_items_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_items - ADD CONSTRAINT draw_result_items_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE CASCADE; - - --- --- Name: draw_result_items draw_result_items_result_batch_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.draw_result_items - ADD CONSTRAINT draw_result_items_result_batch_id_foreign FOREIGN KEY (result_batch_id) REFERENCES public.draw_result_batches(id) ON DELETE CASCADE; - - --- --- Name: jackpot_contributions jackpot_contributions_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_contributions - ADD CONSTRAINT jackpot_contributions_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE CASCADE; - - --- --- Name: jackpot_contributions jackpot_contributions_jackpot_pool_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_contributions - ADD CONSTRAINT jackpot_contributions_jackpot_pool_id_foreign FOREIGN KEY (jackpot_pool_id) REFERENCES public.jackpot_pools(id) ON DELETE CASCADE; - - --- --- Name: jackpot_contributions jackpot_contributions_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_contributions - ADD CONSTRAINT jackpot_contributions_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: jackpot_contributions jackpot_contributions_ticket_item_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_contributions - ADD CONSTRAINT jackpot_contributions_ticket_item_id_foreign FOREIGN KEY (ticket_item_id) REFERENCES public.ticket_items(id) ON DELETE SET NULL; - - --- --- Name: jackpot_payout_logs jackpot_payout_logs_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_payout_logs - ADD CONSTRAINT jackpot_payout_logs_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE CASCADE; - - --- --- Name: jackpot_payout_logs jackpot_payout_logs_jackpot_pool_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_payout_logs - ADD CONSTRAINT jackpot_payout_logs_jackpot_pool_id_foreign FOREIGN KEY (jackpot_pool_id) REFERENCES public.jackpot_pools(id) ON DELETE CASCADE; - - --- --- Name: jackpot_pool_adjustments jackpot_pool_adjustments_admin_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_pool_adjustments - ADD CONSTRAINT jackpot_pool_adjustments_admin_user_id_foreign FOREIGN KEY (admin_user_id) REFERENCES public.admin_users(id) ON DELETE CASCADE; - - --- --- Name: jackpot_pool_adjustments jackpot_pool_adjustments_jackpot_pool_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_pool_adjustments - ADD CONSTRAINT jackpot_pool_adjustments_jackpot_pool_id_foreign FOREIGN KEY (jackpot_pool_id) REFERENCES public.jackpot_pools(id) ON DELETE CASCADE; - - --- --- Name: jackpot_pools jackpot_pools_last_trigger_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.jackpot_pools - ADD CONSTRAINT jackpot_pools_last_trigger_draw_id_foreign FOREIGN KEY (last_trigger_draw_id) REFERENCES public.draws(id) ON DELETE SET NULL; - - --- --- Name: odds_items odds_items_version_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.odds_items - ADD CONSTRAINT odds_items_version_id_foreign FOREIGN KEY (version_id) REFERENCES public.odds_versions(id) ON DELETE CASCADE; - - --- --- Name: odds_versions odds_versions_updated_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.odds_versions - ADD CONSTRAINT odds_versions_updated_by_foreign FOREIGN KEY (updated_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: payment_records payment_records_confirmed_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.payment_records - ADD CONSTRAINT payment_records_confirmed_by_foreign FOREIGN KEY (confirmed_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: payment_records payment_records_created_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.payment_records - ADD CONSTRAINT payment_records_created_by_foreign FOREIGN KEY (created_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: payment_records payment_records_settlement_bill_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.payment_records - ADD CONSTRAINT payment_records_settlement_bill_id_foreign FOREIGN KEY (settlement_bill_id) REFERENCES public.settlement_bills(id) ON DELETE CASCADE; - - --- --- Name: play_config_items play_config_items_version_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_config_items - ADD CONSTRAINT play_config_items_version_id_foreign FOREIGN KEY (version_id) REFERENCES public.play_config_versions(id) ON DELETE CASCADE; - - --- --- Name: play_config_versions play_config_versions_updated_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.play_config_versions - ADD CONSTRAINT play_config_versions_updated_by_foreign FOREIGN KEY (updated_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: player_credit_accounts player_credit_accounts_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_credit_accounts - ADD CONSTRAINT player_credit_accounts_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: player_rebate_profiles player_rebate_profiles_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_rebate_profiles - ADD CONSTRAINT player_rebate_profiles_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: player_wallets player_wallets_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.player_wallets - ADD CONSTRAINT player_wallets_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: players players_agent_node_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.players - ADD CONSTRAINT players_agent_node_id_foreign FOREIGN KEY (agent_node_id) REFERENCES public.agent_nodes(id) ON DELETE SET NULL; - - --- --- Name: rebate_allocations rebate_allocations_rebate_record_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_allocations - ADD CONSTRAINT rebate_allocations_rebate_record_id_foreign FOREIGN KEY (rebate_record_id) REFERENCES public.rebate_records(id) ON DELETE CASCADE; - - --- --- Name: rebate_allocations rebate_allocations_settlement_bill_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_allocations - ADD CONSTRAINT rebate_allocations_settlement_bill_id_foreign FOREIGN KEY (settlement_bill_id) REFERENCES public.settlement_bills(id) ON DELETE SET NULL; - - --- --- Name: rebate_records rebate_records_owner_agent_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_records - ADD CONSTRAINT rebate_records_owner_agent_id_foreign FOREIGN KEY (owner_agent_id) REFERENCES public.agent_nodes(id) ON DELETE SET NULL; - - --- --- Name: rebate_records rebate_records_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_records - ADD CONSTRAINT rebate_records_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: rebate_records rebate_records_reversal_of_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_records - ADD CONSTRAINT rebate_records_reversal_of_id_foreign FOREIGN KEY (reversal_of_id) REFERENCES public.rebate_records(id) ON DELETE SET NULL; - - --- --- Name: rebate_records rebate_records_settlement_period_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_records - ADD CONSTRAINT rebate_records_settlement_period_id_foreign FOREIGN KEY (settlement_period_id) REFERENCES public.settlement_periods(id) ON DELETE SET NULL; - - --- --- Name: rebate_records rebate_records_ticket_item_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rebate_records - ADD CONSTRAINT rebate_records_ticket_item_id_foreign FOREIGN KEY (ticket_item_id) REFERENCES public.ticket_items(id) ON DELETE SET NULL; - - --- --- Name: reconcile_items reconcile_items_reconcile_job_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reconcile_items - ADD CONSTRAINT reconcile_items_reconcile_job_id_foreign FOREIGN KEY (reconcile_job_id) REFERENCES public.reconcile_jobs(id) ON DELETE CASCADE; - - --- --- Name: reconcile_jobs reconcile_jobs_admin_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reconcile_jobs - ADD CONSTRAINT reconcile_jobs_admin_user_id_foreign FOREIGN KEY (admin_user_id) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: report_jobs report_jobs_admin_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.report_jobs - ADD CONSTRAINT report_jobs_admin_user_id_foreign FOREIGN KEY (admin_user_id) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: risk_cap_items risk_cap_items_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_cap_items - ADD CONSTRAINT risk_cap_items_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE SET NULL; - - --- --- Name: risk_cap_items risk_cap_items_version_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_cap_items - ADD CONSTRAINT risk_cap_items_version_id_foreign FOREIGN KEY (version_id) REFERENCES public.risk_cap_versions(id) ON DELETE CASCADE; - - --- --- Name: risk_cap_versions risk_cap_versions_updated_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_cap_versions - ADD CONSTRAINT risk_cap_versions_updated_by_foreign FOREIGN KEY (updated_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: risk_pool_lock_logs risk_pool_lock_logs_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_pool_lock_logs - ADD CONSTRAINT risk_pool_lock_logs_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE CASCADE; - - --- --- Name: risk_pool_lock_logs risk_pool_lock_logs_ticket_item_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_pool_lock_logs - ADD CONSTRAINT risk_pool_lock_logs_ticket_item_id_foreign FOREIGN KEY (ticket_item_id) REFERENCES public.ticket_items(id) ON DELETE SET NULL; - - --- --- Name: risk_pools risk_pools_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.risk_pools - ADD CONSTRAINT risk_pools_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE CASCADE; - - --- --- Name: settlement_adjustments settlement_adjustments_created_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_adjustments - ADD CONSTRAINT settlement_adjustments_created_by_foreign FOREIGN KEY (created_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: settlement_adjustments settlement_adjustments_original_bill_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_adjustments - ADD CONSTRAINT settlement_adjustments_original_bill_id_foreign FOREIGN KEY (original_bill_id) REFERENCES public.settlement_bills(id) ON DELETE SET NULL; - - --- --- Name: settlement_adjustments settlement_adjustments_settlement_period_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_adjustments - ADD CONSTRAINT settlement_adjustments_settlement_period_id_foreign FOREIGN KEY (settlement_period_id) REFERENCES public.settlement_periods(id) ON DELETE SET NULL; - - --- --- Name: settlement_batches settlement_batches_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_batches - ADD CONSTRAINT settlement_batches_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE CASCADE; - - --- --- Name: settlement_batches settlement_batches_result_batch_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_batches - ADD CONSTRAINT settlement_batches_result_batch_id_foreign FOREIGN KEY (result_batch_id) REFERENCES public.draw_result_batches(id) ON DELETE CASCADE; - - --- --- Name: settlement_batches settlement_batches_reviewed_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_batches - ADD CONSTRAINT settlement_batches_reviewed_by_foreign FOREIGN KEY (reviewed_by) REFERENCES public.admin_users(id) ON DELETE SET NULL; - - --- --- Name: settlement_bills settlement_bills_reversed_bill_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_bills - ADD CONSTRAINT settlement_bills_reversed_bill_id_foreign FOREIGN KEY (reversed_bill_id) REFERENCES public.settlement_bills(id) ON DELETE SET NULL; - - --- --- Name: settlement_bills settlement_bills_settlement_period_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_bills - ADD CONSTRAINT settlement_bills_settlement_period_id_foreign FOREIGN KEY (settlement_period_id) REFERENCES public.settlement_periods(id) ON DELETE CASCADE; - - --- --- Name: settlement_periods settlement_periods_admin_site_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settlement_periods - ADD CONSTRAINT settlement_periods_admin_site_id_foreign FOREIGN KEY (admin_site_id) REFERENCES public.admin_sites(id) ON DELETE CASCADE; - - --- --- Name: share_ledger share_ledger_agent_node_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.share_ledger - ADD CONSTRAINT share_ledger_agent_node_id_foreign FOREIGN KEY (agent_node_id) REFERENCES public.agent_nodes(id) ON DELETE SET NULL; - - --- --- Name: share_ledger share_ledger_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.share_ledger - ADD CONSTRAINT share_ledger_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: share_ledger share_ledger_reversal_of_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.share_ledger - ADD CONSTRAINT share_ledger_reversal_of_id_foreign FOREIGN KEY (reversal_of_id) REFERENCES public.share_ledger(id) ON DELETE SET NULL; - - --- --- Name: share_ledger share_ledger_settlement_period_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.share_ledger - ADD CONSTRAINT share_ledger_settlement_period_id_foreign FOREIGN KEY (settlement_period_id) REFERENCES public.settlement_periods(id) ON DELETE SET NULL; - - --- --- Name: share_ledger share_ledger_ticket_item_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.share_ledger - ADD CONSTRAINT share_ledger_ticket_item_id_foreign FOREIGN KEY (ticket_item_id) REFERENCES public.ticket_items(id) ON DELETE CASCADE; - - --- --- Name: ticket_combinations ticket_combinations_ticket_item_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_combinations - ADD CONSTRAINT ticket_combinations_ticket_item_id_foreign FOREIGN KEY (ticket_item_id) REFERENCES public.ticket_items(id) ON DELETE CASCADE; - - --- --- Name: ticket_items ticket_items_agent_node_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_items - ADD CONSTRAINT ticket_items_agent_node_id_foreign FOREIGN KEY (agent_node_id) REFERENCES public.agent_nodes(id) ON DELETE SET NULL; - - --- --- Name: ticket_items ticket_items_agent_settlement_reversal_of_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_items - ADD CONSTRAINT ticket_items_agent_settlement_reversal_of_id_foreign FOREIGN KEY (agent_settlement_reversal_of_id) REFERENCES public.ticket_items(id) ON DELETE SET NULL; - - --- --- Name: ticket_items ticket_items_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_items - ADD CONSTRAINT ticket_items_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE CASCADE; - - --- --- Name: ticket_items ticket_items_order_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_items - ADD CONSTRAINT ticket_items_order_id_foreign FOREIGN KEY (order_id) REFERENCES public.ticket_orders(id) ON DELETE CASCADE; - - --- --- Name: ticket_items ticket_items_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_items - ADD CONSTRAINT ticket_items_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: ticket_orders ticket_orders_draw_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_orders - ADD CONSTRAINT ticket_orders_draw_id_foreign FOREIGN KEY (draw_id) REFERENCES public.draws(id) ON DELETE CASCADE; - - --- --- Name: ticket_orders ticket_orders_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_orders - ADD CONSTRAINT ticket_orders_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: ticket_settlement_details ticket_settlement_details_settlement_batch_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_settlement_details - ADD CONSTRAINT ticket_settlement_details_settlement_batch_id_foreign FOREIGN KEY (settlement_batch_id) REFERENCES public.settlement_batches(id) ON DELETE CASCADE; - - --- --- Name: ticket_settlement_details ticket_settlement_details_ticket_item_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.ticket_settlement_details - ADD CONSTRAINT ticket_settlement_details_ticket_item_id_foreign FOREIGN KEY (ticket_item_id) REFERENCES public.ticket_items(id) ON DELETE CASCADE; - - --- --- Name: transfer_orders transfer_orders_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.transfer_orders - ADD CONSTRAINT transfer_orders_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: wallet_txns wallet_txns_player_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.wallet_txns - ADD CONSTRAINT wallet_txns_player_id_foreign FOREIGN KEY (player_id) REFERENCES public.players(id) ON DELETE CASCADE; - - --- --- Name: wallet_txns wallet_txns_wallet_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.wallet_txns - ADD CONSTRAINT wallet_txns_wallet_id_foreign FOREIGN KEY (wallet_id) REFERENCES public.player_wallets(id) ON DELETE CASCADE; - - --- --- PostgreSQL database dump complete --- - -\unrestrict rPXEgF1VaYgsz0ptn4X1KcYROWRPYlYb6daN4zAOY961hMNjxCs5gLhsUZO9N0E - --- --- PostgreSQL database dump --- - -\restrict RiLJxG2okqJB0Ghnyl7nmKPp6kFTgq0lQmAb7r3CeeShjxRjgjZVfbJ1VM9V1oB - --- Dumped from database version 18.3(ServBay) --- Dumped by pg_dump version 18.3(ServBay) - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET transaction_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Data for Name: migrations; Type: TABLE DATA; Schema: public; Owner: - --- - -COPY public.migrations (id, migration, batch) FROM stdin; -1 0001_01_01_000000_create_users_table 1 -2 0001_01_01_000001_create_cache_table 1 -3 0001_01_01_000002_create_jobs_table 1 -4 2026_05_08_100000_create_currencies_table 1 -5 2026_05_08_100001_create_players_table 1 -6 2026_05_08_100002_create_admin_users_table 1 -7 2026_05_08_100003_create_admin_roles_and_permissions_tables 1 -8 2026_05_08_100004_create_player_wallets_table 1 -9 2026_05_08_100005_create_wallet_txns_table 1 -10 2026_05_08_100006_create_transfer_orders_table 1 -11 2026_05_08_100007_create_draws_table 1 -12 2026_05_08_100008_create_draw_result_batches_table 1 -13 2026_05_08_100009_create_draw_result_items_table 1 -14 2026_05_08_120000_drop_laravel_default_users_and_password_reset_tables 1 -15 2026_05_08_130000_create_play_types_table 1 -16 2026_05_08_130001_create_play_config_versions_and_items_tables 1 -17 2026_05_08_130002_create_odds_versions_and_items_tables 1 -18 2026_05_08_130003_create_risk_cap_versions_and_items_tables 1 -19 2026_05_08_130004_create_ticket_orders_table 1 -20 2026_05_08_130005_create_ticket_items_table 1 -21 2026_05_08_130006_create_ticket_combinations_table 1 -22 2026_05_08_130007_create_risk_pools_and_lock_logs_tables 1 -23 2026_05_08_130008_create_settlement_and_jackpot_tables 1 -24 2026_05_08_130009_create_report_audit_reconcile_tables 1 -25 2026_05_08_140000_create_lottery_settings_table 1 -26 2026_05_09_023835_create_personal_access_tokens_table 1 -27 2026_05_09_119999_rename_duplicate_migration_filenames_in_table 1 -28 2026_05_09_120001_add_username_and_nullable_email_to_admin_users 1 -29 2026_05_09_120002_migrate_draw_status_to_domain_dict 1 -30 2026_05_11_120000_add_admin_user_id_to_reconcile_jobs_table 1 -31 2026_05_11_173000_create_admin_user_permissions_table 1 -32 2026_05_13_100000_rebuild_admin_authorization_system 1 -33 2026_05_16_000100_add_snapshot_columns_to_play_config_items_table 1 -34 2026_05_18_000001_add_combo_trigger_to_jackpot_pools_table 1 -35 2026_05_18_090000_add_config_version_snapshots_to_ticket_orders 1 -36 2026_05_18_120000_sync_complete_admin_api_resources 1 -37 2026_05_19_112752_seed_default_jackpot_pools 1 -38 2026_05_19_120000_create_admin_role_legacy_permissions_table 1 -39 2026_05_19_121000_sync_admin_role_manage_permission 1 -40 2026_05_19_122000_sync_player_permission_resource_bindings 1 -41 2026_05_20_000001_add_admin_ticket_items_api_resource 1 -42 2026_05_21_000002_add_admin_currency_api_resources 1 -43 2026_05_21_093141_add_dimension_to_odds_items_table 1 -44 2026_05_21_150000_add_admin_currency_destroy_api_resource 1 -45 2026_05_21_160000_add_currency_manage_legacy_permission 1 -46 2026_05_21_170000_move_currency_menu_to_top_level_route 1 -47 2026_05_22_100000_add_admin_report_module 1 -48 2026_05_22_110000_fix_admin_report_authorization 1 -49 2026_05_22_120000_drop_redundant_admin_and_system_tables 1 -50 2026_05_22_130000_consolidate_admin_rbac_slugs 1 -51 2026_05_22_140000_add_frontend_play_rules_html_i18n_settings 1 -52 2026_05_25_120001_consolidate_play_display_name_columns 1 -53 2026_05_25_120002_expand_audit_logs_target_type 1 -54 2026_05_25_120003_refine_admin_permission_granularity 1 -55 2026_05_25_130000_remove_stale_admin_menu_actions 1 -56 2026_05_25_140000_add_admin_dashboard_analytics_resource 1 -57 2026_05_25_180000_add_settlement_batch_review_columns 1 -58 2026_05_26_100000_expand_admin_permission_granularity 1 -59 2026_05_27_100000_add_jackpot_manual_burst_permission 1 -60 2026_05_28_100000_resync_admin_api_resources_after_dashboard_view 2 -61 2026_05_29_100000_add_unique_ticket_item_id_to_jackpot_contributions 3 -62 2026_05_30_100000_create_jackpot_pool_adjustments_table 4 -63 2026_05_30_100001_add_jackpot_pool_adjustment_api_resources 5 -64 2026_05_26_120000_add_unique_client_trace_to_ticket_orders 6 -65 2026_05_27_140000_add_integration_fields_to_admin_sites 6 -66 2026_05_27_140001_seed_integration_menu_actions 7 -67 2026_05_31_100000_add_query_performance_indexes 8 -68 2026_06_01_100000_add_admin_settings_batch_update_api_resource 8 -69 2026_06_02_100000_create_agent_hierarchy_tables 8 -70 2026_06_02_100001_seed_agent_node_permissions 8 -71 2026_06_02_110000_agent_scoped_roles_and_player_agent 8 -72 2026_06_02_110001_seed_agent_role_permissions 8 -73 2026_06_02_120000_create_agent_delegation_grants 8 -74 2026_06_02_130000_backfill_players_agent_node_id 9 -75 2026_06_03_140000_ensure_agent_admin_user_destroy_api_resource 10 -76 2026_06_03_120000_split_agent_permission_granularity 11 -77 2026_06_03_150000_align_root_agent_codes 12 -78 2026_06_03_160000_agent_credit_and_settlement_tables 12 -79 2026_06_03_170000_seed_agent_settlement_api_resources 13 -80 2026_06_03_180000_add_agent_profile_capability_flags 14 -81 2026_06_03_190000_fix_agent_primary_admin_user_status 15 -82 2026_06_04_100000_agent_game_settlement_ledger 16 -83 2026_06_04_120000_resync_agent_owner_role_permissions 16 -84 2026_06_04_130000_seed_platform_agent_role_and_resync_bindings 16 -85 2026_06_04_140000_bind_agents_to_platform_agent_role 17 -86 2026_06_04_120000_add_player_auth_and_funding_mode 18 -87 2026_06_04_120000_agent_settlement_payment_proof 19 -88 2026_06_04_140000_agent_settlement_reports_and_tags 20 -89 2026_06_04_150000_ensure_platform_fixed_system_roles 20 -90 2026_06_05_120000_seed_credit_ledger_admin_api_resource 21 -\. - - --- --- Name: migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - --- - -SELECT pg_catalog.setval('public.migrations_id_seq', 90, true); - - --- --- PostgreSQL database dump complete --- - -\unrestrict RiLJxG2okqJB0Ghnyl7nmKPp6kFTgq0lQmAb7r3CeeShjxRjgjZVfbJ1VM9V1oB - diff --git a/lottery b/lottery new file mode 100644 index 0000000000000000000000000000000000000000..c96ecc99e3a1cf1f5b76301d2f3054bcc1bcf256 GIT binary patch literal 786432 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 literal 0 HcmV?d00001 diff --git a/tests/Feature/DrawPipelineTest.php b/tests/Feature/DrawPipelineTest.php index 15263a3..ae1fb1f 100644 --- a/tests/Feature/DrawPipelineTest.php +++ b/tests/Feature/DrawPipelineTest.php @@ -17,6 +17,8 @@ use App\Models\DrawResultBatch; use App\Models\SettlementBatch; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; use App\Services\LotterySettings; use App\Events\DrawCountdownBroadcast; use App\Lottery\DrawResultBatchStatus; @@ -38,6 +40,8 @@ beforeEach(function (): void { 'lottery.draw.require_manual_review' => false, 'lottery.draw.cooldown_minutes' => 15, ]); + + Cache::flush(); }); test('draw planner fills buffer rows with ordered draw_no', function (): void { @@ -1128,6 +1132,7 @@ test('lottery draw-tick command runs successfully', function (): void { test('lottery hall-countdown dispatches draw.countdown when using reverb connection', function (): void { Event::fake([DrawCountdownBroadcast::class]); + Cache::forget('lottery:hall:countdown:last-fingerprint'); config([ 'broadcasting.default' => 'reverb', 'broadcasting.connections.reverb.driver' => 'reverb', @@ -1156,6 +1161,56 @@ test('lottery hall-countdown dispatches draw.countdown when using reverb connect ); }); +test('lottery hall-countdown skips unchanged non-sync pulse but broadcasts on state change', function (): void { + Cache::forget('lottery:hall:countdown:last-fingerprint'); + Event::fake([DrawCountdownBroadcast::class]); + config([ + 'broadcasting.default' => 'reverb', + 'broadcasting.connections.reverb.driver' => 'reverb', + 'lottery.realtime_hall_countdown_sync_interval_seconds' => 5, + ]); + + Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:00', 'UTC')); + + $draw = Draw::query()->create([ + 'draw_no' => '20260509-002', + 'business_date' => '2026-05-09', + 'sequence_no' => 2, + 'status' => DrawStatus::Open->value, + 'start_time' => now()->subMinutes(5), + 'close_time' => now()->addMinutes(1), + 'draw_time' => now()->addMinutes(2), + 'cooling_end_time' => null, + 'result_source' => null, + 'current_result_version' => 0, + 'settle_version' => 0, + 'is_reopened' => false, + ]); + + $this->artisan('lottery:hall-countdown')->assertSuccessful(); + Event::assertDispatchedTimes(DrawCountdownBroadcast::class, 1); + + Event::fake([DrawCountdownBroadcast::class]); + Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:01', 'UTC')); + $this->artisan('lottery:hall-countdown')->assertSuccessful(); + Event::assertNotDispatched(DrawCountdownBroadcast::class); + + Event::fake([DrawCountdownBroadcast::class]); + $draw->forceFill([ + 'status' => DrawStatus::Closing->value, + 'close_time' => Carbon::parse('2026-05-09 12:00:00', 'UTC'), + ])->save(); + + Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:02', 'UTC')); + $this->artisan('lottery:hall-countdown')->assertSuccessful(); + Event::assertDispatched( + DrawCountdownBroadcast::class, + fn (DrawCountdownBroadcast $event): bool => ($event->data['status'] ?? null) === DrawStatus::Closing->value, + ); + + Carbon::setTestNow(); +}); + test('hall snapshot skips stale pending draw and picks next upcoming row', function (): void { Carbon::setTestNow(Carbon::parse('2026-05-25 18:00:00', 'UTC')); @@ -1243,6 +1298,80 @@ test('hall snapshot switches to next bettable draw when cooldown ended', functio Carbon::setTestNow(); }); +test('hall snapshot reuses cached heavy fragments between second-level countdown builds', function (): void { + Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:00', 'UTC')); + + $draw = Draw::query()->create([ + 'draw_no' => '20260509-001', + 'business_date' => '2026-05-09', + 'sequence_no' => 1, + 'status' => DrawStatus::Cooldown->value, + 'start_time' => now()->subMinutes(5), + 'close_time' => now()->subMinute(), + 'draw_time' => now()->subSeconds(30), + 'cooling_end_time' => now()->addMinutes(5), + 'result_source' => 'rng', + 'current_result_version' => 1, + 'settle_version' => 0, + 'is_reopened' => false, + ]); + + RiskPool::query()->create([ + 'draw_id' => $draw->id, + 'normalized_number' => '1234', + 'total_cap_amount' => 100_000, + 'locked_amount' => 90_000, + 'sold_out_status' => 0, + ]); + + $batch = DrawResultBatch::query()->create([ + 'draw_id' => $draw->id, + 'result_version' => 1, + 'source_type' => 'rng', + 'rng_seed_hash' => hash('sha256', 'fixture'), + 'raw_seed_encrypted' => null, + 'status' => DrawResultBatchStatus::Published->value, + 'created_by' => null, + 'confirmed_by' => null, + 'confirmed_at' => now(), + ]); + + DrawResultItem::query()->create([ + 'draw_id' => $draw->id, + 'result_batch_id' => $batch->id, + 'prize_type' => 'first', + 'prize_index' => 0, + 'number_4d' => '1234', + 'suffix_3d' => '234', + 'suffix_2d' => '34', + 'head_digit' => 1, + 'tail_digit' => 4, + ]); + + DB::enableQueryLog(); + app(DrawHallSnapshotBuilder::class)->build(now()->utc()); + $firstQueries = DB::getQueryLog(); + + DB::flushQueryLog(); + Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:01', 'UTC')); + $payload = app(DrawHallSnapshotBuilder::class)->build(now()->utc()); + $secondQueries = DB::getQueryLog(); + DB::disableQueryLog(); + + $secondSql = collect($secondQueries)->pluck('query')->implode("\n"); + + expect($payload['draw_no'])->toBe('20260509-001') + ->and($payload['result_items'][0]['number_4d'] ?? null)->toBe('1234') + ->and($payload['risk_pool_alerts'][0]['normalized_number'] ?? null)->toBe('1234') + ->and(count($secondQueries))->toBeLessThan(count($firstQueries)) + ->and($secondSql)->not->toContain('jackpot_pools') + ->and($secondSql)->not->toContain('risk_pools') + ->and($secondSql)->not->toContain('draw_result_batches') + ->and($secondSql)->not->toContain('draw_result_items'); + + Carbon::setTestNow(); +}); + test('draw tick dispatches draw.status_change when hall draw_no or status changes', function (): void { Event::fake([DrawStatusChangeBroadcast::class]); config([