From 55bd2fab90614b180dd7e96f26cb8175b2b51a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=9C=E4=BA=91?= Date: Wed, 16 Apr 2025 15:34:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96hyperf-admin?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=81=E6=9C=8D=E5=8A=A1=E5=92=8C=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交初始化了hyperf-admin项目,主要包括以下内容: - 添加了基础模型如User、Role、Menu等 - 实现了权限、日志、配置等核心服务 - 添加了系统管理、用户管理、日志管理等控制器 - 配置了路由和中间件 - 添加了.gitignore和README.md文件 --- .gitignore | 3 + README.md | 3 + composer.json | 31 ++ src/ConfigProvider.php | 36 ++ src/Controller/AdminAbstractController.php | 83 +++ src/Controller/CommonConfigController.php | 126 +++++ src/Controller/LogController.php | 147 ++++++ src/Controller/MenuController.php | 581 +++++++++++++++++++++ src/Controller/RoleController.php | 198 +++++++ src/Controller/SystemController.php | 131 +++++ src/Controller/UploadController.php | 62 +++ src/Controller/UserController.php | 325 ++++++++++++ src/Crontab/ExportTask.php | 33 ++ src/Install/InstallCommand.php | 29 + src/Install/UpdateCommand.php | 44 ++ src/Install/install.sql | 142 +++++ src/Middleware/AuthMiddleware.php | 54 ++ src/Middleware/PermissionMiddleware.php | 166 ++++++ src/Model/CommonConfig.php | 48 ++ src/Model/ExportTasks.php | 54 ++ src/Model/FrontRoutes.php | 104 ++++ src/Model/GlobalConfig.php | 96 ++++ src/Model/OperatorLog.php | 23 + src/Model/RequestLog.php | 43 ++ src/Model/Role.php | 49 ++ src/Model/RoleMenu.php | 28 + src/Model/User.php | 90 ++++ src/Model/UserRole.php | 29 + src/Model/Version.php | 76 +++ src/Model/Versionable.php | 195 +++++++ src/Service/AuthService.php | 72 +++ src/Service/CommonConfig.php | 49 ++ src/Service/ExportService.php | 198 +++++++ src/Service/GlobalConfig.php | 67 +++ src/Service/Menu.php | 95 ++++ src/Service/ModuleProxy.php | 53 ++ src/Service/OperatorLogService.php | 92 ++++ src/Service/PermissionService.php | 392 ++++++++++++++ src/Service/UserService.php | 79 +++ src/config/config.php | 59 +++ src/config/routes.php | 47 ++ src/funcs/common.php | 34 ++ 42 files changed, 4266 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 src/ConfigProvider.php create mode 100644 src/Controller/AdminAbstractController.php create mode 100644 src/Controller/CommonConfigController.php create mode 100644 src/Controller/LogController.php create mode 100644 src/Controller/MenuController.php create mode 100644 src/Controller/RoleController.php create mode 100644 src/Controller/SystemController.php create mode 100644 src/Controller/UploadController.php create mode 100644 src/Controller/UserController.php create mode 100644 src/Crontab/ExportTask.php create mode 100644 src/Install/InstallCommand.php create mode 100644 src/Install/UpdateCommand.php create mode 100644 src/Install/install.sql create mode 100644 src/Middleware/AuthMiddleware.php create mode 100644 src/Middleware/PermissionMiddleware.php create mode 100644 src/Model/CommonConfig.php create mode 100644 src/Model/ExportTasks.php create mode 100644 src/Model/FrontRoutes.php create mode 100644 src/Model/GlobalConfig.php create mode 100644 src/Model/OperatorLog.php create mode 100644 src/Model/RequestLog.php create mode 100644 src/Model/Role.php create mode 100644 src/Model/RoleMenu.php create mode 100644 src/Model/User.php create mode 100644 src/Model/UserRole.php create mode 100644 src/Model/Version.php create mode 100644 src/Model/Versionable.php create mode 100644 src/Service/AuthService.php create mode 100644 src/Service/CommonConfig.php create mode 100644 src/Service/ExportService.php create mode 100644 src/Service/GlobalConfig.php create mode 100644 src/Service/Menu.php create mode 100644 src/Service/ModuleProxy.php create mode 100644 src/Service/OperatorLogService.php create mode 100644 src/Service/PermissionService.php create mode 100644 src/Service/UserService.php create mode 100644 src/config/config.php create mode 100644 src/config/routes.php create mode 100644 src/funcs/common.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82cfc4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +composer.lock +vendor diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca7ee22 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## hyperf-admin 的分包 + +[文档地址](https://hyperf-admin.github.io/hyperf-admin/) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c110c96 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "hyperf-admin/admin", + "type": "project", + "license": "MIT", + "authors": [ + { + "name": "daodao97", + "email": "daodao97@foxmail.com" + } + ], + "require": { + "hyperf-admin/base-utils": "dev-master", + "hyperf-admin/validation": "dev-master" + }, + "autoload": { + "psr-4": { + "HyperfAdmin\\Admin\\": "./src" + }, + "files": [ + "src/funcs/common.php" + ] + }, + "config": { + "sort-packages": false + }, + "extra": { + "hyperf": { + "config": "HyperfAdmin\\Admin\\ConfigProvider" + } + } +} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php new file mode 100644 index 0000000..22d268f --- /dev/null +++ b/src/ConfigProvider.php @@ -0,0 +1,36 @@ + [ + InstallCommand::class, + UpdateCommand::class, + ], + 'dependencies' => [], + 'listeners' => [], + 'publish' => [], + 'middlewares' => [ + 'http' => [ + CorsMiddleware::class, + AuthMiddleware::class, + PermissionMiddleware::class, + HttpLogMiddleware::class + ] + ] + ]); + } +} diff --git a/src/Controller/AdminAbstractController.php b/src/Controller/AdminAbstractController.php new file mode 100644 index 0000000..e613a19 --- /dev/null +++ b/src/Controller/AdminAbstractController.php @@ -0,0 +1,83 @@ +auth_service = make(AuthService::class); + $this->permission_service = make(PermissionService::class); + + parent::__construct($container, $request, $response); + } + + public function getRecordHistory() + { + $history_versions = $this->getEntity()->lastVersion($this->previous_version_number); + $history_versions = array_node_append($history_versions, 'user_id', 'username', function ($uids) { + $ret = User::query()->select(['id', 'username'])->whereIn('id', $uids)->get(); + if (!$ret) { + return []; + } + $ret = $ret->toArray(); + array_change_v2k($ret, 'id'); + foreach ($ret as &$item) { + $item = $item['username']; + unset($item); + } + return $ret; + }); + return $history_versions; + } + + /** + * 新版本检查 + * + * @param int $id + * @param int $last_ver_id + * + * @return array + */ + public function newVersion(int $id, int $last_ver_id) + { + $last = $this->getEntity()->lastVersion(); + if (!$last || $last->id == $last_ver_id) { + return $this->success(['has_new_version' => false]); + } + if ($last->user_id == auth()->get('id')) { + return $this->success(['has_new_version' => false]); + } + $user = User::query()->find($last->user_id); + return $this->success([ + 'has_new_version' => true, + 'message' => sprintf("%s在%s保存了新的数据, 请刷新页面获取最新数据", $user->username, $last->created_at), + ]); + } + + public function userId() + { + return auth()->get('id'); + } +} diff --git a/src/Controller/CommonConfigController.php b/src/Controller/CommonConfigController.php new file mode 100644 index 0000000..9762dd7 --- /dev/null +++ b/src/Controller/CommonConfigController.php @@ -0,0 +1,126 @@ + ['name%'], + 'form' => [ + 'id|#' => '', + 'namespace|命名空间' => [ + 'rule' => 'required', + 'type' => 'select', + 'options' => function ($field, $data) { + $namespaces = $this->getModel()->where(['name' => 'namespace'])->value('value'); + $options = []; + foreach ($namespaces as $value => $label) { + $options[] = [ + 'value' => $value, + 'label' => $label, + ]; + } + + return $options; + }, + 'default' => 'common', + ], + 'name|名称' => [ + 'rule' => 'required|unique:hyperf_admin.common_config,name', + 'readonly' => true, + ], + 'title|可读名称' => [ + 'rule' => 'required', + ], + 'rules|规则' => [ + 'type' => 'json', + 'rule' => 'json', + 'depend' => [ + 'field' => 'is_need_form', + 'value' => CommonConfig::NEED_FORM_YES, + ], + ], + 'remark|备注' => 'max:100', + 'is_need_form|是否使用表单' => [ + 'rule' => 'integer', + 'type' => 'radio', + 'options' => CommonConfig::$need_form, + 'default' => CommonConfig::NEED_FORM_YES, + ], + 'value|配置值' => [ + 'type' => 'json', + 'rule' => 'json', + 'depend' => [ + 'field' => 'is_need_form', + 'value' => CommonConfig::NEED_FORM_NO, + ], + ], + ], + 'table' => [ + 'columns' => [ + 'id', + [ + 'field' => 'namespace', + 'enum' => [ + 'default' => '通用', + ], + ], + 'name', + 'title', + [ + 'field' => 'is_need_form', + 'hidden' => true, + ], + ], + 'rowActions' => [ + ['action' => '/cconf/{id}', 'text' => '编辑'], + [ + 'action' => '/cconf/cconf_{name}', + 'text' => '表单', + 'when' => [ + ['is_need_form', '=', CommonConfig::NEED_FORM_YES], + ], + ], + ], + ], + ]; + } + + public function detail($key) + { + $conf = CommonConfigService::getConfigByName($key); + if (!$conf || !$conf['rules']) { + return $this->fail(ErrorCode::CODE_ERR_PARAM, '通用配置未找到 ' . $key); + } + $rules = $this->formOptionsConvert($conf['rules'], true, false, false, $conf['value']); + $compute_map = $this->formComputeConfig($rules); + + return $this->success([ + 'form' => $rules, + 'compute_map' => (object)$compute_map, + ]); + } + + public function saveDetail($key) + { + $conf = CommonConfig::query()->where(['name' => $key])->select([ + 'id', + 'rules', + ])->first(); + if (!$conf) { + return $this->fail(ErrorCode::CODE_ERR_PARAM, '参数错误'); + } + + $saved = $conf->fill(['value' => $this->request->all()])->save(); + + return $saved ? $this->success() : $this->fail(ErrorCode::CODE_ERR_SYSTEM); + } +} diff --git a/src/Controller/LogController.php b/src/Controller/LogController.php new file mode 100644 index 0000000..ab2283a --- /dev/null +++ b/src/Controller/LogController.php @@ -0,0 +1,147 @@ + true, + 'createAble' => false, + 'order_by' => 'id desc', + 'filter' => [ + 'page_url', + 'page_name', + 'action', + 'operator_id', + 'relation_ids', + 'client_ip', + 'detail_json', + 'created_at', + ], + 'formUI' => [ + 'form' => [ + 'size' => 'mini', + ], + ], + 'form' => [ + 'id|#' => 'required|int', + 'page_url|页面URL' => 'required|string', + 'page_name|页面名称' => 'required|string', + 'action|动作' => 'required|string', + 'relation_ids|处理ID' => 'required|string', + 'client_ip|客户端IP' => 'required|string', + 'operator_id|操作人' => [ + 'rule' => 'required|int', + 'type' => 'select', + 'props' => [ + 'allowCreate' => true, + 'filterable' => true, + 'remote' => true, + 'selectApi' => '/search/user', + ], + ], + 'detail_json|其他内容' => 'string', + 'created_at|记录时间' => [ + 'type' => 'date_range', + ], + ], + 'table' => [ + 'columns' => [ + [ + 'field' => 'operator_id', + 'hidden' => true, + ], + ['field' => 'id', 'title' => 'ID', 'hidden' => true], + [ + 'field' => 'created_at', + 'title' => '记录时间', + 'width' => '150px', + ], + [ + 'field' => 'nickname', + 'title' => '操作人', + ], + [ + 'field' => 'page_url', + 'title' => '页面URL', + 'width' => '150px', + ], + [ + 'field' => 'page_name', + 'title' => '页面名称', + 'width' => '220px', + ], + 'action', + [ + 'field' => 'relation_ids', + 'title' => '处理ID', + 'width' => '150px', + ], + [ + 'field' => 'detail_json', + 'hidden' => true, + ], + [ + 'field' => 'description', + 'title' => '描述', + 'virtual_field' => true, + 'width' => '480px', + 'render' => function ($field, $row) { + if (!is_array($row['detail_json'])) { + $row['detail_json'] = json_decode($row['detail_json'], true); + } + + return $row['detail_json']['description'] ?? ''; + }, + ], + [ + 'field' => 'client_ip', + 'title' => '客户端IP', + 'width' => '100px', + ], + [ + 'field' => 'remark', + 'title' => '备注', + 'virtual_field' => true, + 'width' => '200px', + 'render' => function ($field, $row) { + if (!is_array($row['detail_json'])) { + $row['detail_json'] = json_decode($row['detail_json'], true); + } + + return $row['detail_json']['remark'] ?? ''; + }, + ], + ], + ], + ]; + } + + public function beforeListQuery(&$filters) + { + foreach ($filters as $field => $filter) { + if (in_array($field, [ + 'page_url', + 'page_name', + 'action', + 'detail_json', + 'relation_ids', + 'client_ip', + ])) { + $filters[$field] = ['like' => "%$filter%"]; + } + } + if (!empty($filters['created_at']['between'])) { + $filters['created_at']['between'][0] = Carbon::parse($filters['created_at']['between'][0])->toDateTimeString(); + $filters['created_at']['between'][1] = Carbon::parse($filters['created_at']['between'][1] . ' +1 day last second') + ->toDateTimeString(); + } + unset($filters); + } +} diff --git a/src/Controller/MenuController.php b/src/Controller/MenuController.php new file mode 100644 index 0000000..c5e3291 --- /dev/null +++ b/src/Controller/MenuController.php @@ -0,0 +1,581 @@ + false, + 'createAble' => false, + 'where' => [ + 'pid' => 0, + ], + 'formUI' => [ + 'form' => [ + 'size' => 'mini', + ], + ], + 'form' => [ + 'id|#' => 'int', + 'module|#' => [ + 'type' => 'hidden', + 'render' => function ($field, &$data) { + $data['value'] = request()->input('module', $data['value'] ?? 'system'); + }, + ], + 'type|菜单类型' => [ + 'type' => 'radio', + 'default' => 1, + 'options' => ['目录', '菜单', '权限'], + 'compute' => [ + [ + 'when' => ['in', [0, 2]], + 'set' => [ + 'is_scaffold' => [ + 'type' => 'hidden', + ], + 'other_menu' => [ + 'type' => 'hidden', + ], + 'path' => [ + 'type' => 'hidden', + ], + 'page_type' => [ + 'type' => 'hidden', + ], + ], + ], + [ + 'when' => ['=', 1], + 'set' => [ + 'path' => [ + 'rule' => 'requir渲染方式ed', + ], + 'label' => [ + 'title' => '菜单标题', + 'col' => [ + 'span' => 12, + ], + ], + ], + ], + [ + 'when' => ['=', 2], + 'set' => [ + 'icon' => [ + 'type' => 'hidden', + ], + 'is_menu' => [ + 'type' => 'hidden', + ], + 'label' => [ + 'title' => '权限名称', + 'col' => [ + 'span' => 24, + ], + ], + ], + ], + [ + 'when' => ['=', 0], + 'set' => [ + 'permission' => [ + 'type' => 'hidden', + ], + 'label' => [ + 'title' => '菜单标题', + 'col' => [ + 'span' => 12, + ], + ], + ], + ], + ], + ], + 'icon|菜单图标' => [ + 'type' => 'icon-select', + 'options' => [ + 'example' => 'example', + ], + 'col' => [ + 'span' => 12, + ], + ], + 'sort|菜单排序' => [ + 'type' => 'number', + 'default' => 99, + 'col' => [ + 'span' => 12, + ], + ], + 'label|菜单标题' => [ + 'rule' => 'required|string|max:10', + 'col' => [ + 'span' => 12, + ], + ], + 'path|路由地址' => [ + 'rule' => 'string|max:100', + 'default' => '', + 'col' => [ + 'span' => 12, + ], + ], + 'is_menu|菜单可见' => [ + 'type' => 'radio', + 'options' => [ + 0 => '否', + 1 => '是', + ], + 'default' => 1, + 'col' => [ + 'span' => 12, + ], + ], + 'is_scaffold|渲染方式' => [ + 'type' => 'radio', + 'options' => [ + 1 => '脚手架', + 0 => '自定义', + 2 => '配置化脚手架', + ], + 'default' => 1, + 'col' => [ + 'span' => 12, + ], + 'compute' => [ + 'when' => ['=', 0], + 'set' => [ + 'view' => [ + 'rule' => 'required', + ], + ], + ], + ], + 'config|脚手架配置' => [ + "type" => 'json', + 'depend' => [ + 'field' => 'is_scaffold', + 'value' => 2 + ] + ], + 'view|组件路径' => [ + 'rule' => 'string|max:50', + 'default' => '', + 'depend' => [ + 'field' => 'is_scaffold', + 'value' => 0, + ], + ], + 'scaffold_action|预置权限' => [ + 'type' => 'checkbox', + 'virtual_field' => true, + 'options' => function ($field, $data) { + $scaffold_permissions = config('scaffold_permissions'); + $options = []; + foreach ($scaffold_permissions as $key => $permission) { + $options[] = [ + 'value' => $key, + 'label' => $permission['label'], + ]; + } + + return $options; + }, + 'info' => '新增和编辑会创建/form或/:id的前端路由', + 'depend' => [ + 'field' => 'is_scaffold', + 'value' => 1, + ], + ], + 'permission|权限标识' => [ + 'type' => 'select', + 'default' => '', + 'props' => [ + 'multiple' => true, + 'selectApi' => '/system/routes?module={module}' + ], + ], + 'pid|上级类目' => [ + 'rule' => 'array', + 'type' => 'cascader', + 'default' => [], + 'options' => function ($field, $data) { + $module = request()->input('module', $data['module'] ?? 'system'); + + return (new Menu())->tree([ + 'module' => $module, + 'type' => [0, 1], + ]); + }, + 'props' => [ + 'style' => 'width: 100%;', + 'clearable' => true, + 'props' => [ + 'checkStrictly' => true, + ], + ], + ], + 'roles|分配角色' => [ + 'rule' => 'array', + 'type' => 'cascader', + 'virtual_field' => true, + 'props' => [ + 'style' => 'width: 100%;', + 'props' => [ + 'multiple' => true, + 'leaf' => 'leaf', + 'emitPath' => false, + 'checkStrictly' => true, + ], + ], + 'render' => function ($field, &$data) { + $id = (int)$this->request->route('id', 0); + $data['value'] = $this->permission_service->getMenuRoleIds($id); + $data['options'] = $this->permission_service->getRoleTree(); + }, + ], + ], + 'table' => [ + 'is_tree' => true, + 'tabs' => function() { + $conf = \HyperfAdmin\Admin\Service\CommonConfig::getValByName('website_config'); + $system_module = $conf['system_module'] ?? []; + return array_map(function ($item) { + return [ + 'label' => $item['label'], + 'value' => $item['name'], + 'icon' => $item['icon'] + ]; + }, $system_module); + }, + 'rowActions' => [ + [ + 'text' => '编辑', + 'type' => 'form', + 'target' => '/menu/{id}', + 'formUi' => [ + 'form' => [ + 'labelWidth' => '80px', + 'size' => 'mini', + ], + ], + 'props' => [ + 'type' => 'primary', + ], + ], + [ + 'text' => '加子菜单', + 'type' => 'form', + 'target' => '/menu/form?pid[]={id}&module={module}', + 'formUi' => [ + 'form' => [ + 'labelWidth' => '80px', + 'size' => 'mini', + ], + ], + 'props' => [ + 'type' => 'success', + ], + ], + [ + 'text' => '删除', + 'type' => 'api', + 'target' => '/menu/delete', + 'props' => [ + 'type' => 'danger', + ], + ], + ], + 'topActions' => [ + [ + 'text' => '清除权限缓存', + 'type' => 'api', + 'target' => '/menu/permission/clear', + 'props' => [ + 'icon' => 'el-icon-delete', + 'type' => 'warning', + ], + ], + [ + 'text' => '公共资源', + 'type' => 'jump', + 'target' => '/cconf/cconf_permissions', + 'props' => [ + 'icon' => 'el-icon-setting', + 'type' => 'primary', + ], + ], + [ + 'text' => '新建', + 'type' => 'form', + 'target' => '/menu/form?module={tab_id}', + 'formUi' => [ + 'form' => [ + 'labelWidth' => '80px', + 'size' => 'mini', + ], + ], + 'props' => [ + 'icon' => 'el-icon-plus', + 'type' => 'success', + ], + ], + ], + 'columns' => [ + ['field' => 'id', 'hidden' => true], + ['field' => 'pid', 'hidden' => true], + ['field' => 'module', 'hidden' => true], + [ + 'field' => 'label', + 'width' => '250px', + ], + [ + 'field' => 'is_menu', + 'enum' => [ + 0 => 'info', + 1 => 'success', + ], + 'width' => '80px;', + ], + [ + 'field' => 'icon', + 'type' => 'icon', + 'width' => '80px;', + ], + 'path', + 'permission', + [ + 'field' => 'sort', + 'edit' => true, + 'width' => '170px;', + ], + ], + ], + 'order_by' => 'sort desc', + ]; + } + + protected function beforeFormResponse($id, &$record) + { + if (in_array($record['type'], [ + 1, + 2, + ]) + && !empty($record['permission'])) { + $record['permission'] = array_map(function ($item) use ($record) { + if (!Str::contains($item, '::')) { + $http_method = FrontRoutes::$http_methods[$record['http_method']]; + + return "{$http_method}::{$item}"; + } + + return $item; + }, array_filter(explode(',', $record['permission']))); + } + $scaffold_action = json_decode($record['scaffold_action'], true); + $record['scaffold_action'] = $scaffold_action ? array_keys($scaffold_action) : []; + $record['pid'] = (new Menu())->getPathNodeIds($id); + } + + protected function beforeSave($id, &$data) + { + if ($data['type'] == 1) { + if ($data['path'] == '#' || $data['path'] == '') { + $this->exception('菜单路由地址不能为空或"#"', ErrorCode::CODE_ERR_PARAM); + } + $paths = array_filter(explode('/', $data['path'])); + if (count($paths) > 5) { + $this->exception('路由地址层级过深>5,请设置精简一些', ErrorCode::CODE_ERR_PARAM); + } + } else { + $data['path'] = '#'; + } + $data['is_menu'] = $data['type'] == 2 ? 0 : $data['is_menu']; + if ($data['permission']) { + $data['permission'] = implode(',', $data['permission'] ?? []); + } + $pid = array_pop($data['pid']); + if ($pid == $id) { + $pid = array_pop($data['pid']); + } + $data['pid'] = (int)$pid; + if ($data['type'] > 1) { + $parent_info = $this->getModel()->find($data['pid']); + if (!$parent_info || $parent_info['type'] != 1) { + $this->exception('菜单类型为权限时请选择一个上级类目', ErrorCode::CODE_ERR_PARAM); + } + } + $data['status'] = YES; + } + + protected function afterSave($pk_val, $data, $entity) + { + // 更新预置的脚手架权限 + $scaffold_action = json_decode($entity->scaffold_action, true); + $action_keys = $scaffold_action ? array_keys($scaffold_action) : []; + $need_del_ids = $scaffold_action ? array_values($scaffold_action) : []; + $router_ids = []; + if (!empty($data['scaffold_action'])) { + $need_del_ids = collect($scaffold_action)->except($data['scaffold_action'])->values()->toArray(); + $scaffold_action = collect($scaffold_action)->only($data['scaffold_action'])->toArray(); + $paths = array_filter(explode('/', $data['path'])); + array_pop($paths); + $prefix = implode('/', $paths); + foreach ($data['scaffold_action'] as $k => $action) { + if (in_array($action, $action_keys)) { + continue; + } + $action_conf = config("scaffold_permissions.{$action}"); + $menu = [ + 'pid' => $pk_val, + 'label' => $action_conf['label'], + 'path' => !empty($action_conf['path']) ? "/{$prefix}" . $action_conf['path'] : '', + 'permission' => str_replace('/*/', "/{$prefix}/", $action_conf['permission']), + 'is_scaffold' => $action_conf['type'] == 1 ? 1 : 0, + 'module' => $data['module'], + 'type' => $action_conf['type'], + 'sort' => 99 - $k, + 'status' => 1, + ]; + $model = make(FrontRoutes::class); + $model->fill($menu)->save(); + $scaffold_action[$action] = $model->id; + $router_ids[] = $model->id; + } + } else { + $scaffold_action = ''; + } + $entity->scaffold_action = json_encode($scaffold_action); + // todo entity + //$entity->save(); + // 删除路由 + if (!empty($need_del_ids)) { + $this->getModel()->destroy($need_del_ids); + make(RoleMenu::class)->where2query(['router_id' => $need_del_ids])->delete(); + } + // 分配角色 + if (!empty($data['roles'])) { + $role_menus = []; + $router_ids[] = $pk_val; + foreach ($data['roles'] as $role_id) { + foreach ($router_ids as $router_id) { + $role_menus[] = [ + 'router_id' => $router_id, + 'role_id' => $role_id, + ]; + } + } + make(RoleMenu::class)->insertOnDuplicateKey($role_menus); + } else { + // 删除当前菜单已分配的角色 + make(RoleMenu::class)->where2query(['router_id' => $pk_val])->delete(); + } + // 清除缓存 + $this->permission_service->getPermissionCacheKey(0, true); + } + + protected function afterDelete($pk_val, $deleted) + { + if ($deleted) { + // 删除子菜单 + $sub_ids = $this->getModel()->where2query(['pid' => $pk_val])->select(['id'])->get()->toArray(); + if ($sub_ids) { + $sub_ids = array_column($sub_ids, 'id'); + $this->afterDelete($sub_ids, $deleted); + } + if (is_array($pk_val)) { + make(FrontRoutes::class)->where2query(['id' => $pk_val])->delete(); + } + make(RoleMenu::class)->where2query(['router_id' => $pk_val])->delete(); + } + } + + public function clearPermissionCache() + { + $this->permission_service->getPermissionCacheKey(0, true); + + return $this->success(); + } + + public function getOpenApis() + { + $field = $this->request->input('field', 'open_api'); + $conf = CommonConfig::query()->where([ + 'namespace' => 'system', + 'name' => 'permissions', + ])->value('value')[$field] ?? []; + $router = container(DispatcherFactory::class)->getRouter('http'); + $data = $router->getData(); + $options = []; + foreach ($data as $routes_data) { + foreach ($routes_data as $http_method => $routes) { + $route_list = []; + if (isset($routes[0]['routeMap'])) { + foreach ($routes as $map) { + array_push($route_list, ...$map['routeMap']); + } + } else { + $route_list = $routes; + } + foreach ($route_list as $route => $v) { + $route = is_string($route) ? rtrim($route) : rtrim($v[0]->route); + $route_key = "$http_method::{$route}"; + if (in_array($route_key, $conf)) { + continue; + } + // 过滤掉脚手架页面配置方法 + $callback = is_array($v) ? ($v[0]->callback) : $v->callback; + if (!is_array($callback)) { + continue; + } + [$controller, $action] = $callback; + if (empty($action) || in_array($action, $this->permission_service->scaffold_actions)) { + continue; + } + $options[] = [ + 'id' => $route_key, + 'controller' => $controller, + 'action' => $action, + 'http_method' => $http_method, + ]; + } + } + } + $right_options = []; + foreach ($conf as $route) { + [$http_method, $uri] = explode("::", $route, 2); + $dispatcher = container(DispatcherFactory::class)->getDispatcher('http'); + $route_info = $dispatcher->dispatch($http_method, $uri); + if (!empty($route_info[1]->callback[0])) { + $right_options[] = [ + 'id' => $route, + 'controller' => $route_info[1]->callback[0], + 'action' => $route_info[1]->callback[1], + 'http_method' => $http_method, + ]; + } + } + + return $this->success([ + 'left' => $options, + 'right' => $right_options, + ]); + } + + public function beforeListQuery(&$where) + { + $where['module'] = $this->request->input('tab_id', 'default'); + } +} diff --git a/src/Controller/RoleController.php b/src/Controller/RoleController.php new file mode 100644 index 0000000..6d63802 --- /dev/null +++ b/src/Controller/RoleController.php @@ -0,0 +1,198 @@ + true, + 'deleteAble' => true, + 'importAble' => true, + 'filter' => ['name'], + 'where' => [ + 'pid' => 0, + ], + 'form' => [ + 'id' => 'int', + 'name|名称' => [ + 'rule' => 'required|max:20', + 'type' => 'input', + 'props' => [ + 'size' => 'small', + 'maxlength' => 20, + ], + ], + 'pid|上级角色' => [ + 'rule' => 'int', + 'type' => 'select', + 'info' => '没有上级角色则为一级角色', + 'default' => 0, + 'props' => [ + 'multipleLimit' => 1, + ], + 'options' => function ($field, &$data) { + $options = $this->permission_service->getAllRoleList(['pid' => 0], [ + 'id as value', + 'name as label', + ]); + array_unshift($options, ['value' => 0, 'label' => '无']); + + return $options; + }, + ], + 'sort|排序' => [ + 'rule' => 'int', + 'type' => 'number', + 'default' => 0, + ], + 'permissions|权限设置' => [ + 'rule' => 'Array', + 'type' => 'el-cascader-panel', + 'virtual_field' => true, + 'props' => [ + 'style' => 'height:500px;', + 'props' => [ + 'multiple' => true, + 'leaf' => 'leaf', + 'checkStrictly' => false, + ], + ], + 'render' => function ($field, &$data) { + $id = (int)$this->request->route('id', 0); + [ + $data['value'], + $data['props']['options'], + ] = $this->permission_service->getPermissionOptions($id); + }, + ], + 'user_ids|授权用户' => [ + 'type' => 'select', + 'props' => [ + 'multiple' => true, + 'selectApi' => '/user/act', + 'remote' => true, + ], + 'virtual_field' => true, + 'render' => function ($field, &$data) { + $id = (int)$this->request->route('id', 0); + $data['value'] = $this->permission_service->getRoleUserIds($id); + if (!empty($data['value'])) { + $data['options'] = select_options($data['props']['selectApi'], $data['value']); + } + }, + ], + ], + 'table' => [ + 'columns' => [ + ['field' => 'id', 'hidden' => true], + ['field' => 'pid', 'hidden' => true], + 'name', + [ + 'field' => 'sort', + 'edit' => true, + ], + ], + 'rowActions' => [ + [ + 'action' => '/role/{id}', + 'text' => '编辑', + ], + [ + 'action' => 'api', + 'api' => '/role/delete', + 'text' => '删除', + 'type' => 'danger', + ], + ], + ], + 'order_by' => 'pid asc, sort desc', + ]; + } + + protected function beforeListResponse(&$list) + { + $ids = array_column($list, 'id'); + $children = $this->getModel()->where2query(['pid' => $ids])->get()->toArray(); + $list = array_merge($list, $children); + $list = generate_tree($list); + } + + protected function beforeSave($pk_val, &$data) + { + $data['permissions'] = $data['permissions'] ? json_encode($data['permissions']) : ''; + unset($data); + } + + protected function afterSave($pk_val, $data) + { + $data['permissions'] = json_decode($data['permissions'], true); + if (empty($data['permissions'])) { + return true; + } + // 1、删除角色拥有的菜单 + $id = $data['id'] ?? 0; + if ((int)$id == $pk_val) { + // 删除角色关联的菜单 + RoleMenu::where('role_id', $pk_val)->delete(); + // 删除角色关联的用户 + $user_ids = $this->permission_service->getRoleUserIds($pk_val); + if (!empty($user_ids)) { + make(UserRole::class)->where2query([ + 'role_id' => $pk_val, + 'user_id' => array_values(array_diff($user_ids, $data['user_ids'] ?? [])), + ])->delete(); + } + // 清除缓存 + $this->permission_service->getPermissionCacheKey(0, true); + } + $menu_ids = []; + foreach ($data['permissions'] as $permissions) { + unset($permissions[0]); + $menu_ids = array_merge($menu_ids, $permissions); + } + // 2、保存角色新分配的菜单 + $role_menus = []; + $menu_ids = array_unique($menu_ids); + foreach ($menu_ids as $menu_id) { + $role_menus[] = [ + 'role_id' => $pk_val, + 'router_id' => (int)$menu_id, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]; + } + if (!empty($role_menus)) { + RoleMenu::insertOnDuplicateKey($role_menus, [ + 'role_id', + 'router_id', + ]); + } + // 3、保存角色关联的用户 + if (!empty($data['user_ids'])) { + $user_role_ids = []; + foreach ($data['user_ids'] as $user_id) { + $user_role_ids[] = [ + 'role_id' => $pk_val, + 'user_id' => (int)$user_id, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]; + } + if (!empty($user_role_ids)) { + UserRole::insertOnDuplicateKey($user_role_ids, [ + 'role_id', + 'user_id', + ]); + } + } + + return true; + } +} diff --git a/src/Controller/SystemController.php b/src/Controller/SystemController.php new file mode 100644 index 0000000..8b8a9b5 --- /dev/null +++ b/src/Controller/SystemController.php @@ -0,0 +1,131 @@ +success([ + 'state' => $swoole_server->stats(), + ]); + } + + public function config() + { + $config = CommonConfig::getValue('system', 'website_config', [ + 'open_export' => false, + 'navbar_notice' => '', + ]); + + if (isset($config['system_module']) && !$this->auth_service->isSupperAdmin()) { + $user_id = $this->auth_service->get('id'); + $modules = $this->permission_service->getModules($user_id); + $config['system_module'] = array_filter($config['system_module'], function ($item) use ($modules) { + return in_array($item['name'], $modules); + }); + } + + return $this->success($config); + } + + public function routes() + { + $module_proxy = make(ModuleProxy::class); + if ($module_proxy->needProxy()) { + return $this->success($module_proxy->request()['payload']); + } + + $kw = $this->request->input('kw', ''); + $routes = $this->permission_service->getSystemRouteOptions(); + $routes = array_filter($routes, function ($item) use ($kw) { + return Str::contains($item['value'], $kw); + }); + return $this->success(array_values($routes)); + } + + public function listInfo(int $id) + { + $config = FrontRoutes::query()->find($id)->getAttributeValue("config"); + $this->options = $config; + return $this->info(); + } + + public function listDetail(int $id) + { + $config = FrontRoutes::query()->find($id)->getAttributeValue("config"); + $listApi = $config['listApi'] ?? ''; + if (!$listApi) { + return $this->fail(ErrorCode::CODE_ERR_SYSTEM, '脚手架配置错误, 缺少列表接口'); + } + try { + return Guzzle::proxy($listApi, $this->request); + } catch (\Exception $e) { + return $this->fail(ErrorCode::CODE_ERR_SYSTEM, '外部接口转发失败 ' . $e->getMessage()); + } + } + + public function formInfo($route_id, $id) + { + $config = FrontRoutes::query()->find($route_id)->getAttributeValue("config"); + try { + $this->options = $config; + $form = $this->form(); + if ($id) { + // todo token or aksk + $getApi = str_var_replace($config['getApi'] ?? '', ['id' => $id]); + $result = Guzzle::proxy($getApi, $this->request); + if ($result['code'] !== 0) { + return $this->fail(ErrorCode::CODE_ERR_SYSTEM, '外部接口转发失败 ' . $result['message'] ?? ''); + } + foreach ($form['payload']['form'] as &$item) { + $item['value'] = $result['payload'][$item['field']] ?? null; + unset($item); + } + } + return $form; + } catch (\Exception $e) { + return $this->fail(ErrorCode::CODE_ERR_SYSTEM, '外部接口转发失败 ' . $e->getMessage()); + } + } + + public function formSave($route_id, $id) + { + $config = FrontRoutes::query()->find($route_id)->getAttributeValue("config"); + $saveApi = str_var_replace($config['saveApi'] ?? '', ['id' => $id]); + if (!$saveApi) { + return $this->fail(ErrorCode::CODE_ERR_SYSTEM, '脚手架配置错误, 缺少列表接口'); + } + try { + return Guzzle::post($saveApi, $this->request); + } catch (\Exception $e) { + return $this->fail(ErrorCode::CODE_ERR_SYSTEM, '外部接口转发失败 ' . $e->getMessage()); + } + } + + public function delete() + { + } + + public function proxy() + { + $proxyUrl = $this->request->query('proxy_url'); + + if (!$proxyUrl) { + return $this->fail(ErrorCode::CODE_ERR_SYSTEM, '脚手架配置错误, 缺少列表接口'); + } + try { + return Guzzle::proxy($proxyUrl, $this->request); + } catch (\Exception $e) { + return $this->fail(ErrorCode::CODE_ERR_SYSTEM, '外部接口转发失败 ' . $e->getMessage()); + } + } +} diff --git a/src/Controller/UploadController.php b/src/Controller/UploadController.php new file mode 100644 index 0000000..c899a57 --- /dev/null +++ b/src/Controller/UploadController.php @@ -0,0 +1,62 @@ +request->input('bucket', 'local'); + $private = $this->request->input('private', false); + + $file = $this->request->file('file'); + if (!$file->isValid()) { + return $this->fail(ErrorCode::CODE_ERR_PARAM); + } + + $tmp_file = $file->toArray()['tmp_file']; + $md5_filename = md5_file($tmp_file); + $path = '1/' . date('Ym') . '/' . $md5_filename . '.' . $file->getExtension(); + + try { + $uploaded = move_local_file_to_filesystem($tmp_file, $path, $private, $bucket); + if ($uploaded === false) { + return $this->fail(ErrorCode::CODE_ERR_SERVER, '上传失败'); + } + [$width, $height] = getimagesize($tmp_file); + $info = [ + 'path' => $uploaded['path'], + 'url' => $uploaded['file_path'], + 'key' => 'file', + 'size' => $file->toArray()['size'], + 'width' => $width, + 'height' => $height, + ]; + + return $this->success($info); + } catch (\Exception $e) { + Log::get('upload')->error($e->getMessage()); + + return $this->fail(ErrorCode::CODE_ERR_SERVER, $e->getMessage()); + } + } + + public function privateFileUrl() + { + $oss_path = $this->request->input('key'); + $bucket = $this->request->input('storage', config('file.default')); + + if (!$oss_path) { + return $this->fail(ErrorCode::CODE_ERR_PARAM); + } + $private_url = filesystem_private_url($oss_path, MINUTE * 5, $bucket); + if (!$private_url) { + return $this->fail(ErrorCode::CODE_ERR_SYSTEM); + } + + return $this->response->redirect($private_url); + } +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 0000000..cc5fa15 --- /dev/null +++ b/src/Controller/UserController.php @@ -0,0 +1,325 @@ + true, + 'deleteAble' => true, + 'defaultList' => true, + 'filter' => ['realname%', 'username%', 'created_at'], + 'form' => [ + 'id' => 'int', + 'username|登录账号' => [ + 'rule' => 'required', + 'readonly' => true, + ], + 'avatar|头像' => [ + 'type' => 'image', + 'rule' => 'string', + ], + 'realname|昵称' => '', + 'mobile|手机' => '', + 'email|邮箱' => 'email', + 'sign|签名' => '', + 'pwd|密码' => [ + 'virtual_field' => true, + 'default' => '', + 'props' => [ + 'size' => 'small', + 'maxlength' => 20, + ], + 'info' => '若设置, 将会更新用户密码' + ], + 'status|状态' => [ + 'rule' => 'required', + 'type' => 'radio', + 'options' => User::$status, + 'default' => 0, + ], + 'is_admin|类型' => [ + 'rule' => 'int', + 'type' => 'radio', + 'options' => [ + NO => '普通管理员', + YES => '超级管理员', + ], + 'info' => '普通管理员需要分配角色才能访问角色对应的资源;超级管理员可以访问全部资源', + 'default' => NO, + 'render' => function ($field, &$rule) { + if (!$this->auth_service->isSupperAdmin()) { + $rule['type'] = 'hidden'; + } + }, + ], + 'role_ids|角色' => [ + 'rule' => 'array', + 'type' => 'el-cascader-panel', + 'virtual_field' => true, + 'props' => [ + 'props' => [ + 'multiple' => true, + 'leaf' => 'leaf', + 'emitPath' => false, + 'checkStrictly' => true, + ], + ], + 'render' => function ($field, &$data) { + $id = (int)$this->request->route('id', 0); + $data['value'] = $this->permission_service->getUserRoleIds($id); + $data['props']['options'] = $this->permission_service->getRoleTree(); + }, + ], + 'created_at|创建时间' => [ + 'form' => false, + 'type' => 'date_range', + ], + ], + 'hasOne' => function ($field, $row) { + return 'hyperf_admin.'.env('HYPERF_ADMIN_DB_NAME').'.user_role:user_id,role_id'; + }, + 'table' => [ + 'columns' => [ + 'id', + 'realname', + 'username', + [ + 'field' => 'mobile', + 'render' => function ($field, $row) { + return data_desensitization($field, 3, 4); + }, + ], + ['field' => 'avatar', 'render' => 'avatarRender'], + 'email', + [ + 'field' => 'status', + 'enum' => [ + User::USER_DISABLE => 'info', + User::USER_ENABLE => 'success', + ], + ], + [ + 'field' => 'role_id', + 'title' => '权限', + 'virtual_field' => true, + ], + ], + 'rowActions' => [ + ['action' => '/user/{id}', 'text' => '编辑',], + ], + ], + ]; + } + + public function menu() + { + $module = $this->request->input('module', 'default'); + $user = auth()->user(); + $base_path = BASE_PATH . '/runtime/menu/'; + $cache_key = $this->permission_service->getPermissionCacheKey($user['id']); + $cache_file = "{$base_path}{$cache_key}/{$module}.menu.{$user['id']}.cache"; + $menu_list = file_exists($cache_file) ? require $cache_file : []; + if (empty($menu_list)) { + $where = [ + 'module' => $module, + 'type' => [0, 1], + ]; + if (!$this->auth_service->isSupperAdmin()) { + $user_role_ids = $this->permission_service->getUserRoleIds($user['id']); + $where['id'] = $this->permission_service->getRoleMenuIds($user_role_ids); + } + $menu_list = (new Menu())->tree($where, [ + 'id', + 'pid', + 'label as menu_name', + 'is_menu as hidden', + 'is_scaffold as scaffold', + 'path as url', + 'view', + 'icon', + ], 'id'); + if (!empty($menu_list)) { + if (file_exists($base_path)) { + rmdir_recursive($base_path); + } + mkdir($base_path . $cache_key, 0755, true); + file_put_contents($cache_file, 'success([ + 'menuList' => $menu_list, + ]); + } + + protected function beforeSave($pk_val, &$data) + { + if (!empty($data['pwd'])) { + $data['password'] = $this->passwordHash($data['pwd']); + } + } + + public function afterSave($pk_val, $data) + { + $role_ids = array_filter(array_unique($data['role_ids'])); + if ((int)$data['id'] == $pk_val) { + UserRole::where('user_id', $pk_val)->delete(); + // 清除缓存 + $this->permission_service->getPermissionCacheKey(0, true); + } + $user_roles = []; + foreach ($role_ids as $role_id) { + $user_roles[] = [ + 'user_id' => $pk_val, + 'role_id' => (int)$role_id, + ]; + } + if (!empty($user_roles)) { + UserRole::insertOnDuplicateKey($user_roles, ['user_id', 'role_id']); + } + + return true; + } + + public function login() + { + $username = $this->request->input('username', ''); + $password = $this->request->input('password', ''); + if (!$username || !$password) { + return $this->fail(ErrorCode::CODE_ERR_PARAM); + } + $user = $this->getModel()->where('username', $username)->first(); + if (!$user || $user['status'] !== YES) { + return $this->fail(ErrorCode::CODE_ERR_PARAM, 'Incorrect or invalid username'); + } + if ($user->password !== $this->passwordHash($password)) { + return $this->fail(ErrorCode::CODE_ERR_PARAM, 'Incorrect password'); + } + $data = [ + 'iat' => Carbon::now()->timestamp, + 'exp' => Carbon::now()->addDay()->timestamp, + 'user_info' => [ + 'id' => $user->id, + 'name' => $user->username, + 'alias_name' => $user->realname, + 'email' => '', + 'avatar' => $user->avatar, + 'mobile' => $user->mobile, + ], + ]; + $token = JWT::token($data); + + return $this->success([ + 'id' => $user->id, + 'mobile' => $user->mobile, + 'name' => $user->realname, + 'avatar' => $user->avatar, + 'token' => $token, + ]); + } + + public function passwordHash($password) + { + return sha1(md5($password) . md5(config('password.salt'))); + } + + public function logout() + { + $user = $this->auth_service->logout(); + return $this->success(); + } + + public function act() + { + $attr = ['select' => ['id as value', 'realname as label']]; + $model = $this->getModel(); + $options = $model->search($attr, [], 'realname'); + + return $this->success($options); + } + + public function export() + { + $url = $this->request->input('url'); + $task = new ExportTasks(); + $task->name = $this->request->input('name'); + $task->list_api = $url; + $task->filters = array_filter($this->request->input('filters'), function ($item) { + return $item !== ''; + }); + $task->operator_id = $this->userId() ?? 0; + if ((new ExportService())->getFirstSameTask($task->list_api, $task->filters, $task->operator_id)) { // 如果当天已经有相同过滤条件且还未成功生成文件的任务 + return $this->success([], '已经有相同的任务,请勿重复导出'); + } + $task->current_page = 0; + $saved = $task->save(); + log_operator($this->getModel(), '导出', $task->id ?? 0); + $limit_max = ExportTasks::LIMIT_SIZE_MAX; + return $saved ? $this->success([], '导出任务提交成功, 请在右上角小铃铛处查看任务状态,您将最多导出' . $limit_max . '条数据。') : $this->fail(ErrorCode::CODE_ERR_SERVER, '导出失败'); + } + + public function exportTasks() + { + /** @var ExportService $export */ + $export = make(ExportService::class); + $list = $export->getTasks(null, $this->auth_service->get('id'), [ + 'id', + 'name', + 'status', + 'total_pages', + 'current_page', + 'download_url', + 'created_at', + ]); + + return $this->success([ + 'list' => $list, + ]); + } + + public function exportLimit() + { + $limit_max = ExportTasks::LIMIT_SIZE_MAX; + return $this->success([ + 'max' => $limit_max + ]); + } + + /** + * 导出任务重试 + * 重新丢入队列 + * + * @param int $id 重试id + * + * @return Mixed + */ + public function exportTasksRetry($id) + { + $export_tasks = ExportTasks::find($id); + $updated = false; + if ($export_tasks) { + $updated = $export_tasks->update([ + 'status' => 0, + 'current_page' => 0 + ]); + } + return $this->success([ + 'retry' => $updated + ]); + } +} diff --git a/src/Crontab/ExportTask.php b/src/Crontab/ExportTask.php new file mode 100644 index 0000000..55307db --- /dev/null +++ b/src/Crontab/ExportTask.php @@ -0,0 +1,33 @@ +info(__METHOD__ . ' ==================> started'); + $list = $export->getTasks(0, 0, ['*'], $params ?? []); + $ids = is_array($list) ? array_column($list, 'id') : $list->pluck('id')->toArray(); + if($ids) { + ExportTasks::whereIn('id', $ids) + ->update(['status' => ExportTasks::STATUS_PRE_PROCESSING]); // 设置预处理状态 + } + foreach($list as $task) { + $export->processTask($task); + } + } + + protected function evaluate(): bool + { + return true; + } +} diff --git a/src/Install/InstallCommand.php b/src/Install/InstallCommand.php new file mode 100644 index 0000000..0304a19 --- /dev/null +++ b/src/Install/InstallCommand.php @@ -0,0 +1,29 @@ +setDescription('install db from hyperf-admin.'); + } + + public function handle() + { + $db_conf = config('databases.hyperf_admin'); + if (!$db_conf || !$db_conf['host']) { + $this->output->error('place set hyperf_admin db config in env'); + } + + $sql = file_get_contents(__DIR__ . '/install.sql'); + + $re = Db::connection('hyperf_admin')->getPdo()->exec($sql); + + $this->output->success('hyperf-admin db install success'); + } +} diff --git a/src/Install/UpdateCommand.php b/src/Install/UpdateCommand.php new file mode 100644 index 0000000..bddd15a --- /dev/null +++ b/src/Install/UpdateCommand.php @@ -0,0 +1,44 @@ +setDescription('update db for hyperf-admin.') + ->addArgument('version', InputArgument::REQUIRED, 'the update db version.'); + } + + public function handle() + { + $version = $this->input->getArgument('version'); + $db_conf = config('databases.hyperf_admin'); + if (!$db_conf || !$db_conf['host']) { + $this->output->error('place set hyperf_admin db config in env'); + return 1; + } + + $update_sql_file = __DIR__ . "/update_{$version}.sql"; + + if (!file_exists($update_sql_file)) { + $this->output->error("the version {$version} file not found"); + return 1; + } + + $sql = file_get_contents($update_sql_file); + + $re = Db::connection('hyperf_admin')->getPdo()->exec($sql); + + $this->output->success('hyperf-admin db update success'); + + } +} diff --git a/src/Install/install.sql b/src/Install/install.sql new file mode 100644 index 0000000..f92df2b --- /dev/null +++ b/src/Install/install.sql @@ -0,0 +1,142 @@ +-- rock-admin db 安装 + +CREATE TABLE `common_config` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `namespace` varchar(50) NOT NULL DEFAULT '' COMMENT '命名空间, 字母', + `name` varchar(100) NOT NULL COMMENT '配置名, 字母', + `title` varchar(100) NOT NULL DEFAULT '' COMMENT '可读配置名', + `remark` varchar(100) NOT NULL DEFAULT '' COMMENT '备注', + `rules` text COMMENT '配置规则描述', + `value` text COMMENT '具体配置值 key:value', + `permissions` text COMMENT '权限', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_need_form` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '是否启用表单:0,否;1,是', + PRIMARY KEY (`id`), + UNIQUE KEY `unique` (`name`,`namespace`), + KEY `namespace` (`namespace`), + KEY `updated_at` (`updated_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通用配置'; + +CREATE TABLE `export_tasks` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '任务名称', + `list_api` varchar(255) NOT NULL COMMENT '列表接口', + `filters` text COMMENT '过滤条件', + `status` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '任务状态: 0未开始, 1进行中, 2已完成, 3失败', + `total_pages` int(11) unsigned NOT NULL DEFAULT '1' COMMENT '总页数', + `current_page` int(11) unsigned NOT NULL DEFAULT '1' COMMENT '当前页', + `operator_id` int(11) unsigned NOT NULL COMMENT '管理员id', + `download_url` varchar(100) NOT NULL DEFAULT '' COMMENT '下载地址', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `host_ip` varchar(50) DEFAULT '' COMMENT '主机ip', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `front_routes` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父级ID', + `label` varchar(50) NOT NULL DEFAULT '' COMMENT 'label名称', + `module` varchar(50) NOT NULL DEFAULT '' COMMENT '模块', + `path` varchar(100) NOT NULL DEFAULT '' COMMENT '路径', + `view` varchar(100) NOT NULL DEFAULT '' COMMENT '非脚手架渲染是且path路径为正则时, vue文件路径', + `icon` varchar(50) NOT NULL DEFAULT '' COMMENT 'icon', + `open_type` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '打开方式 0 当前页面 2 新标签页', + `is_scaffold` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '是否脚手架渲染, 1是, 0否', + `is_menu` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否菜单 0 否 1 是', + `status` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '状态:0 禁用 1 启用', + `sort` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '排序,数字越大越在前面', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `permission` text NOT NULL COMMENT '权限标识', + `http_method` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '请求方式; 0, Any; 1, GET; 2, POST; 3, PUT; 4, DELETE;', + `type` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '菜单类型 0 目录 1 菜单 2 其他', + `page_type` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '页面类型: 0 列表 1 表单', + `scaffold_action` varchar(255) NOT NULL DEFAULT '' COMMENT '脚手架预置权限', + `config` text COMMENT '配置化脚手架', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=90 DEFAULT CHARSET=utf8mb4 COMMENT='前端路由(菜单)'; + +CREATE TABLE `global_config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `namespace` varchar(50) NOT NULL DEFAULT '', + `name` varchar(100) NOT NULL, + `title` varchar(100) NOT NULL DEFAULT '', + `remark` varchar(100) NOT NULL DEFAULT '', + `value` longtext, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_name` (`name`), + KEY `namespace` (`namespace`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='全局配置'; + +CREATE TABLE `role_menus` ( + `role_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '角色ID', + `router_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '路由ID', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `role_router_id` (`role_id`,`router_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `roles` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父级ID', + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称', + `permissions` text NOT NULL COMMENT '角色拥有的权限', + `status` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '状态:0 禁用 1 启用', + `sort` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '排序,数字越大越在前面', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; + +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名', + `realname` varchar(50) NOT NULL DEFAULT '', + `password` char(40) NOT NULL, + `mobile` varchar(20) NOT NULL DEFAULT '', + `email` varchar(50) NOT NULL DEFAULT '', + `status` tinyint(4) NOT NULL DEFAULT '1', + `login_time` timestamp NULL DEFAULT NULL, + `login_ip` varchar(50) DEFAULT NULL, + `is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'is admin', + `is_default_pass` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否初始密码1:是,0:否', + `qq` varchar(20) NOT NULL DEFAULT '' COMMENT '用户qq', + `roles` varchar(50) NOT NULL DEFAULT '10', + `sign` varchar(255) NOT NULL DEFAULT '' COMMENT '签名', + `avatar` varchar(255) NOT NULL DEFAULT '', + `avatar_small` varchar(255) NOT NULL DEFAULT '', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `user_role` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(11) unsigned NOT NULL DEFAULT '0', + `role_id` int(11) unsigned NOT NULL DEFAULT '0', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_role_id` (`user_id`,`role_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `operator_log` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `page_url` varchar(50) DEFAULT '' COMMENT '页面url', + `page_name` varchar(50) DEFAULT '' COMMENT '页面面包屑/名称', + `action` varchar(50) DEFAULT '' COMMENT '动作', + `operator_id` int(11) DEFAULT '0' COMMENT '操作人ID', + `nickname` varchar(50) DEFAULT '' COMMENT '操作人名称', + `relation_ids` text COMMENT '多个id-当前版本ID[id-current_version_id,]', + `detail_json` text COMMENT '需要灵活记录的json', + `client_ip` varchar(50) DEFAULT '' COMMENT '客户端地址', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4511 DEFAULT CHARSET=utf8 COMMENT='通用操作日志'; diff --git a/src/Middleware/AuthMiddleware.php b/src/Middleware/AuthMiddleware.php new file mode 100644 index 0000000..a7a90f4 --- /dev/null +++ b/src/Middleware/AuthMiddleware.php @@ -0,0 +1,54 @@ +log = $logger->get('auth'); + $this->auth_service = make(AuthService::class); + parent::__construct($container, 'http'); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + // 检验登录状态 + $user = $this->auth_service->check(); + $this->log->info('当前登录用户信息', $user); + + return $handler->handle($request); + } +} diff --git a/src/Middleware/PermissionMiddleware.php b/src/Middleware/PermissionMiddleware.php new file mode 100644 index 0000000..bfe99c8 --- /dev/null +++ b/src/Middleware/PermissionMiddleware.php @@ -0,0 +1,166 @@ +container = $container; + $this->response = $response; + $this->request = $request; + $this->log = $logger->get('permission'); + $this->permission_service = make(PermissionService::class); + $this->auth_service = make(AuthService::class); + parent::__construct($container, 'http'); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $uri = $request->getUri(); + $path = $uri->getPath(); + $method = $request->getMethod(); + + $module_proxy = make(ModuleProxy::class); + if ($module_proxy->needProxy()) { + $res = $module_proxy->request(); + if (isset($res['payload']) && $res['payload'] === []) { + $res['payload'] = (object)[]; + } + $response = $this->response->json($res); + Log::get('http')->info('proxy_end', [ + 'module' => $module_proxy->getTargetModule(), + 'path' => $path, + 'response' => $response, + ]); + + return $response; + } + + // 其他系统调用,走AKSK中间件验证 + $client_token = $request->getHeader('Authorization')[0] ?? ''; + if ($client_token) { + if (!$this->akSkAuth($uri, $method, $client_token)) { + return $this->fail(ErrorCode::CODE_ERR_DENY, '接口权限校验失败'); + } + + return $handler->handle($request); + } + // 内部请求时不做鉴权 + if ($this->getRealIp() == '127.0.0.1') { + return $handler->handle($request); + } + // 开放资源,不进行鉴权 + if ($this->permission_service->isOpen($path, $method)) { + return $handler->handle($request); + } + // 检验登录状态 + $user = $this->auth_service->user(); + if (empty($user)) { + return $this->fail(ErrorCode::CODE_LOGIN, '请先登录'); + } + + if (!$this->permission_service->hasPermission($path, $method)) { + return $this->fail(ErrorCode::CODE_NO_AUTH, "{$path}权限不足"); + } + + return $handler->handle($request); + } + + protected function getRealIp() + { + return $this->request->header('x-real-ip'); + } + + /** + * @param int $code + * @param string|null $message + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function fail(int $code = -1, ?string $message = null) + { + $response = [ + 'code' => $code, + 'message' => $message ?: ErrorCode::getMessage($code), + 'payload' => (object)[], + ]; + $this->log->warning($this->request->getUri()->getPath() . ' fail', $response); + + return $this->response->json($response); + } + + private function akSkAuth($uri, $method, $client_token) + { + $host = env('APP_DOMAIN', ''); + $query = $this->request->getQueryParams(); + if (!empty($query)) { + ksort($query); + $query = http_build_query($query); + } else { + $query = ''; + } + $path = $uri->getPath(); + $content_type = $this->request->getHeader('Content-type')[0] ?? ''; + $body = $this->request->getParsedBody(); + if (!empty($body)) { + ksort($body); + $body = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } else { + $body = ''; + } + $ak = str_replace('ha ', '', explode(':', $client_token)[0] ?? ''); + $sk = config('client_user')[$ak] ?? ''; + $auth = new AKSK($ak, $sk); + $token = $auth->token($method, $path, $host, $query, $content_type, $body); + $this->log->info('aksk auth:', [ + 'client_token' => $client_token, + 'except_token' => $token, + ]); + + return $token === $client_token; + } +} diff --git a/src/Model/CommonConfig.php b/src/Model/CommonConfig.php new file mode 100644 index 0000000..1d47746 --- /dev/null +++ b/src/Model/CommonConfig.php @@ -0,0 +1,48 @@ + 'array', + 'rules' => 'array', + 'is_need_form' => 'integer', + ]; + + const NEED_FORM_NO = 0; + + const NEED_FORM_YES = 1; + + public static $need_form = [ + self::NEED_FORM_NO => '否', + self::NEED_FORM_YES => '是', + ]; +} diff --git a/src/Model/ExportTasks.php b/src/Model/ExportTasks.php new file mode 100644 index 0000000..b7b3921 --- /dev/null +++ b/src/Model/ExportTasks.php @@ -0,0 +1,54 @@ + 'array', + 'status' => 'integer', + 'total_pages' => 'integer', + 'current_page' => 'integer', + 'operator_id' => 'integer', + ]; + + const STATUS_NOT_START = 0; + + const STATUS_PRE_PROCESSING = 10; // 预处理状态 + + const STATUS_PROCESSING = 1; + + const STATUS_SUCCESS = 2; + + const STATUS_FAIL = 3; + + const LIMIT_SIZE_MAX = 30000; // 导出最大条数 +} diff --git a/src/Model/FrontRoutes.php b/src/Model/FrontRoutes.php new file mode 100644 index 0000000..d8a15e3 --- /dev/null +++ b/src/Model/FrontRoutes.php @@ -0,0 +1,104 @@ + 'integer', + 'open_type' => 'integer', + 'type' => 'integer', + 'is_menu' => 'integer', + 'status' => 'integer', + 'is_scaffold' => 'integer', + 'page_type' => 'integer', + 'sort' => 'integer', + 'config' => 'json', + ]; + + const HTTP_METHOD_ANY = 0; + + const HTTP_METHOD_GET = 1; + + const HTTP_METHOD_POST = 2; + + const HTTP_METHOD_PUT = 3; + + const HTTP_METHOD_DELETE = 4; + + const PAGE_TYPE_LIST = 0; + + const PAGE_TYPE_FORM = 1; + + const IS_MENU = 1; + + const IS_NOT_MENU = 0; + + const RESOURCE_OPEN = 0; + + const RESOURCE_NEED_AUTH = 1; + + public static $http_methods = [ + self::HTTP_METHOD_ANY => 'ANY', + self::HTTP_METHOD_GET => 'GET', + self::HTTP_METHOD_POST => 'POST', + self::HTTP_METHOD_PUT => 'PUT', + self::HTTP_METHOD_DELETE => 'DELETE', + ]; + + public function scopeDefaultSelect($query) + { + return $query->select([ + 'id', + 'pid', + 'label as menu_name', + 'is_menu as hidden', + 'is_scaffold as scaffold', + 'path as url', + 'view', + 'icon', + 'sort', + ]); + } + + public function scopeOnlyMenu($query) + { + return $query->where('is_menu', YES); + } +} diff --git a/src/Model/GlobalConfig.php b/src/Model/GlobalConfig.php new file mode 100644 index 0000000..278cbef --- /dev/null +++ b/src/Model/GlobalConfig.php @@ -0,0 +1,96 @@ + 'int']; + + const CACHE_PREFIX = 'omsapi_config_'; + + const CACHE_TIME = 60 * 5; + + const VALUE_STATUS_YES = 1; //启用 + + const VALUE_STATUS_NO = 0; //禁用 + + const VALUE_STATUS_MAP = [ + self::VALUE_STATUS_YES => '启用', + self::VALUE_STATUS_NO => '禁用', + ]; + + const NAMESPACE_AB_WHITE_LIST = 'ab_white_list'; + + public static function getConfig($name, $default = null, $cache = false) + { + $cache_key = self::getConfigCache($name); + $cache_config = Redis::get($cache_key); + if($cache_config) { + return json_decode($cache_config, true); + } + $item = static::query()->where('name', $name)->get()->toArray(); + if(empty($item)) { + return $default; + } + $config = json_decode($item[0]['value'], true); + if(is_null($config)) { + return $default; + } + if($cache) { + Redis::setex($cache_key, self::CACHE_TIME, json_encode($config)); + } + + return $config; + } + + public static function getConfigCache($name) + { + return self::CACHE_PREFIX . $name; + } + + public static function setConfig($name, $value, $ext = [], $raw = true) + { + $namespace = ''; + if(($pos = strpos($name, '.')) !== false) { + $namespace = substr($name, 0, $pos); + } + if($raw) { + $value = json_encode($value); + } + $ins = [ + 'name' => $name, + 'value' => $value, + 'namespace' => $namespace, + ]; + if($ext) { + $ins = array_merge($ins, $ext); + } + + return static::query()->updateOrInsert(['name' => $name], $ins); + } +} diff --git a/src/Model/OperatorLog.php b/src/Model/OperatorLog.php new file mode 100644 index 0000000..d2be479 --- /dev/null +++ b/src/Model/OperatorLog.php @@ -0,0 +1,23 @@ + 'array', + 'params' => 'array', + 'user_id' => 'integer', + 'req_id' => 'integer', + ]; + + /** + * 获取产生当前版本的用户信息 + * + * @return \Hyperf\Database\Model\Model|\Hyperf\Database\Model\Relations\BelongsTo|object|null + */ + public function getUser() + { + return $this->belongsTo(User::class, 'user_id', 'id')->first(); + } +} diff --git a/src/Model/Role.php b/src/Model/Role.php new file mode 100644 index 0000000..1d8e5e1 --- /dev/null +++ b/src/Model/Role.php @@ -0,0 +1,49 @@ + 'integer', + 'permissions' => 'array', + 'sort' => 'integer', + 'name' => 'string', + 'pid' => 'integer', + 'id' => 'integer', + 'value' => 'integer', + ]; + + public function menus() + { + return $this->hasMany('App\System\Model\MtOms\RoleMenu', 'role_id'); + } + + public function resources() + { + return $this->hasMany('App\System\Model\MtOms\RoleResource', 'role_id'); + } +} diff --git a/src/Model/RoleMenu.php b/src/Model/RoleMenu.php new file mode 100644 index 0000000..0ac58e7 --- /dev/null +++ b/src/Model/RoleMenu.php @@ -0,0 +1,28 @@ + 'integer', + 'router_id' => 'integer', + ]; +} diff --git a/src/Model/User.php b/src/Model/User.php new file mode 100644 index 0000000..7b16b21 --- /dev/null +++ b/src/Model/User.php @@ -0,0 +1,90 @@ + 'int', + 'status' => 'integer', + 'is_admin' => 'integer', + 'is_default_pass' => 'integer', + 'value' => 'integer', + ]; + + const USER_ENABLE = 1; + + const USER_DISABLE = 0; + + public static $status = [ + self::USER_DISABLE => '禁用', + self::USER_ENABLE => '启用', + ]; + + public function getRolesAttribute($value) + { + return explode(',', $value); + } + + public function setRolesAttribute($value) + { + return implode(',', $value); + } + + public function getRealnameAttribute($value) + { + return $value ?: $this->username; + } + + public function setRealnameAttribute($value) + { + $this->attributes['realname'] = $value ?: $this->username; + } +} diff --git a/src/Model/UserRole.php b/src/Model/UserRole.php new file mode 100644 index 0000000..9358653 --- /dev/null +++ b/src/Model/UserRole.php @@ -0,0 +1,29 @@ + 'integer', + 'role_id' => 'integer', + ]; +} diff --git a/src/Model/Version.php b/src/Model/Version.php new file mode 100644 index 0000000..3b90cda --- /dev/null +++ b/src/Model/Version.php @@ -0,0 +1,76 @@ + 'array', + 'modify_fields' => 'array', + ]; + + public function versionable() + { + return $this->morphTo(null, 'table', 'pk'); + } + + /** + * 获取产生当前版本的请求信息 + * @return \Hyperf\Database\Model\Model|\Hyperf\Database\Model\Relations\BelongsTo|object|null + */ + public function getRequest() + { + return $this->belongsTo(RequestLog::class, 'req_id', 'req_id')->first(); + } + + /** + * 获取产生当前版本的用户信息 + * @return \Hyperf\Database\Model\Model|\Hyperf\Database\Model\Relations\BelongsTo|object|null + */ + public function getUser() + { + return $this->belongsTo(User::class, 'user_id', 'id')->first(); + } + + /** + * TODO: 该方法应该不可用 + * 回滚版本 + * @return mixed + */ + public function revert() + { + $model = new $this->versionable_type(); + $model->unguard(); + $model->fill($this->content); + $model->exists = true; + $model->reguard(); + + unset($model->{$model->getCreatedAtColumn()}); + unset($model->{$model->getUpdatedAtColumn()}); + if (method_exists($model, 'getDeletedAtColumn')) { + unset($model->{$model->getDeletedAtColumn()}); + } + + $model->save(); + return $model; + } +} diff --git a/src/Model/Versionable.php b/src/Model/Versionable.php new file mode 100644 index 0000000..71050b1 --- /dev/null +++ b/src/Model/Versionable.php @@ -0,0 +1,195 @@ +versioning_enable; + } + + /** + * 启动版本控制 + * + * @return $this + */ + public function enableVersioning() + { + $this->versioning_enable = true; + return $this; + } + + /** + * 关闭版本控制 + * + * @return $this + */ + public function disableVersioning() + { + $this->versioning_enable = false; + return $this; + } + + /** + * 定义版本关联 + * + * @return mixed + */ + public function versions() + { + return $this->morphMany(Version::class, null, 'table', 'pk'); + } + + public function getMorphClass() + { + if (strpos($this->getTable(), '.') !== false) { + return $this->getTable(); + } + return $this->getConnectionName() . '.' . $this->getTable(); + } + + /** + * 是否是一次有效的版本控制 + * + * @return bool + */ + private function isValid() + { + $versionable_fields = $this->getVersionableFields(); + return $this->versioning_enable + && Context::has(ServerRequestInterface::class) + && $this->isDirty($versionable_fields); + } + + public function getVersionableFields() + { + $remove_version_keys = $this->versioning_expect_fields ?? []; + $remove_version_keys[] = $this->getUpdatedAtColumn(); + if (method_exists($this, 'getDeletedAtColumn')) { + $remove_version_keys[] = $this->getDeletedAtColumn(); + } + return collect($this->getAttributes())->except($remove_version_keys)->keys()->all(); + } + + public function saving(Saving $event) + { + $this->is_update = $this->exists; + } + + /** + * 监听saved事件保存表更数据 + * + * @param Saved $event + */ + public function saved(Saved $event) + { + if (!$this->isValid()) { + return; + } + + $request_log = $this->processRequest(); + + $version = new Version(); + $version->pk = $this->getKey(); + $version->table = strpos($this->getTable(), '.') ? $this->getTable() : $this->getConnectionName() . '.' . $this->getTable(); + $version->content = $this->getAttributes(); + $versionable_fields = $this->getVersionableFields(); + $changes = array_remove_keys_not_in($this->getChanges(), $versionable_fields); + $version->modify_fields = array_keys($changes); + $version->action = $this->is_update ? 'update' : 'insert'; + $version->user_id = $request_log->user_id; + /** @var ServerRequestInterface $request */ + $request = container(ServerRequestInterface::class); + $version->req_id = $request->getAttribute('_req_id'); + $version->save(); + + $this->clearOldVersions(); + } + + /** + * 记录数据版本前先记录请求 + * + * @return RequestLog + */ + private function processRequest(): RequestLog + { + /** @var ServerRequestInterface $request */ + $request = container(ServerRequestInterface::class); + + $req_id = $request->getAttribute('_req_id'); + if (empty($req_id)) { + $req_id = id_gen(); + $request = Context::set(ServerRequestInterface::class, $request->withAttribute('_req_id', $req_id)); + return RequestLog::create([ + 'host' => $request->getUri()->getHost(), + 'path' => $request->getUri()->getPath(), + 'method' => $request->getMethod(), + 'header' => $request->getHeaders(), + 'params' => $request->getParsedBody(), + 'user_id' => JWT::verifyToken(cookie('X-Token') ?? '')['user_info']['id'] ?? 0, + 'req_id' => $req_id, + ]); + } else { + return RequestLog::where('req_id', $req_id)->first(); + } + } + + /** + * 默认保留100个数据版本,可被覆盖 + */ + private function clearOldVersions() + { + $keep = $this->keep_version_count ?? 100; + $count = $this->versions()->count(); + if ($keep > 0 && $count > $keep) { + $this->versions()->limit($count - $keep)->delete(); + } + } + + /** + * 返回当前版本的数据 + * + * @return mixed + */ + public function currentVersion() + { + return $this->versions()->orderBy(Version::CREATED_AT, 'DESC')->orderBy('id', 'DESC')->first(); + } + + /** + * 前几个版本 + * + * @param int $previous + * + * @return mixed + */ + public function previousVersion(int $previous = 1) + { + return $this->versions()->orderBy(Version::CREATED_AT, 'DESC')->orderBy('id', 'DESC')->offset($previous)->first(); + } + + /** + * 最后几个版本 + * + * @param int $num + * + * @return mixed + */ + public function lastVersion($num = 1) + { + return $this->versions()->orderBy('id', 'DESC')->limit($num) + //->offset(1) // 排除当前版本 + ->get(); + } +} diff --git a/src/Service/AuthService.php b/src/Service/AuthService.php new file mode 100644 index 0000000..a56b408 --- /dev/null +++ b/src/Service/AuthService.php @@ -0,0 +1,72 @@ +info('hyperf_admin_token_payload', is_array($payload) ? $payload : [$payload]); + } + if(!$payload) { + return []; + } + $sso_user_info = Arr::get($payload, 'user_info'); + if(empty($sso_user_info)) { + return []; + } + $cache_key = config('user_info_cache_prefix') . md5(json_encode($sso_user_info)); + if(!$user = Redis::get($cache_key)) { + $user = container(UserService::class)->findUserOrCreate($sso_user_info); + $expire = $payload['exp'] - time(); + if($user && $expire > 0) { + // 缓存用户信息 + Redis::setex($cache_key, $expire, json_encode($user)); + } + } else { + $user = json_decode($user, true); + } + + return $this->setUser($user); + } + + public function user() + { + return Context::get('user_info') ?? []; + } + + public function isSupperAdmin() + { + $user = $this->user(); + + return ($user['is_admin'] ?? NO) === YES; + } + + public function setUser($data) + { + return Context::set('user_info', $data); + } + + public function get($key) + { + return Arr::get($this->user(), $key); + } + + public function logout() + { + $token = cookie(config('admin_cookie_name', '')) ?: (request_header('x-token')[0] ?? ''); + $payload = JWT::verifyToken($token); + $user = Arr::get($payload, 'user_info'); + $cache_key = config('user_info_cache_prefix') . md5(json_encode($user)); + Redis::del($cache_key); + Context::set('user_info', null); + } +} diff --git a/src/Service/CommonConfig.php b/src/Service/CommonConfig.php new file mode 100644 index 0000000..e0c53ea --- /dev/null +++ b/src/Service/CommonConfig.php @@ -0,0 +1,49 @@ +where('namespace', 'system') + ->where('name', 'namespace') + ->first() + ->toArray()['rules'] ?? []; + } + + public static function getValue($namespace, $name, $default = []) + { + if($record = CommonConfigModel::query() + ->where('namespace', $namespace) + ->where('name', $name) + ->first()) { + return $record->toArray()['value']; + } + + return $default; + } + + public static function getConfigByName($name) + { + $conf = CommonConfigModel::query()->where(['name' => $name])->select([ + 'id', + 'rules', + 'value', + ])->first(); + if(!$conf) { + return false; + } + + return $conf->toArray(); + } + + public static function getValByName($name) + { + $value = CommonConfigModel::query()->where(['name' => $name])->value('value'); + + return $value ?: []; + } +} diff --git a/src/Service/ExportService.php b/src/Service/ExportService.php new file mode 100644 index 0000000..6d25137 --- /dev/null +++ b/src/Service/ExportService.php @@ -0,0 +1,198 @@ + ['list_api' => ['%123%', '%345'], 'filters' => ['%123%', '%456%']],'like' => ['list_pai' => ['%ttt%']], 'in' => ['operator_id' => [123,123,123]], 'not_in' => ['operator_id' => [456,5466,234]]] + * + * @return array + */ + public function getTasks($status = 0, $operator_id = 0, $columns = ['*'], $filter_options = []) + { + $query = ExportTasks::query()->select($columns); + if($status !== null) { + $query->where('status', $status); + } + if($operator_id) { + $query->where('operator_id', $operator_id); + } + if(!empty($filter_options['not_like'])) { + $query->where(function ($q) use ($filter_options) { + foreach($filter_options['not_like'] as $column => $likes) { + foreach($likes as $like) { + $q->where($column, 'not like', $like); + } + } + }); + } + if(!empty($filter_options['like'])) { + $query->where(function ($q) use ($filter_options) { + foreach($filter_options['like'] as $column => $likes) { + foreach($likes as $like) { + $q->where($column, 'like', $like); + } + } + }); + } + if(!empty($filter_options['in'])) { + $query->where(function ($q) use ($filter_options) { + foreach($filter_options['in'] as $column => $ins) { + $q->whereIn($column, $ins); + } + }); + } + if(!empty($filter_options['not_in'])) { + $query->where(function ($q) use ($filter_options) { + foreach($filter_options['not_in'] as $column => $ins) { + $q->whereNotIn($column, $ins); + } + }); + } + $query->orderBy('id', 'desc'); + + return $query->get() ?: []; + } + + public function processTask(ExportTasks $task) + { + try { + if(in_array(ExportTasks::find($task->id)->status, [ + ExportTasks::STATUS_PROCESSING, + ExportTasks::STATUS_SUCCESS, + ])) { + Log::get('export_service')->info('正在处理该任务,不要重复处理', [$task]); + + return; + } + $task->fill(['status' => ExportTasks::STATUS_PROCESSING])->save(); + $list_api = 'http://127.0.0.1:' . config('server.servers.0.port') . $task->list_api; + $query['_page'] = ($query['_page'] ?? 1); + $size = 100; + $query['_size'] = $size; + $query = array_merge($query, $task->filters); + $headers = [ + 'X-Real-IP' => '127.0.0.1', + ]; + $total = 999; + if(Str::endsWith($task->list_api, self::LIST_API_SUFFIX)) { + $info_api = 'http://127.0.0.1:' . config('server.servers.0.port') . str_replace(self::LIST_API_SUFFIX, self::INFO_API_SUFFIX, $task->list_api); + } else { + $subject = explode('/', $task->list_api)[1]; + $info_api = 'http://127.0.0.1:' . config('server.servers.0.port') . '/' . $subject . '/info'; + } + $info = Guzzle::get($info_api, [], $headers); + $table_headers = array_filter($info['payload']['tableHeader'], function ($item) { + return $item['hidden'] ?? true; + }); + $table_headers_str = []; + foreach($table_headers as $item) { + $table_headers_str[] = $item['title']; + } + $table_headers_str = implode(',', $table_headers_str); + $file_name = '《' . $task->name . '》下载-' . Carbon::now()->format('YmdHis') . '.csv'; + $file_path = '/tmp/' . $file_name; + file_put_contents($file_path, $this->encoding($table_headers_str) . PHP_EOL); + $counter = 0; + $parse = parse_url($list_api); + if(isset($parse['query'])) { + parse_str($parse['query'], $_query); + $query = array_merge($query, $_query); + } + while(($query['_page'] - 1) * $size < $total) { // offset值 + $ret = Guzzle::get($list_api, $query, $headers); + $total = $ret['payload']['total'] <= ExportTasks::LIMIT_SIZE_MAX ? $ret['payload']['total'] : ExportTasks::LIMIT_SIZE_MAX; + if(!is_array($ret['payload']['list'])) { + throw new \Exception('列表获取异常,任务id:' . $task->id); + } + foreach($ret['payload']['list'] as $item) { + $row = $this->getRow($table_headers, $item); + $counter++; + file_put_contents($file_path, $this->encoding($row) . PHP_EOL, FILE_APPEND); + } + $task->fill([ + 'total_pages' => ceil($total / $size), + 'current_page' => $query['_page'], + ])->save(); + $query['_page'] += 1; + } + $bucket = config('file.export_storage', config('file.default')); + $info = move_local_file_to_filesystem($file_path, '1/export_task/' . $file_name, true, $bucket); + if($info) { + $task->fill([ + 'status' => ExportTasks::STATUS_SUCCESS, + 'download_url' => $info['path'], + ])->save(); + Log::get('export_service') + ->info(sprintf('export task success, file_name:%s id:%s rows:%s', $info['file_path'], $task->id, $counter), [], 'export_task'); + } else { + Log::get('export_service') + ->error(sprintf('export task fail id:%s', $task->id), [], 'export_task'); + $task->fill(['status' => ExportTasks::STATUS_FAIL])->save(); + } + } catch (\Exception $exception) { + Log::get('export_service') + ->error(sprintf('export task fail id:%s', $task->id), ['exception' => $exception], 'export_task'); + $task->fill(['status' => ExportTasks::STATUS_FAIL])->save(); + } + } + + public function getRow($table_headers, $data) + { + $arr = []; + foreach($table_headers as $item) { + if(isset($item['options'])) { + $arr[] = $item['options'][$data[$item['field']]] ?? ($data[$item['field']] ?? ''); + continue; + } + if(isset($item['enum'])) { + $arr[] = $item['enum'][$data[$item['field']]] ?? ''; + continue; + } + $arr[] = $data[$item['field']] ?? ''; + } + $arr = array_map(function ($item) { + $item = csv_big_num($item); + $item = preg_replace('/\\n/', ' ', $item); + + return $item; + }, $arr); + + return implode(',', array_map(function ($item) { + if(is_array($item)) { + return json_encode($item, JSON_UNESCAPED_UNICODE); + } + + return $item; + }, $arr)); + } + + public function encoding($str) + { + //return iconv('utf-8', 'gbk//ignore', $str); + return mb_convert_encoding($str, "GBK", "UTF-8"); + } + + public function getFirstSameTask($url, $filters, $operator_id) + { + return ExportTasks::where('filters', json_encode($filters)) + ->where('list_api', $url) + ->where('operator_id', $operator_id) + ->where('created_at', '>=', Carbon::today()->toDateTimeString()) + ->where('status', '!=', ExportTasks::STATUS_SUCCESS) + ->first(); + } +} diff --git a/src/Service/GlobalConfig.php b/src/Service/GlobalConfig.php new file mode 100644 index 0000000..aa58ef3 --- /dev/null +++ b/src/Service/GlobalConfig.php @@ -0,0 +1,67 @@ + $name, + 'value' => $value, + 'namespace' => $namespace, + ]; + if($ext) { + $ins = array_merge($ins, $ext); + } + $model = GlobalModel::query(); + $model->getConnection()->beginTransaction(); + $id = $model->where('name', $name)->value('id'); + if($id) { + $res = $model->where('id', $id)->update($ins); + } else { + $res = $model->insert($ins); + } + if(empty($res)) { + $model->getConnection()->rollBack(); + + return false; + } + $model->getConnection()->commit(); + + return true; + } + + public static function getConfig($name, $default = null) + { + $cache_key = self::getCacheKey($name); + $data = Redis::get($cache_key); + if($data !== false) { + return my_json_decode($data); + } + $data = GlobalModel::query()->where('name', $name)->select('value')->first()->toArray(); + $data = $data ?: $default; + Redis::setex($cache_key, 5 * MINUTE, json_encode($data, JSON_UNESCAPED_UNICODE)); + + return $data; + } + + public static function getCacheKey($name) + { + return "omsapi:global_config:{$name}"; + } + + public static function getIdByName($name) + { + return GlobalModel::query()->where('name', $name)->value('id'); + } +} diff --git a/src/Service/Menu.php b/src/Service/Menu.php new file mode 100644 index 0000000..3526c33 --- /dev/null +++ b/src/Service/Menu.php @@ -0,0 +1,95 @@ +newQuery(); + } + + public function getModuleMenus($module = '', $menu_ids = []) + { + $query = $this->query(); + if (!empty($menu_ids)) { + $query->where(function ($query) use ($menu_ids) { + return $query->whereIn('id', $menu_ids)->orWhere(function ($query) use ($menu_ids) { + return $query->where('is_menu', 0)->whereIn('pid', $menu_ids); + }); + }); + } + $query = $query->select([ + 'id', + 'pid', + 'label as menu_name', + 'is_menu as hidden', + 'is_scaffold as scaffold', + 'path as url', + 'view', + 'icon', + ])->where('status', 1); + if ($module) { + $query->where('module', $module); + } + $list = $query->orderBy('sort', 'desc')->get(); + if (empty($list)) { + return []; + } + $list = $list->toArray(); + foreach ($list as &$item) { + $item['hidden'] = !(bool)$item['hidden']; + $item['scaffold'] = (bool)$item['scaffold']; + unset($item); + } + + return generate_tree($list); + } + + public function tree( + $where = [], $fields = [ + 'id as value', + 'pid', + 'label', + ], $pk_key = 'value' + ) { + $where['status'] = 1; + $query = make(FrontRoutes::class)->where2query($where)->select($fields); + $list = $query->orderBy('sort', 'desc')->get(); + if (empty($list)) { + return []; + } + $list = $list->toArray(); + + return generate_tree($list, 'pid', $pk_key, 'children', function (&$item) use ($pk_key) { + $item[$pk_key] = (int)$item[$pk_key]; + $item['pid'] = (int)$item['pid']; + if (isset($item['hidden'])) { + $item['hidden'] = !(bool)$item['hidden']; + } + if (isset($item['scaffold'])) { + $item['scaffold'] = (bool)$item['scaffold']; + } + unset($item); + }); + } + + public function getPathNodeIds($id) + { + $parents = []; + while ($p = $this->getParent($id)) { + $id = (int)$p['pid']; + if ($id) { + $parents[] = $id; + } + } + + return array_reverse($parents); + } + + public function getParent($id) + { + return $this->query()->select(['id', 'pid'])->find($id); + } +} diff --git a/src/Service/ModuleProxy.php b/src/Service/ModuleProxy.php new file mode 100644 index 0000000..300010b --- /dev/null +++ b/src/Service/ModuleProxy.php @@ -0,0 +1,53 @@ +request = request(); + $this->modules = Redis::conn()->getOrSet('hyperf_admin:system_modules', 500, function () { + $list = CommonConfig::getConfigByName('website_config')['value']['system_module']; + array_change_v2k($list, 'name'); + return $list; + }); + $this->target_module = $this->request->input('module') ?? (request_header('x-module')[0] ?? ''); + } + + public function needProxy() + { + $no_proxy = request_header('x-no-proxy')[0] ?? false; + + if ($no_proxy) { + return false; + } + + if (!isset($this->modules[$this->target_module])) { + return false; + } + + $type = $this->modules[$this->target_module]['type'] ?? 'local'; + if ($type != 'remote') { + return false; + } + + return true; + } + + public function request() + { + return Guzzle::proxy($this->modules[$this->target_module]['remote_base_uri'] . $this->request->getUri()->getPath(), $this->request); + } + + public function getTargetModule() + { + return $this->target_module; + } +} diff --git a/src/Service/OperatorLogService.php b/src/Service/OperatorLogService.php new file mode 100644 index 0000000..7b16bd6 --- /dev/null +++ b/src/Service/OperatorLogService.php @@ -0,0 +1,92 @@ +header('page-url'); + $parse_url = parse_url($page_url); + $fragment = $parse_url['fragment'] ?? '/'; // 抽出#后面的部分 + $fragments = explode('?', $fragment); // 去掉querystring + $page_url = array_shift($fragments); + $page_name = urldecode(request()->header('page-name', '')); // 页面名称 + $relation_ids = json_encode($ids, JSON_UNESCAPED_UNICODE); // 如果没有版本启用,则只记录操作的id + // 关联id-版本id记录 + if(is_string($model) && $model) { + $model = make($model); + } + if($model + && $model instanceof BaseModel + && method_exists($model, 'isVersionEnable') + && $model->isVersionEnable()) { // 如果有版本,则记录版本id + $table = strpos($model->getTable(), '.') ? $model->getTable() : $model->getConnectionName() . '.' . $model->getTable(); + $relation_ids = Version::whereIn('pk', $ids) + ->where('table', $table) + ->selectRaw('concat_ws("-", pk, max(id)) as relation_ids, pk') // 最大版本id为当前版本id + ->groupBy('pk') + ->orderBy('pk', 'desc') + ->get() + ->pluck('relation_ids') + ->toArray(); + $relation_ids = json_encode($relation_ids, JSON_UNESCAPED_UNICODE); + } + // 其他记录 + $detail_json = []; + if($remark) { + $detail_json['remark'] = $remark; + } + if($options) { + $detail_json += $options; + } + // 用户信息 + $user_info = auth()->user() ?: (new UserService())->getUser($user_id); + $client_ip = request()->header('x-real-ip') ?? (request()->getServerParams()['remote_addr'] ?? ''); + $save_data = [ + 'page_url' => $page_url, + 'page_name' => $page_name, + 'action' => $action, + 'relation_ids' => $relation_ids, + 'client_ip' => $client_ip, + 'operator_id' => $user_info['id'] ?? 0, + 'nickname' => $user_info['realname'] ?? ($user_info['username'] ?? ''), + ]; + // {谁}于{时间}在{页面名称}{操作:新增|编辑|删除|导入|导出}了ID为{支持多个}的记录 + $now_date = Carbon::now()->toDateTimeString(); + $detail_json['description'] = "{$save_data['nickname']}于{$now_date} 在{$save_data['page_name']}页{$save_data['action']}了ID为" . implode('、', $ids) . '的记录'; + $detail_json = json_encode($detail_json, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $save_data['detail_json'] = $detail_json; + + return (bool)OperatorLog::create($save_data); + } catch (\Exception $e) { // 如果发生错误,为避免中断主程,catch后记录错误信息即可 + Log::get('operator_log')->error('记录通用操作日志发生错误:' . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); + + return false; + } + } +} diff --git a/src/Service/PermissionService.php b/src/Service/PermissionService.php new file mode 100644 index 0000000..ecbf8c2 --- /dev/null +++ b/src/Service/PermissionService.php @@ -0,0 +1,392 @@ +getRouter('http'); + $data = $router->getData(); + $options = []; + foreach ($data as $routes_data) { + foreach ($routes_data as $http_method => $routes) { + $route_list = []; + if (isset($routes[0]['routeMap'])) { + foreach ($routes as $map) { + array_push($route_list, ...$map['routeMap']); + } + } else { + $route_list = $routes; + } + foreach ($route_list as $route => $v) { + // 过滤掉脚手架页面配置方法 + $callback = is_array($v) ? ($v[0]->callback) : $v->callback; + if (!is_array($callback)) { + continue; + } + $route = is_string($route) ? rtrim($route) : rtrim($v[0]->route); + $route_key = "$http_method::{$route}"; + $options[] = [ + 'value' => $route_key, + 'label' => $route_key, + ]; + } + } + } + + return $options; + } + + public function getRolePermissionValues($router_ids, $module = 'system') + { + if (empty($router_ids)) { + return []; + } + $data = []; + $routers = make(Menu::class)->tree([ + 'module' => $module, + 'id' => $router_ids, + ]); + if (!empty($routers)) { + $paths = array_keys(tree_2_paths($routers, $module)); + foreach ($paths as $path) { + $data[] = explode('-', $path); + } + } + + return $data; + } + + /** + * 构造角色权限设置options + * + * @return array + */ + public function getPermissionOptions($role_id = 0) + { + // todo 配置化 + $modules = make(CommonConfigService::class)->getValue('system', 'website_config')['system_module']; + + $options = []; + $values = []; + + $router_ids = $this->getRoleMenuIds([$role_id]); + + foreach ($modules as $item) { + $options[] = [ + 'value' => $item['name'], + 'label' => $item['label'], + 'children' => make(Menu::class)->tree(['module' => $item['name']]), + ]; + + $values = array_merge($values, $this->getRolePermissionValues($router_ids, $item['name'])); + } + + return [$values, $options]; + } + + public function getAllRoleList($where = [], $fields = ['*']) + { + $model = new Role(); + $roles = $model->where2query($where)->select($fields)->orderByRaw('pid asc, sort desc')->get(); + + return $roles->toArray(); + } + + public function getRoleMenuIds($role_ids) + { + if (empty($role_ids)) { + return []; + } + $routes = RoleMenu::query()->distinct(true)->select(['router_id'])->whereIn('role_id', $role_ids)->get()->toArray(); + + return $routes ? array_column($routes, 'router_id') : []; + } + + public function getRoleTree() + { + $roles = make(Role::class)->search([ + 'select' => ['id as value', 'name as label', 'pid'], + 'limit' => 200, + 'order_by' => 'sort desc', + ], [ + 'status' => YES, + ], 'name', 'id', 'and', true); + + return generate_tree($roles, 'pid', 'value'); + } + + public function getUserRoleIds($user_id) + { + if (!$user_id) { + return []; + } + + return UserRole::query()->select(['role_id'])->where('user_id', $user_id)->get()->pluck('role_id')->toArray(); + } + + public function getRoleUserIds($role_id) + { + if (!$role_id) { + return []; + } + + return UserRole::query()->select(['user_id'])->where('role_id', $role_id)->get()->pluck('user_id')->toArray(); + } + + public function getMenuRoleIds($menu_id) + { + if (!$menu_id) { + return []; + } + + return RoleMenu::query()->select(['role_id'])->where('router_id', $menu_id)->get()->pluck('role_id')->toArray(); + } + + public function getUserResource($user_id) + { + if (!$user_id) { + return []; + } + $user_role_ids = $this->getUserRoleIds($user_id); + $role_menu_ids = $this->getRoleMenuIds($user_role_ids); + $list = make(FrontRoutes::class)->where2query([ + 'id' => $role_menu_ids, + 'type' => ['>' => 0], + 'status' => YES, + ])->distinct(true)->select([ + 'http_method', + 'path', + 'is_scaffold', + 'permission', + 'scaffold_action', + ])->get()->toArray(); + $resources = []; + foreach ($list as $route) { + if (Str::contains($route['permission'], '::')) { + $permissions = array_filter(explode(',', $route['permission'])); + foreach ($permissions as $permission) { + [ + $http_method, + $uri, + ] = array_filter(explode('::', $permission, 2)); + $resources[] = [ + 'http_method' => $http_method, + 'uri' => $uri, + ]; + } + } else { + // 这段代码为兼容老的数据 + $paths = array_filter(explode('/', $route['path'])); + $suffix = array_pop($paths); + $prefix = implode('/', $paths); + if ($suffix == 'list') { + $action_conf = config("scaffold_permissions.list.permission"); + $scaffold_permissions = array_filter(explode(',', str_replace('/*/', "/{$prefix}/", $action_conf))); + foreach ($scaffold_permissions as $scaffold_permission) { + [ + $http_method, + $uri, + ] = array_filter(explode('::', $scaffold_permission, 2)); + $resources[] = [ + 'http_method' => $http_method, + 'uri' => $uri, + ]; + } + } + if (empty($route['permission'])) { + continue; + } + $resources[] = [ + 'http_method' => FrontRoutes::$http_methods[$route['http_method']], + 'uri' => $route['permission'], + ]; + } + } + $user_open_apis = $this->getOpenResourceList('user_open_api'); + $system_user_open = config('system.user_open_resource', ['/system/routes']); + return array_merge($resources, $user_open_apis, $system_user_open); + } + + public function getOpenResourceList($field = 'open_api') + { + $open_apis = CommonConfig::query()->where([ + 'namespace' => 'system', + 'name' => 'permissions', + ])->value('value')[$field] ?? []; + $data = []; + foreach ($open_apis as $route) { + [$http_method, $uri] = explode("::", $route, 2); + $data[] = compact('http_method', 'uri'); + } + + return $data; + } + + public function getResourceDispatcher($user_id = 0, $auth_type = FrontRoutes::RESOURCE_OPEN) + { + $cache_key = $this->getPermissionCacheKey($user_id); + $options = [ + 'routeParser' => 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', + 'routeCollector' => 'FastRoute\\RouteCollector', + ]; + if (!$dispatch_data = json_decode(Redis::get($cache_key), true)) { + /** @var RouteCollector $routeCollector */ + $route_collector = new $options['routeCollector'](new $options['routeParser'], new $options['dataGenerator']); + $this->processUserResource($route_collector, $user_id, $auth_type); + $dispatch_data = $route_collector->getData(); + if (!empty($dispatch_data)) { + Redis::setex($cache_key, DAY, json_encode($dispatch_data)); + } + } + + return new $options['dispatcher']($dispatch_data); + } + + protected function processUserResource(RouteCollector $r, $user_id, $auth_type) + { + $resources = $auth_type == FrontRoutes::RESOURCE_OPEN ? $this->getOpenResourceList() : $this->getUserResource($user_id); + $route_keys = []; + foreach ($resources as $resource) { + if (!isset($resource['uri']) || !$resource['uri']) { + continue; + } + $route_key = "{$resource['http_method']}::{$resource['uri']}"; + if (in_array($route_key, $route_keys)) { + continue; + } + $route_keys[] = $route_key; + $r->addRoute($resource['http_method'], $resource['uri'], ''); + } + } + + public function hasPermission($uri, $method = 'GET') + { + $auth_service = make(AuthService::class); + // 用户为超级管理员 + if ($auth_service->isSupperAdmin()) { + return true; + } + $user = $auth_service->user(); + $dispatcher = $this->getResourceDispatcher($user['id'] ?? 0, FrontRoutes::RESOURCE_NEED_AUTH); + $route_info = $dispatcher->dispatch($method, $uri); + + return $route_info[0] === $dispatcher::FOUND; + } + + public function isOpen($uri, $method) + { + $routes = container(DispatcherFactory::class)->getDispatcher('http')->dispatch($method, $uri); + if ($routes[0] !== Dispatcher::FOUND) { + return false; + } + if ($routes[1] instanceof Handler) { + if ($routes[1]->callback instanceof \Closure) { + return true; + } + [$controller, $action] = $this->prepareHandler($routes[1]->callback); + } else { + return false; + } + $controllerInstance = container($controller); + if (isset($controllerInstance->open_resources) && in_array($action, $controllerInstance->open_resources)) { + return true; + } + + // 获取开放的资源 + $dispatcher = $this->getResourceDispatcher(); + $route_info = $dispatcher->dispatch($method, $uri); + + return $route_info[0] === $dispatcher::FOUND; + } + + public function can($uri, $method) + { + if ($this->isOpen($uri, $method)) { + return true; + } + if ($this->hasPermission($uri, $method)) { + return true; + } + + return false; + } + + public function getPermissionCacheKey($user_id = 0, $force = false) + { + $cache_key = 'hyperf_admin_permission_cache:key_map'; + $new_key = "hyperf_admin_permission_cache:" . md5(time() . Str::random(6)); + $cache_value = !$force ? json_decode(Redis::get($cache_key), true) : []; + if (!isset($cache_value[$user_id])) { + $cache_value[$user_id] = $new_key; + Redis::set($cache_key, json_encode($cache_value)); + } + + return $cache_value[$user_id]; + } + + protected function prepareHandler($handler): array + { + if (is_string($handler)) { + if (strpos($handler, '@') !== false) { + return explode('@', $handler); + } + + return explode('::', $handler); + } + if (is_array($handler) && isset($handler[0], $handler[1])) { + return $handler; + } + throw new \RuntimeException('Handler not exist.'); + } + + public function getModules($user_id) + { + $role_ids = $this->getUserRoleIds($user_id); + if (!$role_ids) { + return []; + } + $route_ids = $this->getRoleMenuIds($role_ids); + if (!$route_ids) { + return []; + } + + return array_unique(FrontRoutes::query() + ->select(['module']) + ->whereIn('id', $route_ids) + ->get() + ->pluck('module') + ->toArray()); + } +} diff --git a/src/Service/UserService.php b/src/Service/UserService.php new file mode 100644 index 0000000..e8ef1b2 --- /dev/null +++ b/src/Service/UserService.php @@ -0,0 +1,79 @@ + (int)$filter]; + } else { + $where = $filter; + } + $user = make(User::class)->where2query($where)->select([ + 'id', + 'username', + 'realname', + 'mobile', + 'email', + 'status', + 'is_admin', + 'avatar', + 'roles', + ])->first(); + if (!$user) { + return false; + } + + return $user->toArray(); + } + + public function batchGetUser(array $ids, $status = YES) + { + $users = User::query()->whereIn('id', $ids)->where('status', $status)->select([ + 'id', + 'username', + 'realname', + 'mobile', + 'email', + 'status', + 'is_admin', + 'avatar', + 'roles', + ])->get(); + if (!$users) { + return false; + } + + return $users->toArray(); + } + + public function findUserOrCreate($sso_user_info) + { + $where = empty($sso_user_info['mobile']) ? [ + 'username' => $sso_user_info['name'], + ] : [ + 'username' => [$sso_user_info['name'], $sso_user_info['mobile']], + ]; + $user_info = $this->getUser($where); + if ($user_info) { + return $user_info; + } + $data = [ + 'username' => $sso_user_info['mobile'], + 'mobile' => $sso_user_info['mobile'], + 'avatar' => $sso_user_info['avatar'] ?? '', + 'password' => '', + 'status' => YES, + 'realname' => $sso_user_info['name'] ?? '', + 'login_time' => date("Y-m-d H:i:s"), + 'login_ip' => '', + ]; + $user = new User($data); + $user->save(); + + return $user->toArray(); + } +} diff --git a/src/config/config.php b/src/config/config.php new file mode 100644 index 0000000..23fd4f3 --- /dev/null +++ b/src/config/config.php @@ -0,0 +1,59 @@ + [ + 'hyperf_admin' => db_complete([ + 'host' => env('HYPERF_ADMIN_DB_HOST'), + 'port' => env('HYPERF_ADMIN_DB_PORT', 3306), + 'database' => env('HYPERF_ADMIN_DB_NAME', 'hyperf_admin'), + 'username' => env('HYPERF_ADMIN_DB_USER'), + 'password' => env('HYPERF_ADMIN_DB_PWD'), + 'prefix' => env('HYPERF_ADMIN_DB_PREFIX'), + ]), + ], + 'init_routes' => [ + __DIR__ . '/routes.php' + ], + 'user_info_cache_prefix' => 'hyperf_admin_userinfo_', + 'admin_cookie_name' => 'hyperf_admin_token', + // 脚手架预置权限 + 'scaffold_permissions' => [ + 'list' => [ + 'label' => '列表', + 'type' => 2, + 'permission' => 'GET::/*/info,GET::/*/list,GET::/*/list.json,GET::/*/childs/{id:\d+},GET::/*/act,GET::/*/notice', + ], + 'create' =>[ + 'label' => '新建', + 'path' => '/form', + 'type' => 1, + 'permission' =>'GET::/*/form,GET::/*/form.json,POST::/*/form', + ], + 'edit' =>[ + 'label' => '编辑', + 'path' => '/:id', + 'type' => 1, + 'permission' => 'GET::/*/{id:\d+},GET::/*/{id:\d+}.json,POST::/*/{id:\d+},GET::/*/newversion/{id:\d+}/{last_ver_id:\d+}', + ], + 'rowchange' =>[ + 'label' => '行编辑', + 'type' => 2, + 'permission' => 'POST::/*/rowchange/{id:\d+}' + ], + 'delete' => [ + 'label' => '删除', + 'type' => 2, + 'permission' => 'POST::/*/delete', + ], + 'import' => [ + 'label' => '导入', + 'type' => 2, + 'permission' => 'POST::/*/import', + ], + 'export' => [ + 'label' => '导出', + 'type' => 2, + 'permission' => 'POST::/*/export', + ], + ], +]; diff --git a/src/config/routes.php b/src/config/routes.php new file mode 100644 index 0000000..f4fcbe9 --- /dev/null +++ b/src/config/routes.php @@ -0,0 +1,47 @@ +write($model, $action, $ids, $remark, $options, $user_id); + } +} + +if (! function_exists('auth')) { + function auth(?string $field = null) + { + $auth = container(AuthService::class); + if (is_null($field)) { + return $auth; + } + return $auth->get($field); + } +} + +if (! function_exists('permission')) { + function permission(?string $uri = null, string $method = 'POST') + { + $permission = make(PermissionService::class); + if (is_null($uri)) { + return $permission; + } + return $permission->can($uri, strtoupper($method)); + } +}