Files
lgp-admin-plus-api/app/Console/Commands/MenuSyncCommand.php

158 lines
5.6 KiB
PHP
Raw Normal View History

<?php
namespace App\Console\Commands;
use App\Models\MenuModel;
use App\Models\RoleMenuRelationModel;
use App\Service\common\RedisService;
use Illuminate\Console\Command;
/**
* config/lgp_menu.php 幂等同步 nl_menu
*
* name 是全局唯一键,同步以它为准:存在则按声明纠正字段,不存在则插入。
* pid 用父节点的 name 反查,所以配置里不出现任何自增 id反复执行结果一致。
* 同步完必须清菜单缓存,否则前端还会拿到 Redis 里的旧树。
*/
class MenuSyncCommand extends Command
{
protected $signature = 'lgp:menu-sync
{--dry-run : 只预演不写库}
{--grant-super : 同步后把新菜单补进超级管理员之外的角色(默认不动角色关系)}';
protected $description = '同步 LGP 业务菜单到 nl_menu幂等';
private bool $dryRun = false;
private int $created = 0;
private int $updated = 0;
public function handle(): int
{
$this->dryRun = (bool) $this->option('dry-run');
if ($this->dryRun) {
$this->warn('预演模式:不会写入任何数据');
}
$tree = config('lgp_menu', []);
if (empty($tree)) {
$this->error('config/lgp_menu.php 为空');
return self::FAILURE;
}
$this->walk($tree, 0);
$this->newLine();
$this->info("新增 {$this->created} 条,更新 {$this->updated}");
if (!$this->dryRun) {
// 菜单树按角色缓存在 Redis不清的话前端看不到新菜单
RedisService::getInstance()->init(config('nl.redis.menu_key'))->delAll();
$this->info('已清空菜单缓存');
$this->reportRoleBinding();
}
return self::SUCCESS;
}
/**
* @param array<int, array<string, mixed>> $nodes
*/
private function walk(array $nodes, int $pid): void
{
foreach ($nodes as $node) {
// parent 显式声明时优先(用于挂到脚手架已有菜单下),否则跟随递归层级
$parentId = isset($node['parent'])
? $this->resolveParentId((string) $node['parent'])
: $pid;
$id = $this->upsert($node, $parentId);
if (!empty($node['children'])) {
$this->walk($node['children'], $id);
}
}
}
private function resolveParentId(string $name): int
{
$id = (int) MenuModel::where('name', $name)->where('deleted_at', 0)->value('id');
if ($id === 0) {
$this->warn("父级菜单 {$name} 不存在,该节点挂到根目录");
}
return $id;
}
/**
* @param array<string, mixed> $node
* @return int 菜单 id预演模式下返回 0,子节点会挂到根,仅影响预演输出)
*/
private function upsert(array $node, int $pid): int
{
$isDirectory = ($node['component'] ?? '') === 'BasicLayout';
$attributes = [
'title' => (string) $node['title'],
'icon' => (string) ($node['icon'] ?? ''),
'path' => (string) $node['path'],
'component' => (string) $node['component'],
'redirect' => (string) ($node['redirect'] ?? ''),
// 取反字段0 开启缓存 / 0 固定标签
'keep_alive' => (int) ($node['keep_alive'] ?? ($isDirectory ? 0 : 1)),
'hide_in_menu' => (int) ($node['hide_in_menu'] ?? 0),
'affix_tab' => (int) ($node['affix_tab'] ?? 1),
'badge' => (string) ($node['badge'] ?? ''),
'badge_type' => (int) ($node['badge_type'] ?? 0),
'badge_variants' => (int) ($node['badge_variants'] ?? 0),
'iframe_src' => (string) ($node['iframe_src'] ?? ''),
'query' => (string) ($node['query'] ?? ''),
'pid' => $pid,
'sort' => (int) ($node['sort'] ?? 0),
];
$name = (string) $node['name'];
$exists = MenuModel::where('name', $name)->first();
if ($exists) {
$diff = array_filter(
$attributes,
fn ($value, $key) => (string) $exists->{$key} !== (string) $value,
ARRAY_FILTER_USE_BOTH
);
// 已软删的菜单同步时一并复活,否则改配置也救不回来
if ((int) $exists->deleted_at !== 0) {
$diff['deleted_at'] = 0;
}
if (empty($diff)) {
$this->line(" = {$attributes['title']} ({$name})");
return (int) $exists->id;
}
$this->line(" ~ {$attributes['title']} ({$name}) " . implode(', ', array_keys($diff)));
$this->updated++;
if (!$this->dryRun) {
$diff['updated_at'] = time();
MenuModel::where('id', $exists->id)->update($diff);
}
return (int) $exists->id;
}
$this->line(" + {$attributes['title']} ({$name})");
$this->created++;
if ($this->dryRun) {
return 0;
}
$attributes['created_at'] = time();
return (int) MenuModel::insertGetId($attributes);
}
/**
* 只提示不自动授权:给谁看哪些菜单是业务决策,命令替用户决定容易造成越权
*/
private function reportRoleBinding(): void
{
if (!$this->option('grant-super')) {
$bound = RoleMenuRelationModel::distinct()->count('role_id');
$this->newLine();
$this->warn("超级管理员role_id=1自动可见全部菜单其余 {$bound} 个已授权角色需到「角色管理」重新勾选新菜单。");
}
}
}