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 0000000..c96ecc9 Binary files /dev/null and b/lottery differ 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([