['label' => '游戏键值配置', 'group' => 'configs'], 'dice_ante_config' => ['label' => '底注配置', 'group' => 'configs'], 'dice_lottery_pool_config' => ['label' => '彩金池配置', 'group' => 'configs'], 'dice_reward_config' => ['label' => '奖励索引配置', 'group' => 'configs'], 'dice_reward' => ['label' => '中奖概率(奖励对照)', 'group' => 'configs'], 'dice_game' => ['label' => '游戏管理', 'group' => 'configs'], 'dice_player' => ['label' => '玩家', 'group' => 'players'], 'dice_play_record' => ['label' => '抽奖记录', 'group' => 'records'], 'dice_play_record_test' => ['label' => '测试抽奖记录', 'group' => 'records'], 'dice_player_wallet_record' => ['label' => '钱包流水', 'group' => 'records'], 'dice_player_ticket_record' => ['label' => '票券记录', 'group' => 'records'], 'dice_reward_config_record' => ['label' => '权重测试记录', 'group' => 'records'], ]; /** * 默认模板 dept_id 统一为 0,并为固定 id 的配置表建立 (dept_id, id) 唯一约束 */ public function ensureConfigCompositeKeys(): void { foreach (array_merge(self::CONFIG_TABLES, ['dice_reward']) as $table) { if ($this->tableHasColumn($table, 'dept_id')) { Db::table($table)->whereNull('dept_id')->update(['dept_id' => AdminScopeHelper::DEFAULT_TEMPLATE_DEPT]); } } foreach (self::COMPOSITE_KEY_TABLES as $table) { if (!$this->tableHasColumn($table, 'dept_id')) { continue; } if (!$this->tableHasColumn($table, 'row_id')) { if ($table === 'dice_reward_config') { Db::execute( 'ALTER TABLE `dice_reward_config`' . ' MODIFY `id` int(11) NOT NULL COMMENT \'ID\',' . ' ADD COLUMN `row_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,' . ' DROP PRIMARY KEY,' . ' ADD PRIMARY KEY (`row_id`)' ); } else { Db::execute( 'ALTER TABLE `dice_config`' . ' ADD COLUMN `row_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,' . ' DROP PRIMARY KEY,' . ' ADD PRIMARY KEY (`row_id`)' ); } } $indexes = Db::query("SHOW INDEX FROM `{$table}` WHERE Key_name = 'uk_dept_config'"); if (empty($indexes)) { Db::execute("ALTER TABLE `{$table}` ADD UNIQUE KEY `uk_dept_config` (`dept_id`, `id`)"); } } $this->ensureDeptScopedUniqueIndexes(); } /** * 将全局唯一键改为按渠道 (dept_id, 业务键) 唯一,便于复制默认模板 */ private function ensureDeptScopedUniqueIndexes(): void { if ($this->tableHasColumn('dice_lottery_pool_config', 'dept_id')) { $old = Db::query("SHOW INDEX FROM `dice_lottery_pool_config` WHERE Key_name = 'dice_lottery_poll_config_unique'"); if (!empty($old)) { Db::execute('ALTER TABLE `dice_lottery_pool_config` DROP INDEX `dice_lottery_poll_config_unique`'); } $uk = Db::query("SHOW INDEX FROM `dice_lottery_pool_config` WHERE Key_name = 'uk_dept_name'"); if (empty($uk)) { Db::execute('ALTER TABLE `dice_lottery_pool_config` ADD UNIQUE KEY `uk_dept_name` (`dept_id`, `name`)'); } } if ($this->tableHasColumn('dice_game', 'dept_id')) { foreach (['uk_dice_game_code', 'uk_dice_game_key'] as $idx) { $exists = Db::query("SHOW INDEX FROM `dice_game` WHERE Key_name = '{$idx}'"); if (!empty($exists)) { Db::execute("ALTER TABLE `dice_game` DROP INDEX `{$idx}`"); } } $ukCode = Db::query("SHOW INDEX FROM `dice_game` WHERE Key_name = 'uk_dept_game_code'"); if (empty($ukCode)) { Db::execute('ALTER TABLE `dice_game` ADD UNIQUE KEY `uk_dept_game_code` (`dept_id`, `game_code`)'); } $ukKey = Db::query("SHOW INDEX FROM `dice_game` WHERE Key_name = 'uk_dept_game_key'"); if (empty($ukKey)) { Db::execute('ALTER TABLE `dice_game` ADD UNIQUE KEY `uk_dept_game_key` (`dept_id`, `game_key`)'); } } if ($this->tableHasColumn('dice_reward', 'dept_id')) { $old = Db::query("SHOW INDEX FROM `dice_reward` WHERE Key_name = 'uk_direction_grid_number'"); if (!empty($old)) { Db::execute('ALTER TABLE `dice_reward` DROP INDEX `uk_direction_grid_number`'); } $uk = Db::query("SHOW INDEX FROM `dice_reward` WHERE Key_name = 'uk_dept_direction_grid'"); if (empty($uk)) { Db::execute('ALTER TABLE `dice_reward` ADD UNIQUE KEY `uk_dept_direction_grid` (`dept_id`, `direction`, `grid_number`)'); } } } /** * 将当前无 dept_id 的配置标记为默认模板(仅执行一次迁移) */ public function markLegacyConfigAsDefault(): int { $this->ensureConfigCompositeKeys(); $total = 0; foreach (self::CONFIG_TABLES as $table) { if (!$this->tableHasColumn($table, 'dept_id')) { continue; } $total += $this->countByDept($table, AdminScopeHelper::DEFAULT_TEMPLATE_DEPT); } return $total; } /** * 为单个渠道从默认模板复制配置(已存在则跳过) */ public function copyDefaultConfigToDept(int $deptId): array { if ($deptId <= 0) { throw new ApiException('Invalid channel id'); } $result = ['dept_id' => $deptId, 'copied' => [], 'skipped' => [], 'merged' => []]; foreach (self::CONFIG_TABLES as $table) { if (!$this->tableHasColumn($table, 'dept_id')) { continue; } if (in_array($table, self::TABLES_KEEP_ID, true)) { $merged = $this->syncCompositeIdTableFromDefault($table, $deptId); if ($merged > 0) { $result['merged'][$table] = $merged; } elseif ($this->countByDept($table, $deptId) > 0) { $result['skipped'][] = $table; } continue; } if ($this->countByDept($table, $deptId) > 0) { $result['skipped'][] = $table; continue; } $rows = $this->defaultTemplateRows($table); if (empty($rows)) { continue; } foreach ($rows as $row) { $row = (array) $row; unset($row['id'], $row['row_id'], $row['create_time'], $row['update_time'], $row['delete_time']); $row['dept_id'] = $deptId; Db::table($table)->insert($row); } $result['copied'][] = $table; } $this->ensureRewardReferenceForDept($deptId); DiceRewardConfig::refreshCache($deptId); return $result; } /** * 按业务 id 从默认模板补齐配置(dice_config / dice_reward_config) */ private function syncCompositeIdTableFromDefault(string $table, int $deptId): int { $templateRows = $this->defaultTemplateRows($table); if (empty($templateRows)) { return 0; } $inserted = 0; foreach ($templateRows as $row) { $row = (array) $row; if (!isset($row['id'])) { continue; } $businessId = $row['id']; $exists = Db::table($table)->where('dept_id', $deptId)->where('id', $businessId)->count(); if ($exists > 0) { continue; } unset($row['row_id'], $row['create_time'], $row['update_time'], $row['delete_time']); $row['dept_id'] = $deptId; Db::table($table)->insert($row); $inserted++; } return $inserted; } /** * 渠道已有奖励索引时,自动生成 dice_reward 对照表 */ public function ensureRewardReferenceForDept(int $deptId): void { if ($deptId <= 0 || !$this->tableHasColumn('dice_reward', 'dept_id')) { return; } if ($this->countByDept('dice_reward_config', $deptId) <= 0) { return; } if ($this->countByDept('dice_reward', $deptId) > 0) { return; } $logic = new DiceRewardLogic(); $logic->createRewardReferenceFromConfig($deptId); } /** * 复制默认 dice_reward 到渠道 */ public function copyDefaultRewardsToDept(int $deptId): void { $this->ensureRewardReferenceForDept($deptId); if (!$this->tableHasColumn('dice_reward', 'dept_id')) { return; } if ($this->countByDept('dice_reward', $deptId) > 0) { return; } if ($this->countByDept('dice_reward_config', $deptId) > 0) { return; } $rows = $this->defaultTemplateRows('dice_reward'); foreach ($rows as $row) { $row = (array) $row; unset($row['id'], $row['row_id']); unset($row['create_time'], $row['update_time'], $row['delete_time']); $row['dept_id'] = $deptId; Db::table('dice_reward')->insert($row); } } /** * 为所有已有渠道补齐缺失配置 */ public function syncAllChannelsFromDefault(): array { $deptIds = SystemDept::column('id'); $summary = []; foreach ($deptIds as $deptId) { $deptId = (int) $deptId; if ($deptId <= 0) { continue; } $summary[$deptId] = $this->copyDefaultConfigToDept($deptId); } return $summary; } /** * 修复已删除渠道 ID、无管理员关联的遗留数据,归并到首个顶级渠道 */ public function repairOrphanDeptReferences(): array { $validDeptIds = array_map('intval', SystemDept::column('id') ?: []); if (empty($validDeptIds)) { return []; } $rootDeptId = min($validDeptIds); $stats = []; $inList = implode(',', $validDeptIds); $stats['sa_system_user'] = Db::execute( "UPDATE sa_system_user SET dept_id = {$rootDeptId} WHERE dept_id IS NOT NULL AND dept_id > 0 AND dept_id NOT IN ({$inList})" ); $bizTables = [ 'dice_player', 'dice_play_record', 'dice_play_record_test', 'dice_player_wallet_record', 'dice_player_ticket_record', 'dice_reward_config_record', ]; foreach ($bizTables as $table) { if (!$this->tableHasColumn($table, 'dept_id')) { continue; } $stats[$table . '_invalid_dept'] = Db::execute( "UPDATE `{$table}` SET dept_id = {$rootDeptId} WHERE dept_id IS NOT NULL AND dept_id > 0 AND dept_id NOT IN ({$inList})" ); } return $stats; } /** * 根据管理员/玩家回填 dept_id */ public function backfillDataDeptId(): array { $stats = $this->repairOrphanDeptReferences(); if ($this->tableHasColumn('dice_player', 'dept_id') && $this->tableHasColumn('dice_player', 'admin_id')) { $stats['dice_player'] = Db::execute( 'UPDATE dice_player p INNER JOIN sa_system_user u ON p.admin_id = u.id SET p.dept_id = u.dept_id WHERE (p.dept_id IS NULL OR p.dept_id = 0) AND u.dept_id IS NOT NULL AND u.dept_id > 0' ); } $validDeptIds = SystemDept::column('id') ?: []; if (!empty($validDeptIds) && $this->tableHasColumn('dice_player', 'dept_id')) { $rootDeptId = (int) min($validDeptIds); $stats['dice_player_legacy'] = Db::table('dice_player') ->where(function ($q) { $q->whereNull('dept_id')->whereOr('dept_id', 0); }) ->update(['dept_id' => $rootDeptId]); } $stats = array_merge($stats, $this->backfillRecordDeptIdByPlayer('dice_play_record')); $stats = array_merge($stats, $this->backfillRecordDeptIdByAdmin('dice_play_record')); $stats = array_merge($stats, $this->backfillRecordDeptIdByPlayer('dice_player_wallet_record')); $stats = array_merge($stats, $this->backfillRecordDeptIdByPlayer('dice_player_ticket_record')); $stats = array_merge($stats, $this->backfillRecordDeptIdByAdmin('dice_play_record_test')); if (!empty($validDeptIds) && $this->tableHasColumn('dice_play_record_test', 'dept_id')) { $rootDeptId = (int) min($validDeptIds); $stats['dice_play_record_test_legacy'] = Db::table('dice_play_record_test') ->where(function ($q) { $q->whereNull('dept_id')->whereOr('dept_id', 0); }) ->update(['dept_id' => $rootDeptId]); } if ($this->tableHasColumn('dice_reward_config_record', 'dept_id')) { $stats['dice_reward_config_record'] = Db::execute( 'UPDATE dice_reward_config_record r INNER JOIN sa_system_user u ON r.admin_id = u.id SET r.dept_id = u.dept_id WHERE (r.dept_id IS NULL OR r.dept_id = 0) AND u.dept_id IS NOT NULL AND u.dept_id > 0' ); } return $stats; } /** * 删除渠道前关联数据统计 */ public function getDestroyPreview(array $deptIds): array { $items = []; foreach ($deptIds as $deptId) { $deptId = (int) $deptId; if ($deptId <= 0) { continue; } $dept = SystemDept::find($deptId); $row = [ 'dept_id' => $deptId, 'dept_name' => $dept ? $dept->name : '', 'user_count' => SystemUser::where('dept_id', $deptId)->count(), 'relations' => [], ]; foreach (self::RELATION_TABLES as $table => $meta) { if (!$this->tableHasColumn($table, 'dept_id')) { continue; } $count = $this->countByDept($table, $deptId); if ($count > 0) { $row['relations'][] = [ 'table' => $table, 'label' => $meta['label'], 'group' => $meta['group'], 'count' => $count, ]; } } $items[] = $row; } return $items; } /** * 删除渠道及勾选的关联数据 * * @param array $deleteTables 要删除的表名列表 */ public function destroyDeptWithRelations(int $deptId, array $deleteTables): void { if ($deptId <= 0) { throw new ApiException('Invalid channel id'); } $userCount = SystemUser::where('dept_id', $deptId)->count(); if ($userCount > 0) { throw new ApiException('This channel has users, please delete or transfer them first'); } $allowed = array_keys(self::RELATION_TABLES); foreach ($deleteTables as $table) { if (!in_array($table, $allowed, true)) { continue; } if (!$this->tableHasColumn($table, 'dept_id')) { continue; } Db::table($table)->where('dept_id', $deptId)->delete(); } SystemDept::destroy($deptId, true); DiceRewardConfig::refreshCache($deptId); DiceReward::refreshCache($deptId); } /** * @return array> */ private function defaultTemplateRows(string $table): array { $templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT; $rows = Db::table($table)->where('dept_id', $templateId)->select()->toArray(); if (!empty($rows)) { return $rows; } return Db::table($table)->whereNull('dept_id')->select()->toArray(); } private function backfillRecordDeptIdByPlayer(string $table): array { if (!$this->tableHasColumn($table, 'dept_id') || !$this->tableHasColumn($table, 'player_id')) { return []; } return [ $table => Db::execute( "UPDATE `{$table}` r INNER JOIN dice_player p ON r.player_id = p.id SET r.dept_id = p.dept_id WHERE (r.dept_id IS NULL OR r.dept_id = 0) AND p.dept_id IS NOT NULL AND p.dept_id > 0" ), ]; } private function backfillRecordDeptIdByAdmin(string $table): array { if (!$this->tableHasColumn($table, 'dept_id') || !$this->tableHasColumn($table, 'admin_id')) { return []; } return [ $table => Db::execute( "UPDATE `{$table}` r INNER JOIN sa_system_user u ON r.admin_id = u.id SET r.dept_id = u.dept_id WHERE (r.dept_id IS NULL OR r.dept_id = 0) AND u.dept_id IS NOT NULL AND u.dept_id > 0" ), ]; } private function countByDept(string $table, ?int $deptId): int { $query = Db::table($table); if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) { $templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT; $query->where(function ($q) use ($templateId) { $q->where('dept_id', $templateId)->whereOr('dept_id', 'null'); }); } else { $query->where('dept_id', $deptId); } return $query->count(); } private function tableHasColumn(string $table, string $column): bool { try { $fields = Db::getFields($table); return isset($fields[$column]); } catch (\Throwable $e) { return false; } } }