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> $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 $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} 个已授权角色需到「角色管理」重新勾选新菜单。"); } } }