From ecdb5c1976ce1ee9f18829abc3a255ba4b5942ac Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Fri, 28 Feb 2025 18:13:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E5=88=97=E8=A1=A8/=E5=88=86=E9=A1=B5/=E8=AF=A6=E6=83=85/?= =?UTF-8?q?=E6=96=B0=E5=A2=9E/=E6=9B=B4=E6=96=B0/=E5=AF=BC=E5=85=A5/?= =?UTF-8?q?=E5=AF=BC=E5=87=BA/=E5=88=A0=E9=99=A4=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/v1/Menu.php | 483 ++++++++++++++++++ .../v1/SysMenuAbilityPermissionModel.php | 19 + app/admin/model/v1/SysMenuModel.php | 51 ++ app/admin/route/v1.php | 26 + app/admin/validate/v1/SysMenuValidate.php | 72 +++ .../SysMenuAbilityPermissionBaseModel.php | 24 + app/common/model/SysMenuBaseModel.php | 37 ++ .../20241228093758_create_sys_menu.php | 2 + 8 files changed, 714 insertions(+) create mode 100644 app/admin/controller/v1/Menu.php create mode 100644 app/admin/model/v1/SysMenuAbilityPermissionModel.php create mode 100644 app/admin/model/v1/SysMenuModel.php create mode 100644 app/admin/validate/v1/SysMenuValidate.php create mode 100644 app/common/model/SysMenuAbilityPermissionBaseModel.php create mode 100644 app/common/model/SysMenuBaseModel.php diff --git a/app/admin/controller/v1/Menu.php b/app/admin/controller/v1/Menu.php new file mode 100644 index 00000000..3371fea6 --- /dev/null +++ b/app/admin/controller/v1/Menu.php @@ -0,0 +1,483 @@ +param(['title' => '']); + + $menus = SysMenuModel::field([ + 'id', + 'pid', + 'title' + ]) + ->with(['menu_ability_permission']) + ->withSearch(['title'], [ + 'title' => $params['title'] ?? null + ]) + ->enabled() + ->order(['sort' => 'asc', 'id' => 'desc']) + ->select() + ->hidden(['menu_ability_permission.menu_id']); + + return success('获取成功', array_to_tree($menus->toArray(), 0, 'pid')); + } + + // 分页数据 + public function index() + { + $params = request()->param(['title' => '']); + + $menus = SysMenuModel::field([ + 'id', + 'pid', + 'icon', + 'title', + 'sort', + 'hidden', + 'status' + ]) + ->withSearch(['title'], [ + 'title' => $params['title'] ?? null + ]) + ->order(['sort' => 'asc', 'id' => 'desc']) + ->select(); + + return success('获取成功', array_to_tree($menus->toArray(), 0, 'pid')); + } + + // 详情数据 + public function read() + { + $id = request()->param('id'); + + $menu = SysMenuModel::withoutField([ + 'created_at', + 'updated_at', + 'deleted_at' + ]) + ->with(['menu_ability_permission']) + ->bypk($id) + ->find() + ->hidden(['menu_ability_permission.menu_id']); + if (empty($menu)) { + return error('菜单不存在'); + } + + return success('获取成功', $menu); + } + + // 新增数据 + public function save() + { + $post = request()->post([ + 'pid' => 0, + 'title', + 'name', + 'path', + 'icon', + 'redirect', + 'component', + 'hidden' => 0, + 'actived' => 0, + 'keep_alive' => 0, + 'sort' => 0, + 'status' => 1, + // $[*].ability_name 能力名称 + // $[*].permission 权限标志 + // $[*].sort 排序 + 'menu_ability_permission' => '[]' + ]); + $menu_ability_permission = []; + if (!empty($post['menu_ability_permission'])) { + $menu_ability_permission = json_decode($post['menu_ability_permission'], true); + unset($post['menu_ability_permission']); + } + + $validate = new SysMenuValidate; + $check_data = array_merge($post, ['menu_ability_permission' => $menu_ability_permission]); + if (!$validate->scene('create')->check($check_data)) { + return error($validate->getError()); + } + + SysMenuModel::startTrans(); + try { + // 新增菜单 + $menu = SysMenuModel::create($post); + if ($menu->isEmpty()) { + throw new InvalidOperateException('新增菜单失败'); + } + + // 新增菜单能力权限 + if (!empty($menu_ability_permission)) { + foreach ($menu_ability_permission as &$item) { + $item['menu_id'] = $menu->id; + } + unset($item); + $permission = $menu->menuAbilityPermission()->saveAll($menu_ability_permission); + if (empty($permission)) { + throw new InvalidOperateException('新增菜单能力权限失败'); + } + } + + SysMenuModel::commit(); + } catch (InvalidOperateException $e) { + SysMenuModel::rollback(); + return error($e->getMessage()); + } catch (\Throwable $th) { + SysMenuModel::rollback(); + return error('操作失败'); + } + + return success('操作成功'); + } + + // 更新数据 + public function update() + { + $id = request()->param('id'); + $put = request()->put([ + 'pid' => 0, + 'title', + 'name', + 'path', + 'icon', + 'redirect', + 'component', + 'hidden' => 0, + 'actived' => 0, + 'keep_alive' => 0, + 'sort' => 0, + 'status' => 1, + // $[*].ability_name 能力名称 + // $[*].permission 权限标志 + // $[*].sort 排序 + 'menu_ability_permission' => '[]' + ]); + $menu_ability_permission = []; + if (!empty($put['menu_ability_permission'])) { + $menu_ability_permission = json_decode($put['menu_ability_permission'], true); + unset($put['menu_ability_permission']); + } + + $validate = new SysMenuValidate; + $check_data = array_merge($put, ['id' => $id, 'menu_ability_permission' => $menu_ability_permission]); + if (!$validate->check($check_data)) { + return error($validate->getError()); + } + + SysMenuModel::startTrans(); + try { + $menu = SysMenuModel::bypk($id)->find(); + if ($menu->isEmpty()) { + throw new InvalidOperateException('请确认操作对象是否存在'); + } + // 更新菜单 + if (!$menu->save($put)) { + throw new InvalidOperateException('更新菜单失败'); + } + + // 更新菜单能力权限 + $original_ability = SysMenuAbilityPermissionModel::menuId($menu->id)->select(); + if (!$original_ability->isEmpty()) { + if (!$original_ability->delete()) { + throw new InvalidOperateException('删除旧菜单能力权限失败'); + } + } + if (!empty($menu_ability_permission)) { + foreach ($menu_ability_permission as &$item) { + $item['menu_id'] = $menu->id; + } + unset($item); + + $permission = (new SysMenuAbilityPermissionModel)->saveAll($menu_ability_permission); + if (empty($permission)) { + throw new InvalidOperateException('更新菜单能力权限失败'); + } + } + + SysMenuModel::commit(); + } catch (InvalidOperateException $e) { + SysMenuModel::rollback(); + return error($e->getMessage()); + } catch (\Throwable $th) { + SysMenuModel::rollback(); + return error('操作失败'); + } + + return success('操作成功'); + } + + // 导入数据 + public function import() + { + // 获取上传文件 + $file = request()->file('file'); + if (empty($file)) { + return error('请上传文件'); + } + + // 读取文件 + $keys_map = [ + 'A' => 'id', + 'B' => 'pid', + 'C' => 'title', + 'D' => 'name', + 'E' => 'path', + 'F' => 'icon', + 'G' => 'redirect', + 'H' => 'component', + 'I' => 'hidden', + 'J' => 'actived', + 'K' => 'keep_alive', + 'L' => 'sort', + 'M' => 'status', + 'N' => 'menu_ability_permission' + ]; + $xlsx_data = xlsx_reader($file->getRealPath(), 2, $keys_map); + if (empty($xlsx_data)) { + return success('操作成功'); + } + + // 处理行序号,防止后续被打乱 + foreach ($xlsx_data as $row => &$item) { + $item['seq_no'] = $row; + } + unset($item); + + // 数据组装成树形结构,方便处理上下级关系 + $xlsx_data_tree = array_to_tree($xlsx_data, 0, 'pid'); + // 处理导入菜单数据 + $handle_errors = $this->handleImportData($xlsx_data_tree); + if (!empty($handle_errors)) { + return error(implode(PHP_EOL, $handle_errors)); + } + return success('操作成功'); + } + // 处理导入数据 + private function handleImportData($menus) + { + $errors = []; + $menu_model = new SysMenuModel; + $chunks = array_chunk($menus, 100, true); + foreach ($chunks as $chunk) { + // 执行保存 + if (!$this->executeReplaceMenu($chunk)) { + $errors[] = sprintf('第【%s】行保存失败', implode(',', array_column($chunk, 'seq_no'))); + continue; + } + + // 获取该批次菜单,以获取新增情况的菜单的id + $menus_map = $menu_model->whereIn('name', array_column($chunk, 'name'))->column('id', 'name'); + foreach ($chunk as &$it) { + // 更改菜单的子菜单pid值为新值 + if (!empty($it['children'])) { + foreach ($it['children'] as &$child) { + if (isset($menus_map[$child['name']])) { + $child['pid'] = $menus_map[$child['name']]; + } + } + unset($child); + } + + // 更改菜单能力权限的menu_id + if (!empty($it['menu_ability_permission'])) { + $menu_ability_permission = json_decode($it['menu_ability_permission'], true); + foreach ($menu_ability_permission as &$permission) { + if (isset($menus_map[$it['name']])) { + $permission['menu_id'] = $menus_map[$it['name']]; + } + } + unset($permission); + $it['menu_ability_permission'] = $menu_ability_permission; + } + } + unset($it); + + // 处理子菜单 + $childrens = array_reduce(array_column($chunk, 'children'), 'array_merge', []); + $handle_errors = $this->handleImportData($childrens); + if (!empty($handler_ret)) { + $errors = array_merge($errors, $handle_errors); + } + + // 更新菜单能力权限 + $menu_ability_permissions = array_reduce(array_column($chunk, 'menu_ability_permission'), 'array_merge', []); + if (!empty($menu_ability_permissions)) { + // 菜单能力权限模型实例 + $ability_permission_model = new SysMenuAbilityPermissionModel; + // 删除旧的菜单能力权限 + $menus_id = array_column($menu_ability_permissions, 'menu_id'); + $exists = $ability_permission_model->whereIn('menu_id', $menus_id)->count(); + if ($exists) { + $deleted = $ability_permission_model->whereIn('menu_id', $menus_id)->delete(); + if (!$deleted) { + $errors[] = sprintf('第【%s】行删除旧菜单能力权限失败', implode(',', array_column($chunk, 'seq_no'))); + continue; + } + } + // 保存新的菜单能力权限 + $ability_updated = $ability_permission_model->saveAll($menu_ability_permissions); + if ($ability_updated->isEmpty()) { + $errors[] = sprintf('第【%s】行保存新菜单能力权限失败', implode(',', array_column($chunk, 'seq_no'))); + continue; + } + } + } + + return $errors; + } + // 组装repalce语句的values + private function buildReplaceMenuValues($menu) + { + return sprintf( + '(%d, %d, "%s", "%s", "%s", "%s", "%s", "%s", %d, %d, %d, %d, %d)', + $menu['id'], + $menu['pid'], + $menu['title'], + $menu['name'], + $menu['path'], + $menu['icon'], + $menu['redirect'], + $menu['component'], + $menu['hidden'], + $menu['actived'], + $menu['keep_alive'], + $menu['sort'], + $menu['status'] + ); + } + // 执行repalce语句 + private function executeReplaceMenu($menus) + { + // 获取已存在的菜单 + $menus_map = SysMenuModel::whereIn('name', array_column($menus, 'name'))->column('id', 'name'); + + // 组装保存sql语句 + $values = []; + foreach ($menus as &$it) { + if (isset($menus_map[$it['name']])) { + $it['id'] = $menus_map[$it['name']]; + } + if ($it['level'] == 1) { + $it['pid'] = 0; + } + $values[] = $this->buildReplaceMenuValues($it); + } + unset($it); + + return Db::execute( + sprintf( + 'REPLACE INTO %s ( + `id`, + `pid`, + `title`, + `name`, + `path`, + `icon`, + `redirect`, + `component`, + `hidden`, + `actived`, + `keep_alive`, + `sort`, + `status` + ) VALUES %s;', + (new SysMenuModel)->getTable(), + implode(',', $values) + ) + ); + } + + // 导出数据 + public function export() + { + $schema = [ + 'id' => '菜单ID', + 'pid' => '父级ID', + 'title' => '菜单标题', + 'name' => '菜单名称', + 'path' => '菜单路径', + 'icon' => '菜单图标', + 'redirect' => '菜单重定向路径', + 'component' => '菜单组件路径', + 'hidden' => '是否隐藏', + 'actived' => '是否激活', + 'keep_alive' => '是否缓存', + 'sort' => '排序', + 'status' => '状态', + 'menu_ability_permission' => '菜单能力权限' + ]; + + // 获取导出数据 + $data = $this->getExportMenuData(); + + // 导出 + xlsx_writer($data, $schema)->save('php://output'); + } + private function getExportMenuData() + { + $param = request()->param(['title' => '']); + + $menus = SysMenuModel::withoutField([ + 'created_at', + 'updated_at', + 'deleted_at' + ]) + ->with(['menu_ability_permission']) + ->withSearch(['title'], [ + 'title' => $param['title'] ?? null + ]) + ->order(['id' => 'asc']) + ->select() + ->hidden(['menu_ability_permission.menu_id']); + if (!$menus->isEmpty()) { + $menus->each(function ($item) { + $item->menu_ability_permission = json_encode($item->menu_ability_permission); + }); + } + + return $menus; + } + + // 删除数据 + public function delete() + { + $id = request()->param('id'); + + SysMenuModel::startTrans(); + try { + $menu = SysMenuModel::bypk($id)->find(); + if ($menu->isEmpty()) { + throw new InvalidOperateException('请确认操作对象是否存在'); + } + if (!$menu->delete()) { + throw new InvalidOperateException('删除菜单失败'); + } + + SysMenuModel::commit(); + } catch (InvalidOperateException $e) { + SysMenuModel::rollback(); + return error($e->getMessage()); + } catch (\Throwable $th) { + SysMenuModel::rollback(); + return error('操作失败'); + } + + return success('操作成功'); + } +} diff --git a/app/admin/model/v1/SysMenuAbilityPermissionModel.php b/app/admin/model/v1/SysMenuAbilityPermissionModel.php new file mode 100644 index 00000000..b02350b4 --- /dev/null +++ b/app/admin/model/v1/SysMenuAbilityPermissionModel.php @@ -0,0 +1,19 @@ +where('menu_id', $value); + } +} diff --git a/app/admin/model/v1/SysMenuModel.php b/app/admin/model/v1/SysMenuModel.php new file mode 100644 index 00000000..310ce4d4 --- /dev/null +++ b/app/admin/model/v1/SysMenuModel.php @@ -0,0 +1,51 @@ +hasMany(SysMenuAbilityPermissionModel::class, 'menu_id', 'id'); + } + + // 菜单标题查询 + public function searchTitleAttr($query, $value, $data) + { + if (empty($value)) { + return; + } + + $query->where('title', 'like', "%$value%"); + } + + // 状态查询 + public function scopeStatus($query, $value) + { + if (empty($value)) { + return; + } + return $query->where('status', '=', $value); + } + + // 获取启用的菜单 + public function scopeEnabled($query) + { + $query->where('status', '=', 1); + } +} diff --git a/app/admin/route/v1.php b/app/admin/route/v1.php index 0c2e5d12..cf6b97eb 100644 --- a/app/admin/route/v1.php +++ b/app/admin/route/v1.php @@ -377,6 +377,32 @@ Route::group('v1', function () { // 角色删除 Route::delete('delete/:id', 'Role/delete'); }); + + // 菜单管理 + // 菜单列表 + Route::get('menus', 'Menu/list'); + Route::group('menu', function() { + // 菜单分页 + Route::get('index', 'Menu/index'); + + // 菜单详情 + Route::get('read/:id', 'Menu/read'); + + // 菜单新增 + Route::post('save', 'Menu/save'); + + // 菜单更新 + Route::put('update/:id', 'Menu/update'); + + // 菜单导入 + Route::post('import', 'Menu/import'); + + // 菜单导出 + Route::get('export', 'Menu/export'); + + // 菜单删除 + Route::delete('delete/:id', 'Menu/delete'); + }); })->prefix('v1.'); Route::miss(function() { diff --git a/app/admin/validate/v1/SysMenuValidate.php b/app/admin/validate/v1/SysMenuValidate.php new file mode 100644 index 00000000..5feb9214 --- /dev/null +++ b/app/admin/validate/v1/SysMenuValidate.php @@ -0,0 +1,72 @@ + ['规则1','规则2'...] + * + * @var array + */ + protected $rule = [ + 'id' => 'require|integer', + 'pid' => 'integer', + 'title' => 'require|max:64', + 'name' => 'require|unique:sys_menu|max:64', + 'path' => 'require|max:128', + 'icon' => 'max:64', + 'redirect' => 'max:128', + 'component' => 'max:128', + 'hidden' => 'in:0,1', + 'actived' => 'in:0,1', + 'keep_alive' => 'in:0,1', + 'sort' => 'integer', + 'status' => 'in:0,1', + 'menu_ability_permission' => 'array', + 'menu_ability_permission.*.ability_name' => 'max:64', + 'menu_ability_permission.*.permission' => 'max:128', + 'menu_ability_permission.*.sort' => 'integer', + ]; + + /** + * 定义错误信息 + * 格式:'字段名.规则名' => '错误信息' + * + * @var array + */ + protected $message = [ + 'id.require' => '菜单ID不能为空', + 'id.integer' => '菜单ID必须为整数', + 'pid.integer' => '父级菜单ID必须为整数', + 'title.require' => '菜单名称不能为空', + 'title.max' => '菜单名称最多64个字符', + 'name.require' => '菜单Name不能为空', + 'name.unique' => '菜单Name已存在', + 'name.max' => '菜单Name最多64个字符', + 'path.require' => '访问路径不能为空', + 'path.max' => '访问路径最多128个字符', + 'icon.max' => '菜单图标最多64个字符', + 'redirect.max' => '菜单重定向路径最多128个字符', + 'component.max' => '菜单组件路径最多128个字符', + 'hidden.in' => '是否显示必须为0或1', + 'actived.in' => '是否高亮必须为0或1', + 'keep_alive.in' => 'KeepAlive必须为0或1', + 'sort.integer' => '菜单排序必须为整数', + 'status.in' => '是否启用必须为0或1', + 'menu_ability_permission.array' => '菜单能力权限必须为数组', + 'menu_ability_permission.*.ability_name.max' => '菜单能力权限名称最多64个字符', + 'menu_ability_permission.*.permission.max' => '菜单能力权限标志最多128个字符', + 'menu_ability_permission.*.sort.integer' => '菜单能力权限排序必须为整数', + ]; + + // 新增验证场景 + public function sceneCreate() + { + return $this->remove('id', 'require|integer'); + } +} diff --git a/app/common/model/SysMenuAbilityPermissionBaseModel.php b/app/common/model/SysMenuAbilityPermissionBaseModel.php new file mode 100644 index 00000000..b26406ce --- /dev/null +++ b/app/common/model/SysMenuAbilityPermissionBaseModel.php @@ -0,0 +1,24 @@ + 'int', + 'ability_name' => 'string', + 'permission' => 'string', + 'sort' => 'int' + ]; +} diff --git a/app/common/model/SysMenuBaseModel.php b/app/common/model/SysMenuBaseModel.php new file mode 100644 index 00000000..91f4aeaf --- /dev/null +++ b/app/common/model/SysMenuBaseModel.php @@ -0,0 +1,37 @@ + 'int', + 'pid' => 'int', + 'title' => 'string', + 'name' => 'string', + 'path' => 'string', + 'icon' => 'string', + 'redirect' => 'string', + 'component' => 'string', + 'hidden' => 'int', + 'actived' => 'int', + 'keep_alive' => 'int', + 'sort' => 'int', + 'status' => 'int', + 'created_at' => 'int', + 'updated_at' => 'int', + 'deleted_at' => 'int', + ]; +} diff --git a/database/migrations/20241228093758_create_sys_menu.php b/database/migrations/20241228093758_create_sys_menu.php index e3ee9674..5de5c0e1 100644 --- a/database/migrations/20241228093758_create_sys_menu.php +++ b/database/migrations/20241228093758_create_sys_menu.php @@ -42,6 +42,8 @@ class CreateSysMenu extends Migrator ->addColumn('status', 'boolean', ['null' => false, 'default' => 1, 'comment' => '-1为禁用, 1为启用']) ->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间']) ->addColumn('updated_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'comment' => '更新时间']) + ->addColumn('deleted_at', 'timestamp', ['null' => true, 'comment' => '删除时间']) + ->addIndex(['unique_name'], ['unique' => true]) ->create(); } }