CheckMenuValidity.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Module\System\Models\AdminMenu;
  4. use Illuminate\Console\Command;
  5. use Illuminate\Support\Facades\Route;
  6. use Illuminate\Support\Str;
  7. /**
  8. * 检查后台菜单有效性命令
  9. *
  10. * 检查后台菜单项是否有对应的控制器和路由,可选择删除无效菜单项
  11. */
  12. class CheckMenuValidity extends Command
  13. {
  14. /**
  15. * The name and signature of the console command.
  16. *
  17. * @var string
  18. */
  19. protected $signature = 'admin:check-menu-validity {--delete : 是否删除无效的菜单项}';
  20. /**
  21. * The console command description.
  22. *
  23. * @var string
  24. */
  25. protected $description = '检查菜单项是否有效,包括检查是否有对应的控制器和路由';
  26. /**
  27. * 已知的有效路由前缀
  28. *
  29. * @var array
  30. */
  31. protected $validPrefixes = [
  32. 'auth', 'dashboard', 'helpers', 'media', 'settings', 'logs', 'config'
  33. ];
  34. /**
  35. * Execute the console command.
  36. *
  37. * @return int
  38. */
  39. public function handle()
  40. {
  41. $this->info("开始检查菜单项的有效性...");
  42. // 获取所有菜单项
  43. $menus = AdminMenu::all();
  44. // 获取所有路由
  45. $routes = Route::getRoutes();
  46. $routeUris = [];
  47. foreach ($routes as $route) {
  48. if (Str::startsWith($route->uri(), 'admin/')) {
  49. $routeUris[] = str_replace('admin/', '', $route->uri());
  50. }
  51. }
  52. // 获取所有模块的控制器
  53. $modules = [
  54. 'Activity', 'Article', 'Farm', 'File', 'Fund', 'Game', 'GameItems',
  55. 'OAuth', 'Pet', 'Sms', 'System', 'Task', 'Team', 'User'
  56. ];
  57. $controllerUris = [];
  58. foreach ($modules as $module) {
  59. $controllerDir = app_path("Module/{$module}/AdminControllers");
  60. if (!is_dir($controllerDir)) {
  61. continue;
  62. }
  63. $controllers = array_filter(scandir($controllerDir), function($file) {
  64. return !in_array($file, ['.', '..']) &&
  65. pathinfo($file, PATHINFO_EXTENSION) === 'php' &&
  66. !Str::contains($file, 'Helper');
  67. });
  68. foreach ($controllers as $controller) {
  69. $controllerName = pathinfo($controller, PATHINFO_FILENAME);
  70. $uri = $this->getControllerUri($controllerName);
  71. $controllerUris[] = $uri;
  72. }
  73. }
  74. $validMenus = [];
  75. $invalidMenus = [];
  76. $parentMenus = [];
  77. foreach ($menus as $menu) {
  78. // 空URI的菜单项是父菜单(不可点击的,只用于展开子菜单)
  79. if (empty($menu->uri)) {
  80. $parentMenus[] = $menu;
  81. continue;
  82. }
  83. // 检查URI是否有效
  84. $isValid = $this->isValidUri($menu->uri, $routeUris, $controllerUris);
  85. if ($isValid) {
  86. $validMenus[] = $menu;
  87. } else {
  88. $invalidMenus[] = $menu;
  89. }
  90. }
  91. // 显示父菜单
  92. $this->info("\n父菜单(不可点击的,只用于展开子菜单): " . count($parentMenus) . " 个");
  93. $table = [];
  94. foreach ($parentMenus as $menu) {
  95. $table[] = [
  96. 'id' => $menu->id,
  97. 'title' => $menu->title,
  98. 'parent_id' => $menu->parent_id,
  99. 'order' => $menu->order,
  100. 'icon' => $menu->icon
  101. ];
  102. }
  103. $this->table(['ID', '标题', '父ID', '排序', '图标'], $table);
  104. // 显示有效的菜单项
  105. $this->info("\n有效的菜单项(有对应的控制器或路由): " . count($validMenus) . " 个");
  106. // 显示无效的菜单项
  107. if (count($invalidMenus) > 0) {
  108. $this->info("\n无效的菜单项(没有对应的控制器或路由): " . count($invalidMenus) . " 个");
  109. $table = [];
  110. foreach ($invalidMenus as $menu) {
  111. $table[] = [
  112. 'id' => $menu->id,
  113. 'title' => $menu->title,
  114. 'uri' => $menu->uri,
  115. 'parent_id' => $menu->parent_id,
  116. 'order' => $menu->order
  117. ];
  118. }
  119. $this->table(['ID', '标题', 'URI', '父ID', '排序'], $table);
  120. // 删除无效的菜单项
  121. if ($this->option('delete') || $this->confirm('是否要删除这些无效的菜单项?')) {
  122. foreach ($invalidMenus as $menu) {
  123. // 获取子菜单
  124. $childMenus = AdminMenu::where('parent_id', $menu->id)->get();
  125. // 更新子菜单的父ID
  126. foreach ($childMenus as $childMenu) {
  127. $childMenu->parent_id = $menu->parent_id;
  128. $childMenu->save();
  129. $this->info(" - 更新子菜单: ID = {$childMenu->id}, 标题 = {$childMenu->title}, 新父ID = {$menu->parent_id}");
  130. }
  131. // 删除菜单项
  132. $menu->delete();
  133. $this->info(" - 删除菜单项: ID = {$menu->id}, 标题 = {$menu->title}, URI = {$menu->uri}");
  134. }
  135. $this->info("\n已删除 " . count($invalidMenus) . " 个无效的菜单项");
  136. }
  137. } else {
  138. $this->info("\n所有菜单项都是有效的!");
  139. }
  140. return 0;
  141. }
  142. /**
  143. * 检查URI是否有效
  144. *
  145. * @param string $uri
  146. * @param array $routeUris
  147. * @param array $controllerUris
  148. * @return bool
  149. */
  150. protected function isValidUri(string $uri, array $routeUris, array $controllerUris): bool
  151. {
  152. // 跳过已知有效的路由前缀
  153. foreach ($this->validPrefixes as $prefix) {
  154. if (Str::startsWith($uri, $prefix)) {
  155. return true;
  156. }
  157. }
  158. // 检查URI是否存在于路由中
  159. $uri = ltrim($uri, '/');
  160. // 检查路由
  161. foreach ($routeUris as $routeUri) {
  162. if (Str::startsWith($routeUri, $uri) || $routeUri === $uri) {
  163. return true;
  164. }
  165. }
  166. // 检查控制器
  167. foreach ($controllerUris as $controllerUri) {
  168. if ($controllerUri === $uri) {
  169. return true;
  170. }
  171. }
  172. return false;
  173. }
  174. /**
  175. * 根据控制器名称猜测URI
  176. *
  177. * @param string $controllerName
  178. * @return string
  179. */
  180. protected function getControllerUri(string $controllerName): string
  181. {
  182. // 移除Controller后缀
  183. $name = str_replace('Controller', '', $controllerName);
  184. // 转换为kebab-case
  185. $uri = Str::kebab($name);
  186. return $uri;
  187. }
  188. }