mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-10-29 08:58:22 +08:00
对齐迁移是菜单快照进入数据库的唯一途径(新装与存量库同路径);缺失行按显式 id 补插,框架结构行仅对齐 pid+sort,软删/用户分组跳过;新增规则文件与索引;清理 status 模块孤儿占位文件。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
132 lines
5.2 KiB
PHP
132 lines
5.2 KiB
PHP
<?php
|
||
|
||
/**
|
||
* @internal-framework
|
||
*
|
||
* 此文件为框架核心服务
|
||
*
|
||
* 用途:菜单快照对齐服务(存量库菜单补齐与结构对齐)
|
||
* 维护者:框架维护者
|
||
*
|
||
* 背景:菜单快照数据由对齐迁移在 migrate:run 时写入数据库,新装与存量库走同一条
|
||
* 安装/更新路径(phinxlog 保证幂等)。本服务把快照对齐到当前库,供数据型 migration
|
||
* 调用,对齐只增不删。
|
||
*
|
||
* 对齐语义(保守,尊重用户自定义):
|
||
* - 行不存在(按 id) → 按快照 INSERT(显式 id)
|
||
* - 行存在且挂在框架结构上 → 仅对齐 pid + sort(不动 title/icon/status/href)
|
||
* - 行存在但挂在用户自建分组(pid 在框架 id 集合与 {0, 99999999} 之外)→ 跳过不动
|
||
* - 行存在但已被软删除(用户隐藏) → 跳过不复活
|
||
* - id 被占但 href 与快照不符 → 跳过并告警(疑似用户行占用快照 id,人工介入)
|
||
*/
|
||
|
||
namespace base\admin\service;
|
||
|
||
use think\console\Output;
|
||
use think\facade\Db;
|
||
|
||
class MenuAlignServiceBase
|
||
{
|
||
/**
|
||
* 快照中「欢迎页」挂载的虚拟 pid(MenuConstant::HOME_PID),不算用户分组.
|
||
*/
|
||
protected const HOME_PID = 99999999;
|
||
|
||
/**
|
||
* @var Output|null 命令行输出(migration 场景传入,便于追踪对齐过程)
|
||
*/
|
||
protected $output = null;
|
||
|
||
public function __construct($output = null)
|
||
{
|
||
$this->output = $output;
|
||
}
|
||
|
||
/**
|
||
* 把 adminInitData/SystemMenu.php 快照对齐到当前库.
|
||
*
|
||
* @param string $snapshotName 快照文件名(adminInitData/ 目录下,不含扩展名)
|
||
*
|
||
* @return array{inserted: int, aligned: int, skipped_exists: int, skipped_user: int, warnings: array<int, string>}
|
||
*/
|
||
public function align(string $snapshotName = 'SystemMenu'): array
|
||
{
|
||
$snapshot = include __DIR__ . '/adminInitData/' . $snapshotName . '.php';
|
||
|
||
// 框架合法 pid 集合:快照内全部 id + 顶级 0 + 欢迎页虚拟 pid
|
||
$frameworkPids = array_merge(array_column($snapshot, 'id'), [0, self::HOME_PID]);
|
||
|
||
$stats = [
|
||
'inserted' => 0,
|
||
'aligned' => 0,
|
||
'skipped_exists' => 0,
|
||
'skipped_user' => 0,
|
||
'warnings' => [],
|
||
];
|
||
|
||
$now = time();
|
||
|
||
foreach ($snapshot as $row) {
|
||
// 注意:每次操作都取全新 Query 对象,复用同一对象会残留 where 条件导致误判。
|
||
// 原生查询不过滤软删除:软删行仍占用主键,必须先识别,避免 INSERT 冲突或复活。
|
||
$exists = Db::name('system_menu')->where('id', $row['id'])->find();
|
||
|
||
if (empty($exists)) {
|
||
$data = $row;
|
||
$data['create_time'] = $now;
|
||
$data['update_time'] = $now;
|
||
$data['delete_time'] = 0;
|
||
Db::name('system_menu')->insert($data);
|
||
++$stats['inserted'];
|
||
$this->writeln(" - 新增菜单 id={$row['id']} {$row['title']} (pid={$row['pid']})");
|
||
continue;
|
||
}
|
||
|
||
// 用户主动软删除(隐藏)的框架菜单:不复活
|
||
if ((int) ($exists['delete_time'] ?? 0) !== 0) {
|
||
++$stats['skipped_user'];
|
||
$this->writeln(" - 菜单 id={$row['id']} {$row['title']} 已被软删除(用户隐藏),跳过");
|
||
continue;
|
||
}
|
||
|
||
// id 被非快照行占用:href 不一致(分组行 href 均为空串,仅叶子行可比对)
|
||
$snapshotHref = $row['href'] ?? '';
|
||
if ($snapshotHref !== '' && ($exists['href'] ?? '') !== $snapshotHref) {
|
||
++$stats['skipped_user'];
|
||
$stats['warnings'][] = "菜单 id={$row['id']} 已被其他记录占用(href={$exists['href']}),快照行 {$snapshotHref} 未插入,请人工核对";
|
||
continue;
|
||
}
|
||
|
||
// 用户已把该行挪进自建分组:不视为框架结构,跳过
|
||
if (!in_array((int) $exists['pid'], $frameworkPids, true)) {
|
||
++$stats['skipped_user'];
|
||
$this->writeln(" - 菜单 id={$row['id']} {$row['title']} 已挂用户分组(pid={$exists['pid']}),跳过");
|
||
continue;
|
||
}
|
||
|
||
// 结构对齐:仅 pid + sort,不动文案与状态
|
||
if ((int) $exists['pid'] !== (int) $row['pid'] || (int) $exists['sort'] !== (int) $row['sort']) {
|
||
Db::name('system_menu')->where('id', $row['id'])->update([
|
||
'pid' => $row['pid'],
|
||
'sort' => $row['sort'],
|
||
'update_time' => $now,
|
||
]);
|
||
++$stats['aligned'];
|
||
$this->writeln(" - 对齐菜单 id={$row['id']} {$row['title']} (pid {$exists['pid']}->{$row['pid']}, sort {$exists['sort']}->{$row['sort']})");
|
||
continue;
|
||
}
|
||
|
||
++$stats['skipped_exists'];
|
||
}
|
||
|
||
return $stats;
|
||
}
|
||
|
||
protected function writeln(string $message): void
|
||
{
|
||
if ($this->output !== null) {
|
||
$this->output->writeln($message);
|
||
}
|
||
}
|
||
}
|