删除(连同挂它下面的子权限与角色授权) * - 按钮节点 system:clean ——> 就地把 name/route 改成 console:clean,并挂到「控制台」菜单下 * * 为什么要就地改名而不是新建:新节点的 id 是新的,角色授权表(backend_role_power)里没有它, * 非超管账号会因为拿不到这个按钮权限而被 auth.filterDom() 摘掉按钮。 * 保留原 id 就等于保留全部既有授权,老库零感知。 * * 用法: * php scripts/migrate_console_clean.php # 预演:只统计,不动库(默认) * php scripts/migrate_console_clean.php --force # 真正执行 * * 幂等:跑第二次时 system:* 已不存在,只要 console:clean 还在「控制台」下就什么都不做。 */ declare(strict_types=1); $root = dirname(__DIR__) . DIRECTORY_SEPARATOR; $force = in_array('--force', $argv ?? [], true); echo $force ? "[模式] 执行:以下动作会真实落库\n" : "[模式] 预演(dry-run):只统计不改动,加 --force 才执行\n"; /* ------------------------- 读配置:极简 env() + include database.php ------------------------- */ $GLOBALS['__env'] = []; foreach ([$root . '.env', $root . '.env.local'] as $envFile) { if (! is_file($envFile)) { continue; } foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $line = trim($line); if ($line === '' || $line[0] === '#') { continue; } $pos = strpos($line, '='); if ($pos === false) { continue; } $GLOBALS['__env'][trim(substr($line, 0, $pos))] = trim(trim(substr($line, $pos + 1)), "\"'"); } } if (! function_exists('env')) { function env(string $key = '', $default = null) { return $GLOBALS['__env'][$key] ?? $default; } } $dbConfFile = $root . 'config' . DIRECTORY_SEPARATOR . 'database.php'; if (! is_file($dbConfFile)) { fwrite(STDERR, "找不到配置文件: {$dbConfFile}\n"); exit(1); } $dbConf = include $dbConfFile; $conn = $dbConf['connections'][$dbConf['default'] ?? 'mysql'] ?? []; /* ------------------------- 连库 ------------------------- */ $dsn = sprintf( 'mysql:host=%s;port=%d;dbname=%s;charset=%s', $conn['hostname'] ?? '127.0.0.1', (int) ($conn['hostport'] ?? 3306), $conn['database'] ?? '', $conn['charset'] ?: 'utf8mb4' ); try { $pdo = new PDO($dsn, $conn['username'] ?? 'root', $conn['password'] ?? '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } catch (Throwable $e) { fwrite(STDERR, '数据库连接失败: ' . $e->getMessage() . "\n"); exit(1); } $prefix = (string) ($conn['prefix'] ?? ''); $powerTable = $prefix . 'backend_power'; $rolePowerTable = $prefix . 'backend_role_power'; $roleTable = $prefix . 'backend_role'; echo '[连接] ' . ($conn['hostname'] ?? '') . ':' . ($conn['hostport'] ?? 3306) . ' / ' . ($conn['database'] ?? '') . " (表前缀 {$prefix})\n"; $tableExists = static function (string $table) use ($pdo): bool { return (bool) $pdo->query("SHOW TABLES LIKE '{$table}'")->fetchColumn(); }; if (! $tableExists($powerTable)) { echo "[权限] {$powerTable} 不存在,没什么可迁移的\n"; exit(0); } /* ------------------------- 1. 「控制台」节点:新权限的挂载点 ------------------------- */ $consoleRow = $pdo->query( "SELECT `id`, `title` FROM `{$powerTable}` WHERE `name` = 'console:index' ORDER BY `id` LIMIT 1" )->fetch(); $consoleId = $consoleRow ? (int) $consoleRow['id'] : 0; echo $consoleId ? "[挂载点] 控制台 #{$consoleId}({$consoleRow['title']})\n" : "[挂载点] 找不到 console:index 节点,新节点将挂到根节点(pid=0)\n"; /* ------------------------- 2. 系统信息页节点:删除 ------------------------- */ $menuRow = $pdo->query( "SELECT `id`, `name`, `title` FROM `{$powerTable}` WHERE `name` = 'system:index' ORDER BY `id` LIMIT 1" )->fetch(); if ($menuRow) { $menuId = (int) $menuRow['id']; echo "[下线] 找到「{$menuRow['title']}」#{$menuId}({$menuRow['name']})\n"; $children = $pdo->query( "SELECT `id`, `name`, `title` FROM `{$powerTable}` WHERE `pid` = {$menuId} ORDER BY `id`" )->fetchAll(); foreach ($children as $row) { echo sprintf(" 子节点 #%d %s (%s)\n", (int) $row['id'], $row['name'], $row['title']); } $ids = array_merge([$menuId], array_map('intval', array_column($children, 'id'))); $in = implode(',', $ids); $hasRolePower = $tableExists($rolePowerTable); $grantRows = $hasRolePower ? (int) $pdo->query("SELECT COUNT(*) FROM `{$rolePowerTable}` WHERE `power_id` IN ({$in})")->fetchColumn() : 0; echo "[下线] 待删权限节点 " . count($ids) . " 个、角色授权 {$grantRows} 条\n"; if ($force) { $pdo->beginTransaction(); try { if ($hasRolePower) { $pdo->exec("DELETE FROM `{$rolePowerTable}` WHERE `power_id` IN ({$in})"); } // system:clean 要在下一步改造成 console:clean,别在这里跟着父节点一起删了 $pdo->exec( "DELETE FROM `{$powerTable}` WHERE `id` IN ({$in}) AND `name` NOT IN ('system:clean', 'console:clean')" ); $pdo->commit(); echo "[执行] 页面节点与其下无用子节点已删除(system:clean 保留待改造)\n"; } catch (Throwable $e) { $pdo->rollBack(); fwrite(STDERR, '[失败] 删除回滚: ' . $e->getMessage() . "\n"); exit(1); } } } else { echo "[下线] 没有 system:index 节点,跳过\n"; } /* ------------------------- 3. 清理按钮权限:system:clean → console:clean ------------------------- */ $oldClean = $pdo->query( "SELECT `id`, `name`, `title` FROM `{$powerTable}` WHERE `name` = 'system:clean' ORDER BY `id` LIMIT 1" )->fetch(); $newClean = $pdo->query( "SELECT `id`, `name`, `title` FROM `{$powerTable}` WHERE `name` = 'console:clean' ORDER BY `id` LIMIT 1" )->fetch(); if ($oldClean && $newClean) { // 两边都有:把授权合并到 console:clean,删掉旧的那个 $oldId = (int) $oldClean['id']; $newId = (int) $newClean['id']; echo "[合并] system:clean #{$oldId} 与 console:clean #{$newId} 并存,授权合并到后者并删除旧节点\n"; if ($force && $tableExists($rolePowerTable)) { $pdo->beginTransaction(); try { // 目标节点已有授权的行会撞唯一键,先去重再迁移 $owned = $pdo->query( "SELECT `role_id` FROM `{$rolePowerTable}` WHERE `power_id` = {$newId}" )->fetchAll(PDO::FETCH_COLUMN); $ownedIds = $owned ? implode(',', array_map('intval', $owned)) : '0'; $pdo->exec( "DELETE FROM `{$rolePowerTable}` WHERE `power_id` = {$oldId} AND `role_id` IN ({$ownedIds})" ); $pdo->exec("UPDATE `{$rolePowerTable}` SET `power_id` = {$newId} WHERE `power_id` = {$oldId}"); $pdo->exec("DELETE FROM `{$powerTable}` WHERE `id` = {$oldId}"); $pdo->commit(); echo "[执行] 授权迁移完成,旧节点已删除\n"; } catch (Throwable $e) { $pdo->rollBack(); fwrite(STDERR, '[失败] 合并回滚: ' . $e->getMessage() . "\n"); exit(1); } } } elseif ($oldClean) { $oldId = (int) $oldClean['id']; echo "[改名] system:clean #{$oldId} → console:clean,路由 system/clean → console/clean,挂到 #{$consoleId}\n"; if ($force) { $pdo->prepare( "UPDATE `{$powerTable}` SET `name` = 'console:clean', `route` = 'console/clean', `pid` = ?, `update_at` = ? WHERE `id` = ?" )->execute([$consoleId, time(), $oldId]); echo "[执行] 已改名(id 不变 ⇒ 角色授权自动保留)\n"; } } elseif ($newClean) { echo "[跳过] console:clean #" . (int) $newClean['id'] . " 已就位\n"; } else { echo "[补建] 没有 system:clean,也没有 console:clean —— 补建一个并授权给所有角色\n"; if ($force) { $now = time(); $newId = (int) $pdo->query("SELECT IFNULL(MAX(`id`), 0) + 1 FROM `{$powerTable}`")->fetchColumn(); $stmt = $pdo->prepare( "INSERT INTO `{$powerTable}` (`id`, `pid`, `title`, `name`, `type`, `route`, `icon`, `sort`, `addon`, `status`, `create_at`, `update_at`, `delete_at`) VALUES (?, ?, '清理缓存', 'console:clean', 3, 'console/clean', '', 1, NULL, 1, ?, ?, 0)" ); $stmt->execute([$newId, $consoleId, $now, $now]); echo "[执行] 已补建 console:clean #{$newId}\n"; // 新节点不在任何角色的授权列表里 ⇒ 非超管看不见按钮,必须全角色补授权 if ($tableExists($roleTable) && $tableExists($rolePowerTable)) { $roles = $pdo->query("SELECT `id` FROM `{$roleTable}`")->fetchAll(PDO::FETCH_COLUMN); $ins = $pdo->prepare( "INSERT INTO `{$rolePowerTable}` (`role_id`, `power_id`, `create_at`) VALUES (?, ?, ?)" ); $n = 0; foreach ($roles as $roleId) { $ins->execute([(int) $roleId, $newId, $now]); $n++; } echo "[执行] 已授权给 {$n} 个角色\n"; } } } echo $force ? "[完成] 权限迁移结束(控制台菜单与按钮权限每次请求读库,无需清缓存)\n" : "[提示] 以上为预演结果,未做任何改动。确认后执行: php scripts/migrate_console_clean.php --force\n";