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'; $bakTable = $prefix . 'backend_power_reindex_bak'; 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. 读全表,按 DFS 算新编号 ------------------------- */ $rows = $pdo->query( "SELECT `id`, `pid`, `sort`, `name`, `title` FROM `{$powerTable}` ORDER BY `pid`, `sort`, `id`" )->fetchAll(); if (! $rows) { echo "[中止] {$powerTable} 是空表\n"; exit(0); } $children = []; $exists = []; foreach ($rows as $r) { $exists[(int) $r['id']] = true; } $orphans = []; foreach ($rows as $r) { $pid = (int) $r['pid']; // pid 指向不存在的行(历史脏数据)当根节点处理,避免整棵树漏掉这一支 if ($pid !== 0 && ! isset($exists[$pid])) { $orphans[] = (int) $r['id'] . '(' . $r['name'] . ',原 pid=' . $pid . ')'; $pid = 0; } $children[$pid][] = $r; } foreach ($children as &$list) { usort($list, static function (array $a, array $b): int { return ((int) $a['sort'] <=> (int) $b['sort']) ?: ((int) $a['id'] <=> (int) $b['id']); }); } unset($list); /** * 深度优先:父先分配编号,紧接着分配它的孩子——保证 id(parent) < id(child) * * @param int $pid * @param array $children * @param array $map oldId => newId * @param int $next * @return void */ $walk = function (int $pid, array &$children, array &$map, int &$next) use (&$walk): void { foreach ($children[$pid] ?? [] as $row) { $old = (int) $row['id']; $map[$old] = $next++; $walk($old, $children, $map, $next); } }; $map = []; $next = 1; $walk(0, $children, $map, $next); if ($orphans) { echo '[提示] 发现 pid 悬空的行,已当根节点处理:' . implode('、', $orphans) . "\n"; } $total = count($rows); $changed = 0; $planLine = []; foreach ($rows as $r) { $old = (int) $r['id']; $new = $map[$old]; if ($old !== $new) { $changed++; } $planLine[] = sprintf(' %3d → %3d %-24s %s', $old, $new, $r['name'], $r['title']); } echo "[计划] 共 {$total} 个节点,编号变动 {$changed} 个\n"; echo implode("\n", $planLine) . "\n"; if ($changed === 0) { echo "[无需处理] 编号已经是连续的 DFS 顺序\n"; exit(0); } $hasRolePower = $tableExists($rolePowerTable); $grantRows = $hasRolePower ? (int) $pdo->query("SELECT COUNT(*) FROM `{$rolePowerTable}`")->fetchColumn() : 0; echo "[计划] 同步 {$rolePowerTable} 的 role_id/power_id 映射(共 {$grantRows} 条授权)\n"; echo '[计划] AUTO_INCREMENT 置为 ' . ($total + 1) . "\n"; if (! $force) { echo "[提示] 以上是预演结果,未做任何改动。确认后执行: php scripts/reindex_backend_power.php --force\n"; exit(0); } /* ------------------------- 2. 备份 + 事务内重编号 ------------------------- */ if ($tableExists($bakTable)) { // 上一轮跑挂了(回滚后备份还在)或跑完忘了收尾,都可以靠 --drop-bak 直接重来 if (! $dropBak) { fwrite(STDERR, "[中止] 备份表 {$bakTable} 已存在,先确认它是否可以删除后再跑,避免覆盖历史备份\n"); fwrite(STDERR, "[提示] 确认无用后重跑: php scripts/reindex_backend_power.php --force --drop-bak\n"); exit(1); } $pdo->exec("DROP TABLE IF EXISTS `{$bakTable}`"); $pdo->exec("DROP TABLE IF EXISTS `{$bakTable}_role`"); echo "[备份] 已按 --drop-bak 删除旧备份\n"; } $pdo->exec("CREATE TABLE `{$bakTable}` AS SELECT * FROM `{$powerTable}`"); $pdo->exec("CREATE TABLE `{$bakTable}_role` AS SELECT * FROM `{$rolePowerTable}`"); echo "[备份] {$bakTable} 与 {$bakTable}_role 已建立\n"; // 中间态偏移量:id/pid 是 int unsigned,最大 4294967295,这里加到 100 万量级足够安全 $offset = 1000000; $pdo->beginTransaction(); try { // 阶段一:整表搬到 OFFSET 高位区间。id/pid 都是 unsigned,负数中间态不行, // 只能先整体抬到一个没人用的区间;自重编号可能互换(85→2、2→3),逐行改必然撞唯一键。 $updPower = $pdo->prepare("UPDATE `{$powerTable}` SET `id` = ?, `pid` = ? WHERE `id` = ?"); foreach ($map as $old => $new) { $row = null; foreach ($rows as $r) { if ((int) $r['id'] === $old) { $row = $r; break; } } $oldPid = (int) $row['pid']; $newPid = $map[$oldPid] ?? 0; // 父不在本表(悬空)则归零 $updPower->execute([$new + $offset, $newPid === 0 ? 0 : $newPid + $offset, $old]); } if ($hasRolePower) { $updGrant = $pdo->prepare("UPDATE `{$rolePowerTable}` SET `power_id` = ? WHERE `power_id` = ?"); foreach ($map as $old => $new) { $updGrant->execute([$new + $offset, $old]); } } // 阶段二:把偏移量减掉,落到最终的 1..N。 // 必须带 WHERE 限定「只动搬上去的那些」:role_power 里可能早就有指向不存在节点的孤儿行 // (历史遗留,不在 $map 里),无差别减会让它们变成负数。 $pdo->exec("UPDATE `{$powerTable}` SET `id` = `id` - {$offset}" . ", `pid` = IF(`pid` = 0, 0, `pid` - {$offset}) WHERE `id` > {$offset}"); if ($hasRolePower) { $pdo->exec("UPDATE `{$rolePowerTable}` SET `power_id` = `power_id` - {$offset}" . " WHERE `power_id` > {$offset}"); } $pdo->commit(); echo "[执行] 主键重编号完成\n"; } catch (Throwable $e) { $pdo->rollBack(); fwrite(STDERR, '[失败] 已回滚:' . $e->getMessage() . "\n"); fwrite(STDERR, "[提示] 如需还原: DROP TABLE `{$powerTable}`; RENAME TABLE `{$bakTable}` TO `{$powerTable}`;\n"); exit(1); } $pdo->exec("ALTER TABLE `{$powerTable}` AUTO_INCREMENT = " . ($total + 1)); echo '[执行] AUTO_INCREMENT 已置为 ' . ($total + 1) . "\n"; // 复核 $bad = $pdo->query("SELECT COUNT(*) FROM `{$powerTable}` p LEFT JOIN `{$powerTable}` f ON p.`pid` = f.`id` WHERE p.`pid` <> 0 AND f.`id` IS NULL")->fetchColumn(); echo "[复核] 悬空 pid 行数:{$bad}\n"; echo "[完成] 建议顺手把 install.sql 的种子同步成同一套编号(本次种子改的就是这个)\n";