--- url: /v3/guide/introduce/contact.md --- # 🗣️ 联系我们 ## 💬 交流群组 ### QQ群 * **官方交流群**: 150105478 ### Discord * **官方Discord**: [加入我们的Discord服务器](https://discord.gg/9jEyY6SC) * **频道介绍**: 国际用户交流、技术讨论、问题解答 ### 微信群 * **微信交流群**: 请添加管理员微信,邀请加入 * **管理员微信**: `onloker_life` `anmyt-m` ## 📧 联系方式 ### 项目负责人 #### 主要维护者 * **GitHub**: [@kanyxmo](https://github.com/kanyxmo) * **邮箱**: 261091613@qq.com * **GitHub**: [@zds-s](https://github.com/zds-s) * **邮箱**: 2771717608@qq.com ### 所有贡献者 ## 🌐 社交媒体 ### 官方账号 * **GitHub**: [mineadmin/mineadmin](https://github.com/mineadmin/mineadmin) * **Gitee**: [mineadmin/mineadmin](https://gitee.com/mineadmin/mineadmin) * **官方网站**: ## 📋 如何获得帮助 ### 1. 常见问题 首先查看我们的 [FAQ 页面](/v3/faq/),大部分常见问题都能在这里找到答案。 ### 2. 文档搜索 使用文档右上角的搜索功能,快速找到相关信息。 ### 3. GitHub Issues 对于 Bug 报告和功能请求,请在 [GitHub Issues](https://github.com/mineadmin/mineadmin/issues) 中提交。 ### 4. 讨论区 加入我们的讨论群,与其他开发者交流经验: * QQ群:150105478 * Discord:[点击加入](https://discord.gg/9jEyY6SC) ## 📝 反馈建议 我们非常重视用户的反馈和建议,您可以通过以下方式向我们提供: ### 产品改进建议 * **功能请求**: [GitHub Feature Request](https://github.com/mineadmin/mineadmin/issues/new) ### 文档改进 * **文档问题**: [文档仓库 Issues](https://github.com/mineadmin/doc-v3/issues) ## ⏰ 响应时间 ### 一般询问 * **QQ群**: 实时响应,最迟12小时内响应 * **Discord**: 24小时内响应 * **邮件**: 1-2个工作日内响应 ### 紧急问题 * **安全漏洞**: 2771717608@qq.com(24小时内响应) *** --- --- url: /v3/plugin/api.md --- # API 参考文档 本文档详细介绍 MineAdmin 插件系统的所有 API 接口、命令行工具和核心类库。 ## 命令行 API ### 插件管理命令 #### 1. mine-extension:initial 初始化插件扩展系统。 ```bash php bin/hyperf.php mine-extension:initial ``` **功能**: * 发布 app-store 配置文件 * 初始化插件系统配置 * 创建必要的目录结构 **实现类**: `Mine\AppStore\Command\InitialCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InitialCommand.php)) #### 2. mine-extension:list 查询远程插件列表。 ```bash php bin/hyperf.php mine-extension:list [options] ``` **参数**: | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | --type | string | all | 筛选扩展类型 (mixed/backend/frontend) | | --name | string | - | 筛选扩展名称 | | --category | string | - | 筛选分类 | | --author | string | - | 筛选作者 | **示例**: ```bash # 查看所有插件 php bin/hyperf.php mine-extension:list # 查看混合型插件 php bin/hyperf.php mine-extension:list --type=mixed # 搜索特定插件 php bin/hyperf.php mine-extension:list --name=user-manager ``` **实现类**: `Mine\AppStore\Command\ListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/ListCommand.php)) #### 3. mine-extension:local-list 查询本地所有插件。 ```bash php bin/hyperf.php mine-extension:local-list [options] ``` **参数**: | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | --status | string | all | 筛选状态 (installed/enabled/disabled) | | --type | string | all | 筛选类型 | **实现类**: `Mine\AppStore\Command\LocalListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/LocalListCommand.php)) #### 4. mine-extension:download 下载远程插件到本地。 ```bash php bin/hyperf.php mine-extension:download --name=plugin-name [options] ``` **参数**: | 参数 | 类型 | 必选 | 说明 | |------|------|------|------| | --name | string | 是 | 插件名称 | | --version | string | 否 | 指定版本 | | --force | bool | 否 | 强制覆盖已存在的插件 | **实现类**: `Mine\AppStore\Command\DownloadCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/DownloadCommand.php)) #### 5. mine-extension:install 安装指定插件。 ```bash php bin/hyperf.php mine-extension:install {path} [options] ``` **参数**: | 参数 | 类型 | 必选 | 说明 | |------|------|------|------| | path | string | 是 | 插件路径 (vendor/plugin-name) | | --yes | bool | 否 | 跳过确认提示 | | --force | bool | 否 | 强制重新安装 | | --skip-dependencies | bool | 否 | 跳过依赖检查 | **示例**: ```bash # 安装插件 php bin/hyperf.php mine-extension:install mineadmin/user-manager --yes # 强制重新安装 php bin/hyperf.php mine-extension:install mineadmin/user-manager --force ``` **实现类**: `Mine\AppStore\Command\InstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InstallCommand.php)) #### 6. mine-extension:uninstall 卸载指定插件。 ```bash php bin/hyperf.php mine-extension:uninstall {path} [options] ``` **参数**: | 参数 | 类型 | 必选 | 说明 | |------|------|------|------| | path | string | 是 | 插件路径 | | --yes | bool | 否 | 跳过确认提示 | | --force | bool | 否 | 强制卸载 (忽略错误) | | --keep-data | bool | 否 | 保留用户数据 | **实现类**: `Mine\AppStore\Command\UninstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/UninstallCommand.php)) #### 7. mine-extension:create 创建新插件。 ```bash php bin/hyperf.php mine-extension:create {path} [options] ``` **参数**: | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | path | string | - | 插件路径 (vendor/plugin-name) | | --name | string | example | 插件显示名称 | | --type | string | mixed | 插件类型 (mixed/backend/frontend) | | --author | string | - | 作者名称 | | --description | string | - | 插件描述 | | --license | string | MIT | 许可证类型 | **示例**: ```bash php bin/hyperf.php mine-extension:create mycompany/hello-world \ --name "Hello World" \ --type mixed \ --author "Your Name" \ --description "我的第一个插件" ``` **实现类**: `Mine\AppStore\Command\CreateCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/CreateCommand.php)) ## 核心类库 API ### Plugin 类 **文件位置**: `Mine\AppStore\Plugin` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Plugin.php)) 插件系统的核心类,负责插件的加载和管理。 #### Plugin::init() 初始化插件系统,在应用启动时调用。 ```php [ 'name' => 'vendor/plugin-name', 'version' => '1.0.0', 'path' => '/path/to/plugin', 'config' => [...], // mine.json 配置 'status' => 'enabled' ] ] ``` #### Plugin::isInstalled() 检查插件是否已安装。 ```php install('vendor/plugin-name', [ 'force' => false, 'skip_dependencies' => false ]); if ($result['success']) { echo "安装成功"; } else { echo "安装失败: " . $result['message']; } ``` #### uninstall() 卸载插件。 ```php uninstall('vendor/plugin-name', [ 'force' => false, 'keep_data' => false ]); ``` #### update() 更新插件。 ```php update('vendor/plugin-name'); ``` ### ConfigProvider 基类 所有插件的 ConfigProvider 都应该遵循以下接口: ```php [ InterfaceA::class => ImplementationA::class, ], // 注解扫描路径 'annotations' => [ 'scan' => [ 'paths' => [ __DIR__, ], ], ], // 命令行命令 'commands' => [ CustomCommand::class, ], // 事件监听器 'listeners' => [ CustomListener::class, ], // 中间件 'middlewares' => [ 'http' => [ CustomMiddleware::class, ], ], // 配置文件发布 'publish' => [ [ 'id' => 'config-id', 'description' => '配置文件描述', 'source' => __DIR__ . '/../publish/config.php', 'destination' => BASE_PATH . '/config/autoload/plugin.php', ], ], // 进程配置 'processes' => [ CustomProcess::class, ], ]; } } ``` ## HTTP API ### 插件管理接口 #### 获取插件列表 ```http GET /admin/plugin/list ``` **请求参数**: ```json { "page": 1, "pageSize": 15, "type": "mixed", "status": "enabled", "keyword": "search term" } ``` **响应示例**: ```json { "code": 200, "message": "success", "data": { "list": [ { "name": "vendor/plugin-name", "display_name": "插件显示名称", "version": "1.0.0", "description": "插件描述", "author": "作者名称", "type": "mixed", "status": "enabled", "installed_at": "2024-01-01 12:00:00", "updated_at": "2024-01-15 10:30:00" } ], "total": 1 } } ``` #### 安装插件 ```http POST /admin/plugin/install ``` **请求参数**: ```json { "name": "vendor/plugin-name", "version": "1.0.0", "force": false } ``` **响应示例**: ```json { "code": 200, "message": "安装成功", "data": { "plugin": "vendor/plugin-name", "version": "1.0.0", "installed_at": "2024-01-01 12:00:00" } } ``` #### 卸载插件 ```http DELETE /admin/plugin/uninstall ``` **请求参数**: ```json { "name": "vendor/plugin-name", "keep_data": false } ``` #### 启用/禁用插件 ```http PUT /admin/plugin/toggle-status ``` **请求参数**: ```json { "name": "vendor/plugin-name", "status": "enabled" // enabled | disabled } ``` ## 事件 API ### 插件事件系统 插件系统提供了丰富的事件钩子,允许开发者在插件生命周期的关键节点执行自定义逻辑。 #### 事件类型 ```php success) { // 插件安装成功后的处理 $this->clearCache(); $this->sendNotification($event->pluginName); $this->updateStatistics($event->pluginName); } else { // 安装失败的处理 logger()->error('插件安装失败', [ 'plugin' => $event->pluginName, 'error' => $event->error ]); } } } ``` ## 钩子 API ### 插件钩子系统 MineAdmin 提供了钩子系统,允许插件在系统关键点注入自定义逻辑。 #### 注册钩子 ```php info('用户尝试登录', ['user_id' => $user->id]); }); HookManager::register('user.login.after', function($user) { // 用户登录后的处理逻辑 $this->recordLoginHistory($user); }); return [ // ... 其他配置 ]; } } ``` #### 触发钩子 ```php authenticate($credentials); if ($result) { // 登录后钩子 HookManager::trigger('user.login.after', $user); } return $result; } } ``` #### 可用钩子列表 | 钩子名称 | 触发时机 | 参数 | |----------|----------|------| | `user.login.before` | 用户登录前 | User $user | | `user.login.after` | 用户登录后 | User $user | | `user.logout.before` | 用户退出前 | User $user | | `user.logout.after` | 用户退出后 | User $user | | `menu.render.before` | 菜单渲染前 | array $menus | | `menu.render.after` | 菜单渲染后 | array $menus | | `permission.check.before` | 权限检查前 | string $permission, User $user | | `permission.check.after` | 权限检查后 | bool $result, string $permission, User $user | ## 工具类 API ### PluginHelper 类 提供插件开发的常用工具方法。 ```php `访问单个选项卡项的内容,使开发者能够自定义选项卡项的显示方式。 ([6e95a9b](https://github.com/mineadmin/mineadmin/commit/6e95a9b82f5c55b9e24f3e464505b9c33eda6958)) * feat(tab组件):新增加了垂直方向选项,使选项卡可以垂直对齐。通过在选项卡组件中引入 'direction' 属性,用户现在可以选择 选项卡是水平对齐还是垂直对齐,从而提高了用户界面的灵活性。 ([db0dff9](https://github.com/mineadmin/mineadmin/commit/db0dff9c734ff47d71214289173065aba155e106)) * feature(web): 增加用于 web 系统的流程脚本 ([16ee204](https://github.com/mineadmin/mineadmin/commit/16ee204ca054388959918d4ebdc3843ae0bab81a)) * feature(web): 增加前端代码 ([ccae081](https://github.com/mineadmin/mineadmin/commit/ccae0817763233422e7ee8bcd047c7c306c0652e)) * feature(upload): 增加资源,资源列表,删除资源接口 ([#338](https://github.com/mineadmin/mineadmin/pull/338)) ([854d393](https://github.com/mineadmin/mineadmin/commit/854d393298a2c4c7558570849afc1de42bddff5f)) * feature(config): 增加配置、分组配置API ([#333](https://github.com/mineadmin/mineadmin/pull/333)) ([945d1ad](https://github.com/mineadmin/mineadmin/commit/945d1ad0c7138f246514633be694e023d3e6ec8c)) * feature(permission): 增加用于用户获取权限的API以及相关单元测试 ([#331](https://github.com/mineadmin/mineadmin/pull/331)) ([0ee8e7e](https://github.com/mineadmin/mineadmin/commit/0ee8e7eabed234d54054ed6b6f90aae0085a33e2)) * feature(role): 新增角色权限赋予API ([#329](https://github.com/mineadmin/mineadmin/pull/329)) ([470d98c](https://github.com/mineadmin/mineadmin/commit/470d98c0bbae7fda1feccc1747bb973fb13ccf73)) * feature(actions): 增加对Swow的测试支持 ([#328](https://github.com/mineadmin/mineadmin/pull/328)) ([f49ffd0](https://github.com/mineadmin/mineadmin/commit/f49ffd011754b157205727d618d3c0e005ffe7a7)) * feature(post): 增加岗位管理API ([#327](https://github.com/mineadmin/mineadmin/pull/327)) ([8a04d81](https://github.com/mineadmin/mineadmin/commit/8a04d810cf778c713064bdebaca22147019c86ce)) * feature(dept): 增加部门管理API ([#326](https://github.com/mineadmin/mineadmin/pull/326)) ([eb88889](https://github.com/mineadmin/mineadmin/commit/eb88889d3f867676fe6226d9446f38be3b3abb8e)) * feature(menu): 添加菜单管理api ([#325](https://github.com/mineadmin/mineadmin/pull/325)) ([8552619](https://github.com/mineadmin/mineadmin/commit/855261908744dc33629260587c8e1622de7ec07c)) * feature(role): 增加权限管理API ([#323](https://github.com/mineadmin/mineadmin/pull/323)) ([ec6d72a](https://github.com/mineadmin/mineadmin/commit/ec6d72a172c2db5ef30d5233bc324657371e1d1e)) * feature(user): 增加用户管理api ([9e704c8](https://github.com/mineadmin/mineadmin/commit/9e704c81796b25598f7f8a581ba21dd56addb3cc)) * feature(menu): 增加用户菜单列表接口 ([10136d2](https://github.com/mineadmin/mineadmin/commit/10136d27234274471410ed55b5a4b65e6b0c9c9d)) * feature(passport): 必应背景图测试用例补充 ([a4db968](https://github.com/mineadmin/mineadmin/commit/a4db96886cb4fd6baeac5feda06d481ec5e8c459)) * feature(passport): 完善刷新 token 接口 ([d2544be](https://github.com/mineadmin/mineadmin/commit/d2544bef225d39c73c59f947f57f0e025a7a1b66)) * feature(passport): 完善我的信息接口 ([62a8558](https://github.com/mineadmin/mineadmin/commit/62a8558776eda578bc397260bdd6a9611cff2f16)) * feature(passport): 完善退出接口 ([f625169](https://github.com/mineadmin/mineadmin/commit/f6251693f0115b3f46b7a8994271aa68818935ab)) * feature(swagger): 完善 swagger ([175d66e](https://github.com/mineadmin/mineadmin/commit/175d66e4dc83ef1ebd37973f28fa9eb722ba33b9)) * feature(passport): 完善登录接口+测试用例 ([938fee6](https://github.com/mineadmin/mineadmin/commit/938fee6f0c911b8863aaacab81878cc83d08caa4)) * feature(passport): 登录,获取背景图 ([f29e719](https://github.com/mineadmin/mineadmin/commit/f29e719f4129009500b7dbee0abe0945b8ee3a8d)) * feature(swagger): 增加 swagger 配置+文档,暂时将旧代码移植到 tmp 目录下 ([a572f22](https://github.com/mineadmin/mineadmin/commit/a572f22a1abf96a7797935c9880e5b0825d25793)) * feat(databases): 移除未使用的模块并清理 seeders 和 migrations 文件 ([3aa4982](https://github.com/mineadmin/mineadmin/commit/3aa49827aa5a91f97686a40b08dc2281f6e29d04)) * feat(attachment): 将 'uploadfile' 重命名为 'attachment' 并更新相关组件 ([08ae915](https://github.com/mineadmin/mineadmin/commit/08ae915cec133be2ef06032628894b41d00a8ab7)) * feat(hyperf/helper): 引入 hyperf/helper 的全局函数 移除了use function 方式 ([63dbfda](https://github.com/mineadmin/mineadmin/commit/63dbfda7446fd268f66369572ddc58e3b07c367b)) * feature(composer): 引入 hyperf/helper ([e4b1a36](https://github.com/mineadmin/mineadmin/commit/e4b1a360ea8644e130fb6cf60c1384e3337a5de7)) * feat(swagger): 新增 Swagger 配置文件 ([3aaf765](https://github.com/mineadmin/mineadmin/commit/3aaf76545b5a718732244ca90df7a64e1c52c697)) * feat(setting\_config): improve encoding & seeding for config\_select\_data ([4d74468](https://github.com/mineadmin/mineadmin/commit/4d744689c937d78d37b6416a0081f5dfa2c078cc)) ### 🐛 Bug Fixes * fix(seeder): 修复执行php-cs-fixer造成seeder文件类名错误的问题 ([#476](https://github.com/mineadmin/mineadmin/pull/476)) ([f368ec1](https://github.com/mineadmin/mineadmin/commit/f368ec1ae0f93c823d6f17a85eb71515790e09b7)) * fix(php-cs) ([#475](https://github.com/mineadmin/mineadmin/pull/475)) ([e380d78](https://github.com/mineadmin/mineadmin/commit/e380d7868a82228f1bf4e2c332e5eb25d519685c)) * fix(layout): 修复布局与iframe页面问题 ([#469](https://github.com/mineadmin/mineadmin/pull/469)) ([74ed80a](https://github.com/mineadmin/mineadmin/commit/74ed80a9270c47e40add28390fe121972e19a93f)) * fix:(menu): 修复提示信息描述不准确 ([#468](https://github.com/mineadmin/mineadmin/pull/468)) ([24b08c1](https://github.com/mineadmin/mineadmin/commit/24b08c17b010adc61a017cff3d4f2400d7ac4472)) * fix(pro-table): 修复`requestPage`设置`size`参数无效的bug ([#467](https://github.com/mineadmin/mineadmin/pull/467)) ([28a028f](https://github.com/mineadmin/mineadmin/commit/28a028f9559e66046fb3c85b9e1a602fb312bb6f)) * fix(pro-table): 修复单元格插件注册后调用无效的问题 ([#466](https://github.com/mineadmin/mineadmin/pull/466)) ([9290f22](https://github.com/mineadmin/mineadmin/commit/9290f22b0fbe7630d6dc7d4a90004a200e903748)) * fix(front-permission): 修复前端权限检查时如果值为空对象时:{},进入判断条件,导致显示无权限 ([#463](https://github.com/mineadmin/mineadmin/pull/463)) ([4f11da1](https://github.com/mineadmin/mineadmin/commit/4f11da1fd6be88776c2e2f585432bd5a8b084dd9)) * fix(welcomePage): 修复路由添加 welcomePage 时,自定义数据未覆盖默认数据 ([#458](https://github.com/mineadmin/mineadmin/pull/458)) ([7331b5f](https://github.com/mineadmin/mineadmin/commit/7331b5fe3128c5290af38249c80ed4c22ab860db)) * fix(cs-fix): fix cs-fix error ([#453](https://github.com/mineadmin/mineadmin/pull/453)) ([d742aa0](https://github.com/mineadmin/mineadmin/commit/d742aa026cfd01400e205beb436336f4b1b2cc0b)) * fix(analyse): fix analyse error ([#452](https://github.com/mineadmin/mineadmin/pull/452)) ([30644a8](https://github.com/mineadmin/mineadmin/commit/30644a8e3af91ed7f4266efebad6fc4362255e62)) * fix(vite-config): 未添加 `base` 参数,导致`VITE_APP_ROOT_BASE` 无效 ([#448](https://github.com/mineadmin/mineadmin/pull/448)) ([618bb66](https://github.com/mineadmin/mineadmin/commit/618bb665b18fb75fca986f17fb5196e142fe6443)) * fix(bug): 修复添加顶级菜单按钮未初始化id,修复应用商店打开官网链接插件详情页404,优化应用商店图片显示 ([#444](https://github.com/mineadmin/mineadmin/pull/444)) ([2589a7d](https://github.com/mineadmin/mineadmin/commit/2589a7de9b46c52d4f9764808ca55e3e9ef59984)) * fix(main-aside): 修复分栏模式下,菜单激活问题 ([#443](https://github.com/mineadmin/mineadmin/pull/443)) ([6def465](https://github.com/mineadmin/mineadmin/commit/6def4653ae2a08cd341ee8987877768c4d633fb5)) * fix:修增菜单含三级或以上的情况下只有一级菜单有选中样式 ([#439](https://github.com/mineadmin/mineadmin/pull/439)) ([2548a1e](https://github.com/mineadmin/mineadmin/commit/2548a1ec97f42674aa0805a098d0fe5f0147de71)) * fix(menu-btn-permission): 修复菜单按钮列表为空时,未清楚的问题 ([#433](https://github.com/mineadmin/mineadmin/pull/433)) ([94c7ded](https://github.com/mineadmin/mineadmin/commit/94c7dedba7e7134d155348a8f41c1367c4777dd0)) * fix(cs-fix): fix 语法 ([#427](https://github.com/mineadmin/mineadmin/pull/427)) ([a6d86a4](https://github.com/mineadmin/mineadmin/commit/a6d86a435de141a90e197867148ccc55b13de265)) * fix(menu): 修复菜单使用bug ([#426](https://github.com/mineadmin/mineadmin/pull/426)) ([8eef50d](https://github.com/mineadmin/mineadmin/commit/8eef50df68c566ac72506466aea71dc56b66a84a)) * fix(menu): 修复编辑类型为M的菜单时,按钮权限列表未回显 ([#424](https://github.com/mineadmin/mineadmin/pull/424)) ([d38a8d3](https://github.com/mineadmin/mineadmin/commit/d38a8d38af6ae357c064465135e4519b15804bfd)) * fix:资源选择器新增删除方法,修复多语言问题 ([#422](https://github.com/mineadmin/mineadmin/pull/422)) ([cf49390](https://github.com/mineadmin/mineadmin/commit/cf49390d9e5b900a39b707da756aa59fbca5f868)) * fix(menu): 拼写错误 ([#421](https://github.com/mineadmin/mineadmin/pull/421)) ([0f7e101](https://github.com/mineadmin/mineadmin/commit/0f7e101f09c0aaafcaf088df0c5e258814ead2b1)) * fix(pro-table, setPermissionForm): 升级pro-table修复classList.add报错bug,修复勾选权限严格模式未生效问题 ([#408](https://github.com/mineadmin/mineadmin/pull/408)) ([97d3a60](https://github.com/mineadmin/mineadmin/commit/97d3a60187f9cabc6fe38a8f5226f7b0b76b6b01)) * fix: 修复顶级菜单无法被添加的问题 ([#407](https://github.com/mineadmin/mineadmin/pull/407)) ([334c619](https://github.com/mineadmin/mineadmin/commit/334c619c86170f17c01718822ee2dc004fcaf820)) * fix(roleCode): code error ([#401](https://github.com/mineadmin/mineadmin/pull/401)) ([9a970b1](https://github.com/mineadmin/mineadmin/commit/9a970b119879c0dc146e80f0752df9591e5df13f)) * fix(watcher, usePluginStore): 移除监听 api 目录, 修复usePluginStore 类型报错问题 ([#395](https://github.com/mineadmin/mineadmin/pull/395)) ([44ce6e3](https://github.com/mineadmin/mineadmin/commit/44ce6e3a7fa99c265655f219b353252bdd8d4fb2)) * fix(前端类型错误): 修复前端插件类型定义问题以及usePluginStore部分函数返回值类型错误问题 ([#382](https://github.com/mineadmin/mineadmin/pull/382)) ([807da0e](https://github.com/mineadmin/mineadmin/commit/807da0e83f5a295d8c34452ee989b3bd4a82545c)) * fix(app): stop propagation on mode not found exception ([#375](https://github.com/mineadmin/mineadmin/pull/375)) ([664d757](https://github.com/mineadmin/mineadmin/commit/664d75783ee03ce127178eec72546b9defbcea6b)) * fix(修复菜单新增和编辑逻辑错误) ([#379](https://github.com/mineadmin/mineadmin/pull/379)) ([a140517](https://github.com/mineadmin/mineadmin/commit/a140517c11de756138585d9414cd257349c664b2)) * fix(水印) ([38ad110](https://github.com/mineadmin/mineadmin/commit/38ad11096229af8e760c6cd7def3fa2b59d06940)) * fix(menu、table): 修复菜单新增可一直点击,优化表结构,修复菜单错误提示未翻译的问题 ([8ac3676](https://github.com/mineadmin/mineadmin/commit/8ac367624f13fc4a57bc3b1991a9b1e083fcc237)) * fix(refresh\_token): 修复刷新token也失效的情况下,导致一直在加载页面转圈 ([6dc7519](https://github.com/mineadmin/mineadmin/commit/6dc7519b2dffa0812c8580240a33f1f6e876de88)) * fix(获取用户信息失败后未跳转登录页问题) ([9cc5bfa](https://github.com/mineadmin/mineadmin/commit/9cc5bfa7351b3100262ebaccc171bd6a51a5e184)) * fix(修复意外引入element-plus图标) ([724479a](https://github.com/mineadmin/mineadmin/commit/724479ad6936a554aec32d99d829d2249da6701e)) * fix(数据返回类型) ([6946606](https://github.com/mineadmin/mineadmin/commit/6946606d84d1552a99345a817b1a2aee2f89f8c6)) * fix(login):默认账号更改为admin,适配后端 ([7182398](https://github.com/mineadmin/mineadmin/commit/71823983ef77dad18c443c34594084c1652fb31c)) * fix(admin): handle null user and optimize menu query ([d07c4ed](https://github.com/mineadmin/mineadmin/commit/d07c4ed57e1efb18b66055256f46e86c179c18e3)) * fix(login):修复蹼泳用户获取后台设置问题导致退出问题 ([e3f70ac](https://github.com/mineadmin/mineadmin/commit/e3f70ac04b67b2db48cfb36117bd9a7468924d86)) * fix(mixed layout):修复混合布局无子级菜单仍显示子侧边栏bug ([f34bf2b](https://github.com/mineadmin/mineadmin/commit/f34bf2b97005de2872433e0c498074bd28dd95e9)) * fix(添加顶级菜单报错bug) ([4d50841](https://github.com/mineadmin/mineadmin/commit/4d50841e61b296d5f79e706b3885c60c133a2cf1)) * fix(修复颜色模式偶尔刷新下,el组件颜色显示不对的问题) ([a2fa06c](https://github.com/mineadmin/mineadmin/commit/a2fa06c8fdd178a9560106a6b0bc632c1fcaa527)) * refactor(exception): use match expression in JwtExceptionHandler ([e20f8d6](https://github.com/mineadmin/mineadmin/commit/e20f8d6e398898d3205dee590451d8103ed9169f)) * fix(修改密码后,关闭弹窗) ([7d4f0ff](https://github.com/mineadmin/mineadmin/commit/7d4f0ffb83dd83a509c52374d73c64042801526b)) * fix(用户中心修改密码) ([06ee54e](https://github.com/mineadmin/mineadmin/commit/06ee54ef389a30e0e3adcacf6aa4e92d9473b8b1)) * fix(m-button组件loading状态下未被禁用的bug,修复登录失败,按钮未恢复正常状态问题) ([3a124bf](https://github.com/mineadmin/mineadmin/commit/3a124bf77834f1261dea1c1767e2551a240eb47a)) * fix(修复前端超管判断逻辑) ([7ab13d9](https://github.com/mineadmin/mineadmin/commit/7ab13d907844c0051c2d74abdeb5497af065fbe2)) * fix(修复会员中心) ([3f877b8](https://github.com/mineadmin/mineadmin/commit/3f877b8f662eeef4428361b5d907862587204f7c)) * fix(修复ma-upload-image组件调用资源选择器未更新v-model的bug) ([d70c92b](https://github.com/mineadmin/mineadmin/commit/d70c92b307d8043e9e364b5dc114ce89fc2a1d7f)) * fixed phpunit ([4bfe14f](https://github.com/mineadmin/mineadmin/commit/4bfe14fba2d4bbe47fdc6433569ffaaefd2525c3)) * fixed phpstan ([ae0787f](https://github.com/mineadmin/mineadmin/commit/ae0787f7b3999b44a7f82ff3724458f4d9103c9f)) * fix(layout) ([d6794a1](https://github.com/mineadmin/mineadmin/commit/d6794a1bbe65203d833c5d5374c2698cb4486bab)) * fix(面包屑bug) ([322839d](https://github.com/mineadmin/mineadmin/commit/322839d3da5c1a6c4101ff2ccc7b84ff80bf1531)) * fix(tab refresh): 修复tab刷新bug ([6a05388](https://github.com/mineadmin/mineadmin/commit/6a053881208a46abb9c1de22006e4b4d7d917d2a)) * fix(role bind menus) ([175b986](https://github.com/mineadmin/mineadmin/commit/175b98680464ae8bbd2b0763a86739fb46981689)) * fix(menuSeeder) ([82811c2](https://github.com/mineadmin/mineadmin/commit/82811c22a8103e068c01cc3df9bc4d509a7c6951)) * fix(优化) ([406391d](https://github.com/mineadmin/mineadmin/commit/406391d0093615cd63c9c7a3020c010334e822a7)) * fix(角色状态错误) ([3bac227](https://github.com/mineadmin/mineadmin/commit/3bac227efdf517b60e3b6ae207a03a72b042f188)) * fix(用户crud) ([134098b](https://github.com/mineadmin/mineadmin/commit/134098bd979b7612a8ff7c19f049169cb6daed96)) * fixed: 修复 refresh token 中间件验证问题 ([acb35cc](https://github.com/mineadmin/mineadmin/commit/acb35cc752921cdb1e5e2c56b184eafa0a00f0f4)) * fix(remove log icon): 移除菜单填充里的操作之日按钮权限的图标 ([fff948d](https://github.com/mineadmin/mineadmin/commit/fff948d4b341f946acada5301a519baa16d646e3)) * fix(public\_url): 错误的问题 ([94510a1](https://github.com/mineadmin/mineadmin/commit/94510a1c2866e71bd606ca419433a6765b4dc669)) * fix(sql打印): substr\_replace导致的位置替换有问题 ([82d2d1e](https://github.com/mineadmin/mineadmin/commit/82d2d1e2c6d8755551b31fe23e8f960f7d27dc64)) * fix(http): 修复前端接管服务器返回错误的处理 ([10e17f2](https://github.com/mineadmin/mineadmin/commit/10e17f2a7c44380679011c4acd78e211b8ed2091)) * fix(web): 修复字体引用 src 属性错误 ([a03995f](https://github.com/mineadmin/mineadmin/commit/a03995ffb42a42750d47c7d5632121a97a362c07)) * fix(menu): 菜单填充数据修复,多语言key修复 ([f0e8273](https://github.com/mineadmin/mineadmin/commit/f0e82739396b88fecb35a2f536cea8f4e688f012)) * fixed(jwt auth): 收敛用户事件到 jwt 组件中 ([81231e1](https://github.com/mineadmin/mineadmin/commit/81231e1c2d5a57cd47b8dc3f5f1a8d139dd3ee09)) * fixed(login event): error class name ([81707b2](https://github.com/mineadmin/mineadmin/commit/81707b2aab3641e2bf3713cf92f6bce0fed6d182)) * fix(eslint去掉import sort规则) ([51853a2](https://github.com/mineadmin/mineadmin/commit/51853a2925f1989d30f1f2a4291f72de8a5ed57f)) * fix(更新pro-table):修复pro-table搜索设置不显示时,但外容器还显示的问题 ([ccfb0a2](https://github.com/mineadmin/mineadmin/commit/ccfb0a298e85448fd1f959cc0a23938f53abcef7)) * fix(seeder) ([25d00f4](https://github.com/mineadmin/mineadmin/commit/25d00f4b44c9ed7ef421a2162fba77afc223c27c)) * fix(修复bug) ([1217685](https://github.com/mineadmin/mineadmin/commit/1217685884178bd3e2e979b68249a30eb075ad6f)) * fixed result response ([381bc19](https://github.com/mineadmin/mineadmin/commit/381bc19455f23576c4211919f9c5ec35049a84ac)) * fix(ResultResponse): 🐛 在解析器中实现对字符串实例化的支持 ([35f23b6](https://github.com/mineadmin/mineadmin/commit/35f23b61f8c4b6b5b82c354ef2bb3b838c2f13cc)) * fix(DbQueryExecutedListener): 添加对position最大值的判断 ([8cc8691](https://github.com/mineadmin/mineadmin/commit/8cc8691e7481a24075c33cbb73cd1c9daf126138)) * fix(seeders): 大驼峰命名 ([8bdda0c](https://github.com/mineadmin/mineadmin/commit/8bdda0c61cd503a95eb25be5bfc71199341232bd)) * fix(seeder): 类名改成驼峰兼容php8.1 ([2759f0e](https://github.com/mineadmin/mineadmin/commit/2759f0e2f66d4c56480b0676b8e12c162efc49e6)) * fix(menuSeeder): 填充数据优化 ([ecdd2c4](https://github.com/mineadmin/mineadmin/commit/ecdd2c4ecff722d65689033b813a426349c41824)) * fix(menuSeeder): 填充数据修复 ([de7c389](https://github.com/mineadmin/mineadmin/commit/de7c389ab575f725b5d465f645ed8ed52d3535a3)) * fix(seeders.menu): 删除data\_scope写入,该字段已移除 ([f19f721](https://github.com/mineadmin/mineadmin/commit/f19f721747ffa3e2b1402c7747c57b933a24a1c5)) * fix(migrations.attachment): 🐛 修复问题 ([e708ccb](https://github.com/mineadmin/mineadmin/commit/e708ccbad0babf82bd3e0df8e4174252c49373d3)) * fix(constants.user.status): 🐛 描述和值错误 ([c9884b4](https://github.com/mineadmin/mineadmin/commit/c9884b4c0de7f83b597902f4edf97d2ba53b58de)) * fix(menu.pageList): 数据返回改为树形 ([7099bce](https://github.com/mineadmin/mineadmin/commit/7099bce32e309607a61e9aab7cdd51b73330a046)) * fix(PermissionMiddleware): 缺少对超管的放行 ([e26b762](https://github.com/mineadmin/mineadmin/commit/e26b762db230e77815815d6df3446d6d1e802e62)) * fix(cancel debug): 去掉显示debug信息 ([6711a44](https://github.com/mineadmin/mineadmin/commit/6711a447acf7965ec3155aa613d8c73e2828e75d)) * fix(jwt): 修复 jwt 过期时间配置不生效问题 ([402d5c3](https://github.com/mineadmin/mineadmin/commit/402d5c3dcc72f2d1df80b1e835558c1f64d6545e)) * fix(menu):刷新后,父菜单不展开的问题 ([5fd7d48](https://github.com/mineadmin/mineadmin/commit/5fd7d481f9b6d75912ca8230e66ae31db375e5b9)) * fix:菜单不显示的问题 ([3216ec4](https://github.com/mineadmin/mineadmin/commit/3216ec4e5a655c19659f9f39683557b46b1c7232)) * fix(seeder): MenuSeeder填充数据优化 ([6b49dd9](https://github.com/mineadmin/mineadmin/commit/6b49dd9fd7feee17e828c399f0aac8c5aa80b2f9)) * fix:token过期退出失败问题 ([2f3a3ba](https://github.com/mineadmin/mineadmin/commit/2f3a3ba327ed4c4ab64ec130fc7639623c6d2646)) * fix(cs-fix): 统一 kernel 编码规范 ([bf5aff2](https://github.com/mineadmin/mineadmin/commit/bf5aff2c2c5f859b763c28cf217edd1c5b9838c3)) * fix(debug): 去掉debug日志输出 ([b2a278a](https://github.com/mineadmin/mineadmin/commit/b2a278a6c72fb197adc2cf50c8cbd497416f172b)) * fix(vue-proxy): 修复前端代理错误的问题 ([33b1064](https://github.com/mineadmin/mineadmin/commit/33b10646f7e6ff62a5b6af5574ecc079dfafadce)) * fix(seeder): db:seed执行后找不到迁移文件的bug ([8f658a6](https://github.com/mineadmin/mineadmin/commit/8f658a68b27e654652fbe29fe10b51f99ad08331)) * fix(dbSeed): 优化数据填充,统一代码风格 ([fc41315](https://github.com/mineadmin/mineadmin/commit/fc41315fa8d0ed5e65651d476f5d8e9cf17177bd)) * fix(unit test): 修复单元测试,修复用户获取角色、菜单接口数据混淆问题 ([554184c](https://github.com/mineadmin/mineadmin/commit/554184c58a9bbc32486bb306599001a762549e1c)) * fix(资源选择器):完善类型定义,完善页面样式 ([67fddcf](https://github.com/mineadmin/mineadmin/commit/67fddcf8d272945c07c5e5d66046531d1ec3347b)) * fix(资源选择器): 完善类型定义 ([efc5717](https://github.com/mineadmin/mineadmin/commit/efc5717b7c5c74ac29d989440989994577c002b1)) * fix(mine-admin/cell-render): 修正switch组件beforeChange回调参数 ([49e6524](https://github.com/mineadmin/mineadmin/commit/49e65240ac1f8b5e2cb63e5a4e17ffadbc7af00d)) * fix(用户信息): 死循环问题 ([ddc7059](https://github.com/mineadmin/mineadmin/commit/ddc705968f6de0697ce589e2a362a8f651a45817)) * fix(pro-table): 修复使用icon组件控制台出警告信息 ([24d2293](https://github.com/mineadmin/mineadmin/commit/24d22939443c9ade9689d83d19338364eb203c8f)) * fix(mine-admin): 修正单元格渲染器的开关请求参数修正了单元格渲染器组件中的开关请求参数。现在正确地传递开关状态更改的请求数据,以反映状态更新时的期望行为。 ([079522d](https://github.com/mineadmin/mineadmin/commit/079522d83853037b8f1024973d8bcebd6be29bea)) * fix(mine-admin): 修正switch组件beforeChange回调执行逻辑 ([3737e92](https://github.com/mineadmin/mineadmin/commit/3737e92198f536391d0aed1dca0b67cab7dce290)) * fix(mine-admin): 确保在switch组件的beforeChange钩子中正确处理加载状态 ([9ed093d](https://github.com/mineadmin/mineadmin/commit/9ed093dabed4c3d80cb8b1bb524d42e2d456d683)) * fix(兼容mock模式) ([d58baa6](https://github.com/mineadmin/mineadmin/commit/d58baa6043bfeeff5bf95159fb8f1a00a38515b2)) * fix(代码格式) ([dfcf5a5](https://github.com/mineadmin/mineadmin/commit/dfcf5a5b909573cf0d6722939906522f7dc06f87)) * fix(mine-admin): 修正switch组件api类型定义及demo使用修正了switch组件中api的类型定义,将其实参从params改为data,以更好地反映其用法。同时,在demo示例中,改为直接使用useHttp().get方法,以便正确演示switch组件的api ([c840bef](https://github.com/mineadmin/mineadmin/commit/c840bef5d279a8913119c4af0f63055972bf0036)) * fix(color): 修复颜色在黑暗模式下显示level的问题 ([046ae30](https://github.com/mineadmin/mineadmin/commit/046ae3043d0657fb277210176af28b186a6e1eee)) * fix(menu): 菜单隐藏失效bug ([b757544](https://github.com/mineadmin/mineadmin/commit/b757544e8da54cb1ed1cc2c93a916aaeec98ce28)) * fix(layout): 菜单版权是否显示与全局取消关联 ([8dd3bba](https://github.com/mineadmin/mineadmin/commit/8dd3bba5bd62a26149461d6e56e11150c38daeeb)) * fix: 🐛 按钮列属性默认值修正为`id` ([0c9bb4f](https://github.com/mineadmin/mineadmin/commit/0c9bb4ffdee8789b9d7f36e23b3573264a90efaa)) * fix(pro-table): 优化加载状态处理和自动查询逻辑 ([07560a5](https://github.com/mineadmin/mineadmin/commit/07560a5e442e48a4d6fb0ed3d8f7bf43199f6129)) * fix(plugin): 🐛 插件的setup钩子调用点修复,非layout布局下不生效问题 ([f327407](https://github.com/mineadmin/mineadmin/commit/f327407b51f3a6ea1c2aa2b0bca61e99cf5e5394)) * fix(menu): 🐛 菜单的badge在Popup状态下仍然显示的问题 ([8b33db5](https://github.com/mineadmin/mineadmin/commit/8b33db5a4cca2283e880b5201cc95e8e66b080b3)) * fix(ma-resource-picker):临时提交,准备做回显相关的处理 ([a3613da](https://github.com/mineadmin/mineadmin/commit/a3613da83a60fc1e1aa7e20b8d8b7667fa378b9c)) * fix(ma-resource-picker): 修复资源选择器的双击选择和数据模型同步问题 ([71d0f8a](https://github.com/mineadmin/mineadmin/commit/71d0f8a06fb1d20a006ba97756a3f9b41ee8b979)) * fix(tab): 修正change事件参数类型:变更事件现在会发出新的参数类型,包括选项项,以便在选择选项时提供额外的上下文。这使得在处理选项变化时能够更方便地访问选项的元数据。 ([88eee4a](https://github.com/mineadmin/mineadmin/commit/88eee4ad4d1378f29cfc0e859794dbf7c1058c57)) * fix(panel): 更正资源项名称的背景颜色和文字颜色 :: ([a963326](https://github.com/mineadmin/mineadmin/commit/a963326c5ef72b86c1e0e811be25933850c0b2a4)) * fix(useImageViewer): 修正类型定义,排除urlList属性 ([c12a70f](https://github.com/mineadmin/mineadmin/commit/c12a70f42da36cbbe499460cefdce2773b3d415c)) * fix(resource-picker): 实现资源项双击预览功能: ([5997477](https://github.com/mineadmin/mineadmin/commit/5997477e8c3b7dab5116ef629a8053df0ba13868)) * fix(resource-picker): 修正选中状态样式显示问题:解决资源选择器组件中选中状态样式未正确显示的问题。调整资源项的选中图标位置并确保其在激活状态下正确显现。去除不必要的样式注释,清理并优化CSS代码可读性。 ([bc63d60](https://github.com/mineadmin/mineadmin/commit/bc63d60eabc728cdb716fc6d7f198ec5c9dac331)) * fix(ma-resource-picker): 修复多选和限制逻辑,并改进资源项样式 ([927d42f](https://github.com/mineadmin/mineadmin/commit/927d42f0a8923b60340affde2eb09179416f084f)) * fix(base): 修正MaResourcePanel容器高度样式 ([ba98b54](https://github.com/mineadmin/mineadmin/commit/ba98b54e4d87887563f28dcdf864d65c406fd4ea)) * fix(ma-icon-picker):在MaIconPicker组件中,移除了更新模型值的emit调用,该调用在model更新时被错误地调用两次。现在,当选择一个图标时,仅更新model值而不进行冗余的事件发射。 ([b5fe3dc](https://github.com/mineadmin/mineadmin/commit/b5fe3dc3b3976ea2793629f8efbc6845af1f4993)) * fix(ci): 自动测试脚本修复 ([7805671](https://github.com/mineadmin/mineadmin/commit/78056715e6b008e91b26ef62ecd7c7f6f7e4b117)) * fix(Tests): 修复DictData测试 ([#335](https://github.com/mineadmin/mineadmin/pull/335)) ([f429262](https://github.com/mineadmin/mineadmin/commit/f429262e09236e6c8bcf2684435760cd49c14345)) * fixed(ci): 优化 pgsql 环境单元测试用例失败 ([#320](https://github.com/mineadmin/mineadmin/pull/320)) ([1d88131](https://github.com/mineadmin/mineadmin/commit/1d8813107dd8996137e2e1dd7b477a78068d25f5)) * fixed(ci): 修复 docker build 错误 ([8c5e188](https://github.com/mineadmin/mineadmin/commit/8c5e1883d2bbdeecdb9ca65c6ba40a95dc8c422f)) * fixed(cache): 测试流程中错误的缓存键拼写 ([74816f9](https://github.com/mineadmin/mineadmin/commit/74816f9214806ed681dca46d7c87bca28fcfcab4)) * fix(QueueMessageService):修复发私信调用函数不存在(直接发送) ([57f2422](https://github.com/mineadmin/mineadmin/commit/57f242284fe708cce716e6a3564ec91f8ccc45f6)) * fix(model, ws router): 修正 NoticeModel 和 ws 路由器的命名空间 ([3550ac1](https://github.com/mineadmin/mineadmin/commit/3550ac14a9233f949a6d4e8361d2bf5c1a4a6b67)) * refactor(structure): rename framework components for consistency ([#310](https://github.com/mineadmin/mineadmin/pull/310)) ([99dff8e](https://github.com/mineadmin/mineadmin/commit/99dff8e98c1f683493d0bcbafe4c8c4ec1aa143c)) ### 📚 Documentation * docs(README): remove badges and update content ([#414](https://github.com/mineadmin/mineadmin/pull/414)) ([b15a004](https://github.com/mineadmin/mineadmin/commit/b15a0043c8f59f5c9b036644f9afb449893ca1b8)) * docs(迁移文件): 📝 规范迁移文件结构 ([bc860e3](https://github.com/mineadmin/mineadmin/commit/bc860e3e2db376fd00374a801f7e65f3259f4dc9)) * docs(迁移文件): 📝 优化代码结构 ([5fd2077](https://github.com/mineadmin/mineadmin/commit/5fd20778a11d37b2d140beeadca57c4e0f3baa2c)) * docs(迁移文件): 📝 优化注释 ([aa312ce](https://github.com/mineadmin/mineadmin/commit/aa312ce469648fb6f220892187ad4120e2891f00)) * docs(迁移文件): 📝 修改`attachment`迁移文件结构,优化字段注释 ([ad6798a](https://github.com/mineadmin/mineadmin/commit/ad6798ae9ce809a766b09373ef075dfee5f5f88e)) * docs(tools.ts): 更新表格单元格渲染工具类型定义 ([dc3ff19](https://github.com/mineadmin/mineadmin/commit/dc3ff1980c05eeebaea08387fc29afd795e8b9db)) ### ⚡ Performance * perf(sql输入): ⚡ 更改DbQueryExecutedListener的日志级别为info ([78d7ab6](https://github.com/mineadmin/mineadmin/commit/78d7ab632cfed1a7d47bf889f1896bc8c476e381)) * perf(更新@mineadmin/table): ⚡️更新 @mineadmin/table 到 1.0.5版本 ([da1dfea](https://github.com/mineadmin/mineadmin/commit/da1dfea4650589f8f04996e8d9f1bb221970d51b)) * perf(更新@mineadmin/table): ⚡️更新 @mineadmin/table 到 1.0.3版本 ([adb2d7d](https://github.com/mineadmin/mineadmin/commit/adb2d7d03ed604845c8dbe8b67a854d35c5edee5)) ### ♻️ Code Refactoring * refactor(iframe): 优化 iframe 在tab页关闭和刷新时重新加载iframe页面。 ([#478](https://github.com/mineadmin/mineadmin/pull/478)) ([666fd46](https://github.com/mineadmin/mineadmin/commit/666fd46e83954c9653676f9dc400751a3f0ce110)) * refactor(logManage): 优化日志管理批量删除时,弹出提示框确认是否删除 ([#473](https://github.com/mineadmin/mineadmin/pull/473)) ([8c8d35d](https://github.com/mineadmin/mineadmin/commit/8c8d35d0b336aec8c9b65c0e8825ebf30bafe912)) * refactor(upload): 抽离上传本地服务器方法到utils里,可被单独调用 ([#472](https://github.com/mineadmin/mineadmin/pull/472)) ([b323488](https://github.com/mineadmin/mineadmin/commit/b32348804bc55024a6bb462f67c82077b952387f)) * refactor(pro-table): 升级到1.0.37,增加暴露搜索事件`@search-submit`, `@search-reset` 和参数 `onSearchSubmit`, `onSearchReset` ([#462](https://github.com/mineadmin/mineadmin/pull/462)) ([3efad49](https://github.com/mineadmin/mineadmin/commit/3efad49c15eb508d1066fb2e4992d5dbfb3a9b98)) * refactor(menu): 菜单排序无效问题 ([#449](https://github.com/mineadmin/mineadmin/pull/449)) ([215decb](https://github.com/mineadmin/mineadmin/commit/215decbf75effd9ec89af4bac8e5a1967421756d)) * refactor(repository): optimize query handling and update saveById method ([#416](https://github.com/mineadmin/mineadmin/pull/416)) ([745b087](https://github.com/mineadmin/mineadmin/commit/745b0874e723f13a6482cec1444b0c01c2e32244)) * refactor(app): improve menu filtering logic ([#409](https://github.com/mineadmin/mineadmin/pull/409)) ([35e59ed](https://github.com/mineadmin/mineadmin/commit/35e59ed364efd5f942aef3ad5f855854496dab79)) * refactor(delete): change delete method return type and behavior ([#404](https://github.com/mineadmin/mineadmin/pull/404)) ([e1c657f](https://github.com/mineadmin/mineadmin/commit/e1c657fcdaedb67d2dad20eab7a31d1ca6c63092)) * refactor(permissions): remove Casbin and refactor permission logic ([#399](https://github.com/mineadmin/mineadmin/pull/399)) ([b445b22](https://github.com/mineadmin/mineadmin/commit/b445b22ca04ee6016e2e10a8980e7c50398f9bb2)) * refactor(ma-table):升级到1.0.25版,优化列头对齐未指定下默认使用单元格对齐 ([#392](https://github.com/mineadmin/mineadmin/pull/392)) ([5e5f6b0](https://github.com/mineadmin/mineadmin/commit/5e5f6b0898a8038ac0229e1ba137050fc2efabd7)) * refactor(admin):重构控制器中的请求数据获取方式 ([#386](https://github.com/mineadmin/mineadmin/pull/386)) ([0859e44](https://github.com/mineadmin/mineadmin/commit/0859e4492823891eb4a40b236b229e1ae47d0935)) * refactor: correct typos in language files ([#372](https://github.com/mineadmin/mineadmin/pull/372)) ([85a5e10](https://github.com/mineadmin/mineadmin/commit/85a5e10e74650273ea6c94796398f28bda977582)) * refactor(user): internationalize error messages in UserListener ([#371](https://github.com/mineadmin/mineadmin/pull/371)) ([c7a30e6](https://github.com/mineadmin/mineadmin/commit/c7a30e6e669b51faf7f662d3e1b89eb65388fec9)) * refactor(auth): rename login request and optimize passport controller ([5c87642](https://github.com/mineadmin/mineadmin/commit/5c876421f4f3e09e4881f42b414ad8633876da0c)) * refactor(localization): update and rename localization files for zh\_TW locale ([b42b314](https://github.com/mineadmin/mineadmin/commit/b42b314fc2253f5704c2b48e24a11bd381079efe)) * refactor(http) ([9e6a5e7](https://github.com/mineadmin/mineadmin/commit/9e6a5e7d6490a7310b6af0470ce0eefed6ed1436)) * refactor(admin): update permission codes and remove unused exception handlers ([48d7d71](https://github.com/mineadmin/mineadmin/commit/48d7d71c27131673ada01e39366c45c8c8e87f69)) * refactor(admin): update permission codes for menu, role and user management ([c6b5f1d](https://github.com/mineadmin/mineadmin/commit/c6b5f1d86a171178b06e6c7e21c0ab9524beb1c3)) * refactor(app-store): 重构代码并添加国际化支持 ([7dbbe2f](https://github.com/mineadmin/mineadmin/commit/7dbbe2f98de2c2994d62f94dccb8ee96ea0118b8)) * refactor(exception): optimize exception handling and remove redundant code ([9e2fbdc](https://github.com/mineadmin/mineadmin/commit/9e2fbdc078bb4f653627323453a0371130314894)) * refactor(attachment): change storageMode property type from string to int ([1f1e09d](https://github.com/mineadmin/mineadmin/commit/1f1e09da516ea85515a79e2b9a8e9b8878e3db49)) * refactor(permission): adjust status handling and improve repository tests ([f84f9b0](https://github.com/mineadmin/mineadmin/commit/f84f9b0d9d3726279a18f009747ed3cc6e0f07f8)) * refactor(exception): use match expression in JwtExceptionHandler ([e20f8d6](https://github.com/mineadmin/mineadmin/commit/e20f8d6e398898d3205dee590451d8103ed9169f)) * refactor(重构modal和drawer组件) ([5784468](https://github.com/mineadmin/mineadmin/commit/5784468fa3d5417f58efd6cb636487ded4aff251)) * refactor: ♻️ 优化请求菜单那、角色逻辑,适配http、code问题,修复一些小bug ([0217955](https://github.com/mineadmin/mineadmin/commit/02179558b42daf2ab7bd9bb3dd5be7db75229f45)) * refactor(ma-resource-picker):简化文件类型选择逻辑并改善封面获取方法 ([5efadf5](https://github.com/mineadmin/mineadmin/commit/5efadf51b742abe1ad4641ed003b9cd249a7255c)) * refactor(mine-admin/cell-render): 重命名RFV接口为RowFieldValues ([8856a1a](https://github.com/mineadmin/mineadmin/commit/8856a1a4066c0f5fa1fabf42fc801ebc35dd39b3)) * refactor(mine-admin): 移除cell-render插件中的路由注册 ([565d2c5](https://github.com/mineadmin/mineadmin/commit/565d2c5e5c1cf834dba1710381b63bc59fbc5370)) * refactor(mine-admin/cell-render): 更新单元格渲染配置 ([032faff](https://github.com/mineadmin/mineadmin/commit/032faff88661b926e9e99ea7334b19f1348cd27b)) * refactor(mine-admin): 更新proTable组件和单元测试 ([09c974b](https://github.com/mineadmin/mineadmin/commit/09c974b038392345a7d2e5660460828a93c180af)) * refactor(resource-picker): 使用Element Plus图标替换SVG图标: ([c9d8038](https://github.com/mineadmin/mineadmin/commit/c9d8038205f8bdea0fef5334ef20798cdcb71996)) * refactor(resource-picker): 优化选定资源的循环迭代: ([9a846d3](https://github.com/mineadmin/mineadmin/commit/9a846d371b872e0cd2748281e9e853e02a99b62e)) * refactor(resource-picker): 在选择按钮上添加 popover 以显示已选资源: ([aff449c](https://github.com/mineadmin/mineadmin/commit/aff449c8e7c9b42bcb24a6ea75182b95b9efde4b)) * refactor(resource-picker): 移除对话框页脚并更新文件类型选择器: - 删除了ma-resource-picker组件中的对话框页脚,以简化UI。 - 使用``替代``用于文件类型选择,增强可用性。 - 调整了输入框的大小并添加了清除功能,提升用户体验。 - 新的文件类型选择器实现了更一致的筛选行为,并优化了视觉展示。 ([159d716](https://github.com/mineadmin/mineadmin/commit/159d7164216b26c547fa19b65b13cf0d8af58748)) * refactor(resource-picker): 更新图标和文件类型列表: ([a9ead21](https://github.com/mineadmin/mineadmin/commit/a9ead21016728fc02d940855c4934ed4052f2e38)) * refactor(resource-picker): 将类型定义移动到专用的type.ts文件: ([4e148e7](https://github.com/mineadmin/mineadmin/commit/4e148e73e8b3c881866783a2bad21d506da8a075)) * refactor(mine-admin): 更改FileType接口继承的范型定义在`ma-resource-picker`组件中,`FileType`接口原是继承自`OptionItems`的。此次更改将其改为继承自`MTabsOptionItems`,以利用`MTabsOptionItems`中定义的更准确的属性,提高代码的可维护性和一致性。 ([98df98c](https://github.com/mineadmin/mineadmin/commit/98df98cd988e2ccb228a4d66e9eba70e537188a4)) * refactor(tab): 更新类型定义并简化props与emits ([fc18910](https://github.com/mineadmin/mineadmin/commit/fc189106dc10fc7e200f7b85313eec03602d86c4)) * refactor(tab): 将类型定义抽离,方便别的组件调用 ([fb301bb](https://github.com/mineadmin/mineadmin/commit/fb301bba3ad2ff8e79eb5aac0fd911239cced468)) * refactor(mock): 优化附件模拟数据和文件类型处理: ([4faef8c](https://github.com/mineadmin/mineadmin/commit/4faef8cd4bc2fc9651491127525b75dbb4bd9cce)) * refactor(resource-picker): 重命名函数参数以提高清晰度:资源选择器组件中的函数参数从`item`重命名为`resource`,以提高代码的可读性和可维护性。相关功能包括切换选择、检查是否选中、预览能力和双击事件处理的函数现在使用更清晰的参数命名。上下文菜单中的操作也进行了类似的重命名处理。 ([9993b41](https://github.com/mineadmin/mineadmin/commit/9993b412b1b4d61f2905de228525b08f32f2cf50)) * refactor(resource-picker): 抽离图像预览功能至useImageViewer钩子: ([34506a0](https://github.com/mineadmin/mineadmin/commit/34506a0e78930cee2efcd7c1564c6b60f966c888)) * refactor(resource-picker): 双击行为待定: ([813f55c](https://github.com/mineadmin/mineadmin/commit/813f55c8ada6b6e8bee40cd4dfe18f663ded1422)) * refactor(resource-picker): 非固定分页大小及平滑加载动画 ([a356816](https://github.com/mineadmin/mineadmin/commit/a35681628b646cdc02eaeb4438afe4ce26f284f0)) * refactor(resource-picker): 重构资源选择器面板的样式和结构,以适应动态内容高度。通过修改CSS类应用和调整元素间距,实现了资源项目的均匀分布。此外,还优化了滚动条组件的使用,以提高在长列表上的性能。 ([a78c4ca](https://github.com/mineadmin/mineadmin/commit/a78c4ca57180ad59ab13bf050cf7a775ee8573a0)) * refactor(cleanup): 删除遗漏ModuleRequest类 ([93023c9](https://github.com/mineadmin/mineadmin/commit/93023c90d9e6b55e9fd885922f14e7134ff2249e)) * refactor(cleanup): 删除自动生成的注释 @throws 等优化可读性 PS:后续还会持续优化 ([49be9fb](https://github.com/mineadmin/mineadmin/commit/49be9fbd319f6b3dd05051adb2d420ac1740d72e)) * refactor(structure): rename framework components for consistency ([#310](https://github.com/mineadmin/mineadmin/pull/310)) ([99dff8e](https://github.com/mineadmin/mineadmin/commit/99dff8e98c1f683493d0bcbafe4c8c4ec1aa143c)) ### 🔧 Others * chore(package): 更新最新依赖,适配最新版i18n ([#471](https://github.com/mineadmin/mineadmin/pull/471)) ([1b73f61](https://github.com/mineadmin/mineadmin/commit/1b73f6190b6cd54b7c8782822e27c11f2be60615)) * chore(pro-table): 修复table参数覆盖问题导致参数失效 ([#461](https://github.com/mineadmin/mineadmin/pull/461)) ([7ccd472](https://github.com/mineadmin/mineadmin/commit/7ccd472cac7f7865f1a84db61f431f872966cb3d)) * chore(package): 更新pro-table和search,修复几处小问题 ([#459](https://github.com/mineadmin/mineadmin/pull/459)) ([2091a3a](https://github.com/mineadmin/mineadmin/commit/2091a3a40356f4659e03e970a426a1e50383b499)) * styles(layout): 优化布局样式 ([#457](https://github.com/mineadmin/mineadmin/pull/457)) ([b3c5d8b](https://github.com/mineadmin/mineadmin/commit/b3c5d8b328722840d3d8c883e3c35b4c0ea6064b)) * chore(front): 优化修改插件钩子参数 ([#456](https://github.com/mineadmin/mineadmin/pull/456)) ([a50284c](https://github.com/mineadmin/mineadmin/commit/a50284c41b6418c70bf59ea289822041819f0f6b)) * chore(other): 修改类型定义,优化默认静态路由 ([#454](https://github.com/mineadmin/mineadmin/pull/454)) ([305ad7f](https://github.com/mineadmin/mineadmin/commit/305ad7f3c68795bb8286776dbf9d0ad91f6ce398)) * chore(ma-pro-table): 更新ma-pro-table到1.0.27版,pnpm-lock加入忽略列表 ([#434](https://github.com/mineadmin/mineadmin/pull/434)) ([f1b74fd](https://github.com/mineadmin/mineadmin/commit/f1b74fd656131b1d56bbac80c86d6ca603e71ecd)) * styles(样式优化) ([#428](https://github.com/mineadmin/mineadmin/pull/428)) ([bb1f17e](https://github.com/mineadmin/mineadmin/commit/bb1f17e947cb970b8caaed5e10fdf73a8b94f619)) * chore(tab): 变更标签页新增时检查的key,优化布局文件 ([#425](https://github.com/mineadmin/mineadmin/pull/425)) ([aa6474a](https://github.com/mineadmin/mineadmin/commit/aa6474aafdb36cb5b867e457dee913be88252feb)) * chore(tsconfig): 开启默认允许js ([#423](https://github.com/mineadmin/mineadmin/pull/423)) ([40e2b24](https://github.com/mineadmin/mineadmin/commit/40e2b24cacd5003d5de844048d8773148f5ab7e4)) * styles(menu): 优化子级菜单激活后,父级菜单高亮 ([#419](https://github.com/mineadmin/mineadmin/pull/419)) ([df8ec2c](https://github.com/mineadmin/mineadmin/commit/df8ec2cc1e099df99039ea253d1936d3c39e7d0b)) * chore(front): 退出清除所有tab,ma-dialog新增操作快捷键,ma-tree增加 buttons插槽 ([#410](https://github.com/mineadmin/mineadmin/pull/410)) ([0fd8605](https://github.com/mineadmin/mineadmin/commit/0fd86053dbe6d6a6d7589e0b0e49b1820428091e)) * chore(ma-form,ma-search):升级俩组件依赖,优化一些方法入参 ([#393](https://github.com/mineadmin/mineadmin/pull/393)) ([4716ffe](https://github.com/mineadmin/mineadmin/commit/4716ffe337a4566b632edc442916a313283b75bc)) * chore(pro-table):升级到1.0.22版,组件增加 `getProTableOptions()` 方法 ([#384](https://github.com/mineadmin/mineadmin/pull/384)) ([c73725e](https://github.com/mineadmin/mineadmin/commit/c73725e2cdf0886fbc882940f952b0680dadac86)) * chore(@mineadmin/pro-table): 升级pro-table到1.0.21,pro-table重构工具栏,开放api可以插件形式扩展: `useProTableToolbar()` ([#378](https://github.com/mineadmin/mineadmin/pull/378)) ([df1df62](https://github.com/mineadmin/mineadmin/commit/df1df62659585e8f5117273c2e12697e2968ac33)) * chore(toolbar): 修改 remove 方法的参数 ([ec639ef](https://github.com/mineadmin/mineadmin/commit/ec639efdb6919ce33146d9e78100b6c5a8a94c4d)) * test: update repository tests and remove unnecessary comments ([9e011a7](https://github.com/mineadmin/mineadmin/commit/9e011a75178073aef15d58366920e83879f45fd4)) * ci: update phpunit configuration and project documentation ([0762acc](https://github.com/mineadmin/mineadmin/commit/0762acc6ef1040dc63a5a2d40ad76d2576d03c80)) * test: adjust code coverage settings and remove @coversNothing annotation ([4782848](https://github.com/mineadmin/mineadmin/commit/4782848ac50c1cb220664af87d4ef6cbbd56bb4e)) * chore(应用商店) ([ea409f1](https://github.com/mineadmin/mineadmin/commit/ea409f17b85fa44f3fda45f4d26dc7af60b31538)) * style(variables) ([d41144f](https://github.com/mineadmin/mineadmin/commit/d41144fc07dbf78c801e4b3dc597724127e32186)) * chore(整理文件) ([0332ceb](https://github.com/mineadmin/mineadmin/commit/0332cebc7f5b6faca16a0694053fbdd873693974)) * chore(更新依赖) ([4271e6e](https://github.com/mineadmin/mineadmin/commit/4271e6e6d6c50203640cae113f484c5ad25f46c9)) * chore(http):优化 ([bfa16e6](https://github.com/mineadmin/mineadmin/commit/bfa16e60d648a0c4993ad37ee23b3f7bcff9fe34)) * chore(workbench):优化工作台快捷入口路由正则匹配 ([9385ecf](https://github.com/mineadmin/mineadmin/commit/9385ecf76e1ea859ee049c67613c3176b4b006bc)) * chore(优化404页面,移除user center假功能) ([12499c9](https://github.com/mineadmin/mineadmin/commit/12499c97ed545dd6f4ad00ee98272fb86c33969e)) * test(repository): add abstract test repository and implement attachment, login log, and operation log repository tests ([3e304db](https://github.com/mineadmin/mineadmin/commit/3e304dbec1b718a9a39f3eb4b9c4a55abafab910)) * chore(优化banner布局下,显隐toolbar的按钮位置) ([728efd8](https://github.com/mineadmin/mineadmin/commit/728efd82504657485863fbab22216f34183b9f80)) * chore(Settings):后台前端设置新增持久化保存 ([371d7d6](https://github.com/mineadmin/mineadmin/commit/371d7d6c85cfa796a872aa7976063e1cc8377d75)) * chore(Menu):优化菜单在树结构里显示所属类型 ([80013fa](https://github.com/mineadmin/mineadmin/commit/80013fae643009b37e3b20ed02ad536920f05d74)) * chore(.env.example 增加 APP\_URL 参数) ([e28380f](https://github.com/mineadmin/mineadmin/commit/e28380f96e281cad20191e64dc6c872cc642e3ff)) * chore(移除useScrollTo,使用vueuse里的替代) ([7d50a8a](https://github.com/mineadmin/mineadmin/commit/7d50a8a8b8fd50779ca7321ffa1d2151d8d391fb)) * chore(移除打包时进行eslint检查) ([1852f53](https://github.com/mineadmin/mineadmin/commit/1852f538a7dea16afd3d17424d0db0269bf6e062)) * chore(菜单、权限标识优化修改-2) ([409282c](https://github.com/mineadmin/mineadmin/commit/409282cc6ef3be4ccd0cf415319882b633f741c7)) * chore(菜单、权限标识优化修改) ([e38144a](https://github.com/mineadmin/mineadmin/commit/e38144ad3b0561cfbeab4a46a8dbde911eab4d93)) * chore(优化修改) ([5c0f9ee](https://github.com/mineadmin/mineadmin/commit/5c0f9eeb1351060e0d9cd900489a18f269d95053)) * chore(优化) ([0c4d0e3](https://github.com/mineadmin/mineadmin/commit/0c4d0e31ba3256c622eb72f43182d450f623ce8b)) * chore(framework): 优化操作日志记录机制、优化获取 client ip 逻辑 ([d91a24c](https://github.com/mineadmin/mineadmin/commit/d91a24cca10db12cffbb29ddc04ada3c1909d13d)) * chore(repository): 优化仓储层设计,增加 page hook 机制 ([21c9012](https://github.com/mineadmin/mineadmin/commit/21c9012b2ebb38d6512107c77ab85eadf890d519)) * chore(watch): 优化热重启 ([b8333bb](https://github.com/mineadmin/mineadmin/commit/b8333bb21d1d1ae53f9c37cdd1aeaa8166c1b7c0)) * chore(jwt): 增加 jwt 过期错误,优化用户登录日志表结构 ([f545335](https://github.com/mineadmin/mineadmin/commit/f54533511cee5a53984283c9060c42431fe634a5)) * chore(登录) ([5408c74](https://github.com/mineadmin/mineadmin/commit/5408c743a3ed9f24b6ead092f9a5674be79f1fb1)) * chore(上传) ([82c4cc4](https://github.com/mineadmin/mineadmin/commit/82c4cc423ed00ed28aa957d2f2852c9f1a7d1775)) * chore(优化用户栏): mixed布局下,新增按钮控制用户栏显隐 ([d8aaf41](https://github.com/mineadmin/mineadmin/commit/d8aaf416328ceccabca326983b17ee61905ed3c7)) * chore(layouts): 优化混合布局 ([a5a21cd](https://github.com/mineadmin/mineadmin/commit/a5a21cd1fae3fa8ffc6966350bad0beb098125b7)) * style(字体样式): 📦 更新字体设置以提升可读性和美观度 ([2e286f8](https://github.com/mineadmin/mineadmin/commit/2e286f8bf928d4f2581c0c20e802ef81b40fd335)) * chore(适配vue3.5.x) ([2e3bcf0](https://github.com/mineadmin/mineadmin/commit/2e3bcf0629f92fc27814d65c95031065299eb26e)) * chore(package): 更新依赖 ([cc33d16](https://github.com/mineadmin/mineadmin/commit/cc33d16fe907920f46acce3772ea85e8fc4ba0fd)) * chore(i18n): 📦 优化多语言资源加载策略 ([4f8a150](https://github.com/mineadmin/mineadmin/commit/4f8a150406a53870a3e149a06ff912baf1d6d727)) * chore(http.ts): 优化401状态防抖策略 ([c3975be](https://github.com/mineadmin/mineadmin/commit/c3975be3ab4af1b92e09382f392408649693f49c)) * chore(min-admin/cell-render): 优化页面展示 ([ae06af2](https://github.com/mineadmin/mineadmin/commit/ae06af2d08646e58247cc53eb43d366839f033fe)) * chore(min-admin/cell-render): 调整请求地址 ([63ccbfa](https://github.com/mineadmin/mineadmin/commit/63ccbfad4c948fe8c8c87f1b91b1a4cc4b7d69cb)) * test(cell-render) ([88c4bd3](https://github.com/mineadmin/mineadmin/commit/88c4bd36ec8bf44f9b2461808ee5666dc9de5d7e)) * test(pro-table): 测试 pro-table ([4b9c259](https://github.com/mineadmin/mineadmin/commit/4b9c2594c12db4086e4b139092c7660e787ce494)) * test: ✅ 表格 ([67978a9](https://github.com/mineadmin/mineadmin/commit/67978a909cd59b494740e087c4bde5633b157ea1)) * test(pro-table) ([aa9bc3a](https://github.com/mineadmin/mineadmin/commit/aa9bc3a6f9ddba8be0148e5595841abff0660725)) * chore(update vue): 🔨 升级vue到3.5,组件适配优化 ([b01295b](https://github.com/mineadmin/mineadmin/commit/b01295b0b3cce05320d274b2138f09ebd981a44c)) * chore(更新、优化、修复): 🔨 更新依赖,优化样式、修复一些类型错误 ([59c39b9](https://github.com/mineadmin/mineadmin/commit/59c39b9f8d084ee329ef1383d7be83e7be6a9c7f)) * chore(更新依赖): 🔨 @mineadmin/form ([1a4ba71](https://github.com/mineadmin/mineadmin/commit/1a4ba71346fc18c446b9b038137ff6e10a84a394)) * chore(更新依赖): 🔨 @mineadmin/table ([875ffd6](https://github.com/mineadmin/mineadmin/commit/875ffd6bfa02db6358a9b3ccb097b6afb505fb6f)) * chore(测试 ssh推送): ✅ 测试 ssh推送 ([2f1f257](https://github.com/mineadmin/mineadmin/commit/2f1f25716117b2fc0ea863fa5188f8a428345860)) * test(resource-picker): ✅ css资源项视觉更新:更改背景色和优化样式 ([0084733](https://github.com/mineadmin/mineadmin/commit/0084733f9027409fd2f22e1caecf50ba5aac3518)) * test(resource-picker): ✅ 资源选择器面板 enhancement ([43a1dd3](https://github.com/mineadmin/mineadmin/commit/43a1dd30d1f68e33482d85bb6a4e81fde55b2f05)) * test(resource-picker): ✅ 在资源选择器组件中添加对话框页脚 ([52adbec](https://github.com/mineadmin/mineadmin/commit/52adbec8766c2088c80d3235c826a21dd8489d99)) * test(resource-picker): ✅ 在welcome页面添加组件,方便调试 ([9d9ba55](https://github.com/mineadmin/mineadmin/commit/9d9ba5572fcee44d055e2cef2e5af65269072314)) ## [v2.0.3] - 2024-10-06 ### 🐛 Bug Fixes * fix(setting\_config\_seeder): 确保config\_select\_data为数组类型 ([#341](https://github.com/mineadmin/mineadmin/pull/341)) ([a79bae6](https://github.com/mineadmin/mineadmin/commit/a79bae66fb966bcee1c7fb3f76edc15eb1109474)) * fix(修复下载插件失败): 修复因space与插件名拼接重叠导致无法下载 ([#319](https://github.com/mineadmin/mineadmin/pull/319)) ([3d796b4](https://github.com/mineadmin/mineadmin/commit/3d796b4165f9e8d815bf7e309afafb908e42def8)) * fix: 修复ClearLogCrontab 清空所有日志时开启事务导致失败 和 watch 脚本php8.2警告 ([#309](https://github.com/mineadmin/mineadmin/pull/309)) ([33d001a](https://github.com/mineadmin/mineadmin/commit/33d001ac1fd84a2966730f4821cb2bd8e706d811)) * fix dept level bug ([#306](https://github.com/mineadmin/mineadmin/pull/306)) ([3f11af4](https://github.com/mineadmin/mineadmin/commit/3f11af44badfa475925b949fa02c330db1ef8d98)) ## [v2.0.2] - 2024-07-09 ### 🐛 Bug Fixes * fixed ([#292](https://github.com/mineadmin/mineadmin/pull/292)) ([a954d96](https://github.com/mineadmin/mineadmin/commit/a954d960ba880b916296ddf6bbe598d0e45d61f2)) ## [v2.0.1.1] - 2024-06-23 ## [v2.0.1] - 2024-06-22 ### ✨ Features * feat: 字典分类新增list接口 ([2f3ab3c](https://github.com/mineadmin/mineadmin/commit/2f3ab3cf72b00c0157bcef5f674cad952fd32d13)) * feat Auto-generated changelog ([#271](https://github.com/mineadmin/mineadmin/pull/271)) ([1abf182](https://github.com/mineadmin/mineadmin/commit/1abf182bb76607bcce1a433306b135d4cf2ccec4)) * feat: 后台可视化应用市场插件 ([87b8a0b](https://github.com/mineadmin/mineadmin/commit/87b8a0b8eca06193ffa61ae7af00462b465bfe34)) * feat: add appStore plugin ([1482197](https://github.com/mineadmin/mineadmin/commit/148219750394fb49726b877e087477ceb812b274)) * feat: `common/commmon.php` add has\_permission() and has\_role() two function for helpes ([dbe16e0](https://github.com/mineadmin/mineadmin/commit/dbe16e057d1bea2a511794b2d6b4252360226c17)) ### 🐛 Bug Fixes * fix 修改用户更新个人资料过滤不存在的字段、修复手机号码验证传递null会报错的问题 ([#283](https://github.com/mineadmin/mineadmin/pull/283)) ([b3c98d5](https://github.com/mineadmin/mineadmin/commit/b3c98d57addb76e7fe78581efa142f70d5fb8eda)) * fix:修复变量注释不自动提示问题 ([#277](https://github.com/mineadmin/mineadmin/pull/277)) ([9d501bb](https://github.com/mineadmin/mineadmin/commit/9d501bba76542594671e49824e5f421787bba315)) * fixed: 修复因 storage\_mode是int类型获取文件系统不正确导致无法删除OSS或其他文件系统文件 ([#275](https://github.com/mineadmin/mineadmin/pull/275)) ([001d656](https://github.com/mineadmin/mineadmin/commit/001d6562269a6af29e995cba31038b98b3c72056)) * fix 解决部门树状数据时重复问题 ([#274](https://github.com/mineadmin/mineadmin/pull/274)) ([4b64fe1](https://github.com/mineadmin/mineadmin/commit/4b64fe190c18d5c0c7c0c21211a7754f81877b02)) * fix: Optimise user filtering logic ([#250](https://github.com/mineadmin/mineadmin/pull/250)) ([f88f2ef](https://github.com/mineadmin/mineadmin/commit/f88f2ef3e5a0810b3a2ff698dfb7ad452a46fb4b)) * fix: 更新模块json里的order属性,市场插件up ([f3ed750](https://github.com/mineadmin/mineadmin/commit/f3ed75095f789717e637c89213a47959541b216f)) * fix: created table migrations allow nullable ([a728b26](https://github.com/mineadmin/mineadmin/commit/a728b2667cb58a9265d8a9ac5db4faff6a3c63c8)) ### ♻️ Code Refactoring * refactor ([25b1818](https://github.com/mineadmin/mineadmin/commit/25b1818b04ea928cf9cafd653e06a3929dce20fe)) ## [v2.0.0-beta.6] - 2024-04-11 ## [v2.0.0-beta.5] - 2024-03-04 ### 🐛 Bug Fixes * fix: monitor service ([3d1a741](https://github.com/mineadmin/mineadmin/commit/3d1a741886c6ba9b6ffa2652120e25f23a1a2f95)) ## [v2.0.0-beta.4] - 2024-02-02 ### 🐛 Bug Fixes * fixed gitignore ([d526a56](https://github.com/mineadmin/mineadmin/commit/d526a567978365c022623ad3d26cdb2fcad97a87)) * fixed pest ([f45ffd7](https://github.com/mineadmin/mineadmin/commit/f45ffd788404daea17826e9b85351683cda2e3eb)) * fix: return value for save function. ([cdf4500](https://github.com/mineadmin/mineadmin/commit/cdf450042f8e7e3c082c473c003ec1de04d2a6b3)) ## [v2.0.0-beta.3] - 2024-01-31 ### 🐛 Bug Fixes * fixed library version suport latest ([1bbe0ff](https://github.com/mineadmin/mineadmin/commit/1bbe0ffb71a8896fd2555534e5bf1cb4631ffd79)) * fix: 修改handleSearch条件检查函数,以及适配主键支持雪花ID和UUID ([800c06e](https://github.com/mineadmin/mineadmin/commit/800c06e56c5e9a11b6686d938bec95d98b661721)) ### 🔧 Others * test.yml add redis and mysql ([8056ef8](https://github.com/mineadmin/mineadmin/commit/8056ef8cc3b0f7630e3fa9c16d2c57c2ded659f8)) ## [v2.0.0-beta.2] - 2024-01-25 ### ✨ Features * feature hyperf issue template ([7dbb095](https://github.com/mineadmin/mineadmin/commit/7dbb0952ddbebf8e8ee194be330fdc24121dbd37)) * feature workflows dockerfile ([3486e82](https://github.com/mineadmin/mineadmin/commit/3486e82f9e5f0fc40d81eb76a10c0fa23251e56b)) ### 🐛 Bug Fixes * fixed mine-core ([0f740ae](https://github.com/mineadmin/mineadmin/commit/0f740aea28cf75a9688a61af5729135d414f0d11)) * fixed cacheable annotation ([7a24d46](https://github.com/mineadmin/mineadmin/commit/7a24d46f3a2e862bd1e65f82878272b8772a6800)) * fixed dockerfile ([d429f3e](https://github.com/mineadmin/mineadmin/commit/d429f3e728e1ce41bd45765fee9ee8ef40821333)) * fix: dockerfile 改为用 hyperf官方镜像 ([f2373e9](https://github.com/mineadmin/mineadmin/commit/f2373e9cbf238b2ff2ddc368528598887144931e)) * fix: readme ([c2148f7](https://github.com/mineadmin/mineadmin/commit/c2148f7f524ce2dac6470ef6d91ee1d7fb53b4bd)) ## [v2.0.0-beta.1] - 2024-01-21 ### 🐛 Bug Fixes * fix common.php autoload ([61eab10](https://github.com/mineadmin/mineadmin/commit/61eab101054efbc25d16ea143082558d765ec352)) * fix env ([0ecd10b](https://github.com/mineadmin/mineadmin/commit/0ecd10b71c5dde7e17a5ff1f68c9b28dee1ca46f)) * fix 在线用户统计优化,配置获取缓存逻辑优化 ([f122ba5](https://github.com/mineadmin/mineadmin/commit/f122ba59c6048f100f99b55b6c001ed83f3fe834)) * fix test actions ([668a219](https://github.com/mineadmin/mineadmin/commit/668a2198e3a6921834fb7ea4f52d2006ddb581d5)) * fix: cs-fix排除runtime ([7af7020](https://github.com/mineadmin/mineadmin/commit/7af7020b020cba0fdd172c422a546c2d1756256f)) ## [v2.0.0-beta] - 2024-01-20 ### 🐛 Bug Fixes * fix: cs-fix ([fd98ce1](https://github.com/mineadmin/mineadmin/commit/fd98ce103420946f6c59f56655b4f3eb04dd984d)) ## [v2.0.0-alpha.5] - 2024-01-19 ## [v2.0.0-alpha.4] - 2024-01-13 ### ✨ Features * feat 新的代码生成器 ([e26fe5c](https://github.com/mineadmin/mineadmin/commit/e26fe5ca123bb1d71adf20d788372a1cae37a3bd)) ### 🐛 Bug Fixes * fix: 附件删除菜单权限父ID归属错误问题 ([78035eb](https://github.com/mineadmin/mineadmin/commit/78035eb918c2e68be0dbe35d1a8e300c8ad78c0c)) ## [v2.0.0-alpha.3] - 2023-12-23 ### 🐛 Bug Fixes * fix 缓存错误处理 ([d7bb21e](https://github.com/mineadmin/mineadmin/commit/d7bb21e25daaf0458f46fce359db805c1033f26c)) ## [v2.0.0-alpha.2] - 2023-12-21 ## [v2.0-stable] - 2024-05-30 ### ✨ Features * feat: 后台可视化应用市场插件 ([87b8a0b](https://github.com/mineadmin/mineadmin/commit/87b8a0b8eca06193ffa61ae7af00462b465bfe34)) * feat: add appStore plugin ([1482197](https://github.com/mineadmin/mineadmin/commit/148219750394fb49726b877e087477ceb812b274)) ### 🐛 Bug Fixes * fix: 更新模块json里的order属性,市场插件up ([f3ed750](https://github.com/mineadmin/mineadmin/commit/f3ed75095f789717e637c89213a47959541b216f)) ## [v2.0-RC.1] - 2024-05-17 ### ✨ Features * feat: `common/commmon.php` add has\_permission() and has\_role() two function for helpes ([dbe16e0](https://github.com/mineadmin/mineadmin/commit/dbe16e057d1bea2a511794b2d6b4252360226c17)) * feature hyperf issue template ([7dbb095](https://github.com/mineadmin/mineadmin/commit/7dbb0952ddbebf8e8ee194be330fdc24121dbd37)) * feature workflows dockerfile ([3486e82](https://github.com/mineadmin/mineadmin/commit/3486e82f9e5f0fc40d81eb76a10c0fa23251e56b)) * feat 新的代码生成器 ([e26fe5c](https://github.com/mineadmin/mineadmin/commit/e26fe5ca123bb1d71adf20d788372a1cae37a3bd)) * feature github actions ([6476a28](https://github.com/mineadmin/mineadmin/commit/6476a28b7fa7b48763d91c900ae5a90c92ccf630)) ### 🐛 Bug Fixes * fix: Optimise user filtering logic ([#250](https://github.com/mineadmin/mineadmin/pull/250)) ([f88f2ef](https://github.com/mineadmin/mineadmin/commit/f88f2ef3e5a0810b3a2ff698dfb7ad452a46fb4b)) * fix: created table migrations allow nullable ([a728b26](https://github.com/mineadmin/mineadmin/commit/a728b2667cb58a9265d8a9ac5db4faff6a3c63c8)) * fix: monitor service ([3d1a741](https://github.com/mineadmin/mineadmin/commit/3d1a741886c6ba9b6ffa2652120e25f23a1a2f95)) * fixed gitignore ([d526a56](https://github.com/mineadmin/mineadmin/commit/d526a567978365c022623ad3d26cdb2fcad97a87)) * fixed pest ([f45ffd7](https://github.com/mineadmin/mineadmin/commit/f45ffd788404daea17826e9b85351683cda2e3eb)) * fixed library version suport latest ([1bbe0ff](https://github.com/mineadmin/mineadmin/commit/1bbe0ffb71a8896fd2555534e5bf1cb4631ffd79)) * fix: return value for save function. ([cdf4500](https://github.com/mineadmin/mineadmin/commit/cdf450042f8e7e3c082c473c003ec1de04d2a6b3)) * fix: 修改handleSearch条件检查函数,以及适配主键支持雪花ID和UUID ([800c06e](https://github.com/mineadmin/mineadmin/commit/800c06e56c5e9a11b6686d938bec95d98b661721)) * fixed mine-core ([0f740ae](https://github.com/mineadmin/mineadmin/commit/0f740aea28cf75a9688a61af5729135d414f0d11)) * fixed cacheable annotation ([7a24d46](https://github.com/mineadmin/mineadmin/commit/7a24d46f3a2e862bd1e65f82878272b8772a6800)) * fixed dockerfile ([d429f3e](https://github.com/mineadmin/mineadmin/commit/d429f3e728e1ce41bd45765fee9ee8ef40821333)) * fix: dockerfile 改为用 hyperf官方镜像 ([f2373e9](https://github.com/mineadmin/mineadmin/commit/f2373e9cbf238b2ff2ddc368528598887144931e)) * fix: readme ([c2148f7](https://github.com/mineadmin/mineadmin/commit/c2148f7f524ce2dac6470ef6d91ee1d7fb53b4bd)) * fix common.php autoload ([61eab10](https://github.com/mineadmin/mineadmin/commit/61eab101054efbc25d16ea143082558d765ec352)) * fix env ([0ecd10b](https://github.com/mineadmin/mineadmin/commit/0ecd10b71c5dde7e17a5ff1f68c9b28dee1ca46f)) * fix 在线用户统计优化,配置获取缓存逻辑优化 ([f122ba5](https://github.com/mineadmin/mineadmin/commit/f122ba59c6048f100f99b55b6c001ed83f3fe834)) * fix test actions ([668a219](https://github.com/mineadmin/mineadmin/commit/668a2198e3a6921834fb7ea4f52d2006ddb581d5)) * fix: cs-fix排除runtime ([7af7020](https://github.com/mineadmin/mineadmin/commit/7af7020b020cba0fdd172c422a546c2d1756256f)) * fix: cs-fix ([fd98ce1](https://github.com/mineadmin/mineadmin/commit/fd98ce103420946f6c59f56655b4f3eb04dd984d)) * fix: 附件删除菜单权限父ID归属错误问题 ([78035eb](https://github.com/mineadmin/mineadmin/commit/78035eb918c2e68be0dbe35d1a8e300c8ad78c0c)) * fix 缓存错误处理 ([d7bb21e](https://github.com/mineadmin/mineadmin/commit/d7bb21e25daaf0458f46fce359db805c1033f26c)) * fix Annotation ([89123af](https://github.com/mineadmin/mineadmin/commit/89123af847dde49758556c3d50d6cf17528ca0c5)) * fix v2.0.0-alpha.2 ([3ae8ae3](https://github.com/mineadmin/mineadmin/commit/3ae8ae38fad770f431943dc1fc9474023946b3a7)) * fix 缓存改为注解形式 ([21cc920](https://github.com/mineadmin/mineadmin/commit/21cc92098b10c650049f3d84ded55d72bbe98275)) * fix: code generator ([5bb743f](https://github.com/mineadmin/mineadmin/commit/5bb743ffa00f2e800542fbc3f7bab092764e887f)) * fix: old syntax ([ea47da4](https://github.com/mineadmin/mineadmin/commit/ea47da4f7362a783d2460196632f71c6b1ce89cf)) * fix library version ([5ebf0fb](https://github.com/mineadmin/mineadmin/commit/5ebf0fb321cc4f5fe99d6c6eb3f8183cb0d611ea)) * fix 适配3.1 ([e211f74](https://github.com/mineadmin/mineadmin/commit/e211f745ffd9548c44236531d739be54a260c9a2)) * fix 优化提示 ([6480ead](https://github.com/mineadmin/mineadmin/commit/6480eada83557d5cfa027aa2d6fea69ef61e6668)) * fix: 适配支持Hyperf 3.1 ([12d3953](https://github.com/mineadmin/mineadmin/commit/12d3953c34fb98198c9110b2588e189323ae8850)) ### ♻️ Code Refactoring * refactor ([25b1818](https://github.com/mineadmin/mineadmin/commit/25b1818b04ea928cf9cafd653e06a3929dce20fe)) ### 🔧 Others * test.yml add redis and mysql ([8056ef8](https://github.com/mineadmin/mineadmin/commit/8056ef8cc3b0f7630e3fa9c16d2c57c2ded659f8)) * style: all code ([07c457d](https://github.com/mineadmin/mineadmin/commit/07c457dae843f401477c9c5f8fc39af6669df002)) ## [v1.4.13] - 2023-12-17 ### 🐛 Bug Fixes * fix 统一子包 ([970f6fb](https://github.com/mineadmin/mineadmin/commit/970f6fbbb08fe7722be0846c966af28eeab981f2)) ## [v1.4.12] - 2024-01-20 ### 🐛 Bug Fixes * fix ide error ([92c50fe](https://github.com/mineadmin/mineadmin/commit/92c50fe94614ec85598e3ebcd202b1da76d48c81)) ## [v1.4.11] - 2024-01-20 ### 🐛 Bug Fixes * fix 语法错误 ([3b22cae](https://github.com/mineadmin/mineadmin/commit/3b22caec4da4cc7e4cd67c1abe12d4366ade1699)) ## [v1.4.1] - 2024-01-19 ### ✨ Features * feat 新增php-cs-fixer配置.本次升级涉及大量代码风格重构.勿无脑升级 ([46861cc](https://github.com/mineadmin/mineadmin/commit/46861cc197b057f4e1e63973431dbf30b44dbc7a)) * feat: 升级mine-core到1.5.5版本,代码生成的mapper用 filled 替换 blank ([1aa57a3](https://github.com/mineadmin/mineadmin/commit/1aa57a31c68fdd25991fbcb93c798e57fea55ed8)) * feat: 升级mine-core到1.5.4版本,修复已知bug,新增表主键支持雪花ID、uuid,自动识别主键类型 ([f733026](https://github.com/mineadmin/mineadmin/commit/f7330267fa853bdd5a4f30f988b404cea74122ac)) ### 🐛 Bug Fixes * fix: mapper的filled函数替换blank函数,blank函数意思有歧义。`注意1.5.4的mine-core升级上来后,需要自行批量替换blank函数` ([30517df](https://github.com/mineadmin/mineadmin/commit/30517dfd95b9c9a550249c1660cb4cae12e15766)) * fix: 附件删除菜单权限父ID归属错误问题 ([f6ec802](https://github.com/mineadmin/mineadmin/commit/f6ec802da160f298b0a3a8cf3b03d214747b886b)) * fix: 修复Seeder php 8.2语法兼容性 ([c0229de](https://github.com/mineadmin/mineadmin/commit/c0229de00abf0ce72a89191fbbe695e283f590a0)) * fix: README.md ([fe71651](https://github.com/mineadmin/mineadmin/commit/fe71651fc960d8d033deec4d35c0356b58f2ccb5)) ## [v1.4.x] - 2023-12-08 ### ✨ Features * feat: UploadController.php 新增 showFile 方法,适配前端hash输入图片或文件 ([f029c32](https://github.com/mineadmin/mineadmin/commit/f029c32b2c283e62356f6013acbc2216b6fc0376)) * feat: 新增sys\_config() 和 sys\_group\_config() 函数 ([15985cf](https://github.com/mineadmin/mineadmin/commit/15985cff0eb228b6c490039e2dc65d177853e744)) ### 🐛 Bug Fixes * fix: 修复拼写错误 ([d24f85b](https://github.com/mineadmin/mineadmin/commit/d24f85ba5ca2fa28a1c12f64a7a7d1a6ed3bef85)) * fix: 修复获取配置文件信息拼写错误 ([d24f21a](https://github.com/mineadmin/mineadmin/commit/d24f21aebeb855fbe5c6c51efacef0f2cfa84469)) * fix: 修复查询字段名称写错的问题 ([a76e35b](https://github.com/mineadmin/mineadmin/commit/a76e35b7498483948c3a0100b039d5ee0ce67dc4)) * fix: 修复个人中心修改头像和资料会导致平权修改数据的漏洞 ([016f175](https://github.com/mineadmin/mineadmin/commit/016f175c1d53483da2e721a42a3e3a261f23cec6)) * fix: 修复个人中心获取登录和操作日志可平权查看数据的漏洞 ([12e5ca1](https://github.com/mineadmin/mineadmin/commit/12e5ca1d4bb229e44eed9a8c8c3d1287fb11d398)) * fix: 开启日志记录requestId ([4b04cad](https://github.com/mineadmin/mineadmin/commit/4b04cad52397e6172dbf8e4b72bf4b720c0cab74)) * fix: 修复上传的文件若在回收站则无法重新上传的问题 ([22267d1](https://github.com/mineadmin/mineadmin/commit/22267d1896567cf769fd3d559bd61413f4b2812d)) * fix: 修复更新系统配置时,提示 `config_select_data` 未定义的bug ([0cd2743](https://github.com/mineadmin/mineadmin/commit/0cd274349ae91c4f0558217e18e933533d82e627)) * fix: 创建setting\_datasource表之前,检查表是否存在 ([be0d45d](https://github.com/mineadmin/mineadmin/commit/be0d45d0050fc839b2f1e7d859406723f4d83b83)) * fix: 修复命名空间大小写问题 ([f63b596](https://github.com/mineadmin/mineadmin/commit/f63b5960e923497b37e6b14aa09330f07c18ec1c)) * fix: 修复系统配置对复选框支持不友好的问题 ([db6a335](https://github.com/mineadmin/mineadmin/commit/db6a3356554316d1e60992f82ae41e19925005b5)) * fix: 部门编辑报错 ([38293ff](https://github.com/mineadmin/mineadmin/commit/38293ff8997e99b03029038825052756e626d0d7)) * fix: 修复代码生成树表后添加数据时报错的问题 ps: composer update xmo/mine-core ([409000f](https://github.com/mineadmin/mineadmin/commit/409000fffcce6316e1cd33fd5e1c201bd9a3bca3)) * fix allow\_roles field cast to array ([33f6fd1](https://github.com/mineadmin/mineadmin/commit/33f6fd1e24e8801dce307490222bd179477782a6)) ### ♻️ Code Refactoring * refactor: 更新mine-core核心包 ([059702d](https://github.com/mineadmin/mineadmin/commit/059702db5371a7995de0a3a259e939b033ab8a76)) * refactor: 关闭 buffer 输出大小限制 ([77731cf](https://github.com/mineadmin/mineadmin/commit/77731cfc33fd6a9d919836d6abd90cfc6f379587)) * refactor: 优化在开启数据权限后非超管账号添加部门时可选择父级部门为自身所在部门 ([d08e2db](https://github.com/mineadmin/mineadmin/commit/d08e2db3dc687a9e61fc03410033bd39bb713f85)) * refactor: 优化登录提示错误信息防止用户被枚举 ([25fa4d3](https://github.com/mineadmin/mineadmin/commit/25fa4d345888952deec2c5b8ca61b17819eb8128)) * refactor: 感谢最菜兄优化 `bin/reboot.php`,mine-core的amqp队列监听器移动到 App\System\Listener 下,升级mine-core ([b3362d9](https://github.com/mineadmin/mineadmin/commit/b3362d9cc6b0eae6f068796d94a2c0b6002901af)) * refactor: 业务里的isset替换为 !empty ([f724295](https://github.com/mineadmin/mineadmin/commit/f724295a2ef10c080331f5dcdbed7a9a302e9fec)) * refactor ([6fc5f01](https://github.com/mineadmin/mineadmin/commit/6fc5f01a2e2955b3b1a1818749dea4f745fc1b55)) * refactor: 优化api抛出异常信息提示 ([1ef5d1e](https://github.com/mineadmin/mineadmin/commit/1ef5d1e0c0d2929e47e6614a6787e46304f82359)) ## [v1.3.3] - 2023-06-02 ### ✨ Features * feat: 新增通用接口功能,变更版本为1.3.3 ([555de3e](https://github.com/mineadmin/mineadmin/commit/555de3e8ca846680901a82dce4a1321ff0d220d0)) ### 🐛 Bug Fixes * fix: php 8.2 兼容 swoole>=4.4.6 PHP Deprecated: Swoole\Event::rshutdown(): ([13b9295](https://github.com/mineadmin/mineadmin/commit/13b92952ea36f7071be72125cbde0a5a7f031577)) * fix: 修复mine改成package后,生成代码时找不到模板文件 ([21c9ef7](https://github.com/mineadmin/mineadmin/commit/21c9ef76f2b8ef5664dbcf95ef6234d496711278)) * fix: 修复用户列表在使用表前缀后报表不存在的问题 ([c980163](https://github.com/mineadmin/mineadmin/commit/c980163a92cd3d3c8b44b9761c049e150c9934ca)) ### ♻️ Code Refactoring * refactor: 优化watch支持8.2,兼容8.0和8.1 ([8bcb7a4](https://github.com/mineadmin/mineadmin/commit/8bcb7a4a41beb8c6df67e7613b6be49e71a6a214)) ## [v1.3.0] - 2023-05-25 ### ✨ Features * feat: mine 剥离 ([0e23e71](https://github.com/mineadmin/mineadmin/commit/0e23e719ecf7548141f0ecbbd2b3b4a5580104fd)) ### 🐛 Bug Fixes * fix: 移除配置项添加时,后端验证value必填 ([38d40fc](https://github.com/mineadmin/mineadmin/commit/38d40fcc265e2c85c5bb12a2809e0ee5cdba37d5)) * fix and refactor ([e92b6c5](https://github.com/mineadmin/mineadmin/commit/e92b6c5e615cd325a540ae07a712f5d178a52f61)) ### ♻️ Code Refactoring * refactor ([b83abb4](https://github.com/mineadmin/mineadmin/commit/b83abb47f1f529ad199e96444cf05bfb9605968d)) ## [v1.2.1] - 2023-05-23 ### ✨ Features * feat: 安装项目命令新增下载前端项目代码到 ./web 目录下 ([80dab0e](https://github.com/mineadmin/mineadmin/commit/80dab0e7accb7deb5851a9783d30b61fd7dd643f)) * feat: 增加重启服务脚本 ([06870c1](https://github.com/mineadmin/mineadmin/commit/06870c13ba16efb4b67a76165b5ac72fa8da0517)) * feat: 添加敏感词过滤,后续待添加管理功能 ([228e1b7](https://github.com/mineadmin/mineadmin/commit/228e1b763d5d10ee797562cc2251f5f31d4314fb)) ### 🐛 Bug Fixes * fix:修复数据迁移表名错误 fix:安装时没有清空超管默认部门数据 ([1130d25](https://github.com/mineadmin/mineadmin/commit/1130d2560723934fb74428fb1020c9a3e79b41d4)) * fix: 修复用户列表在查询部门用户的情况下子部门出现重复数据问题 ([d47c768](https://github.com/mineadmin/mineadmin/commit/d47c768258cd775cff13e35d43cfabe9ee05942e)) * fix trim value is null ([9bc0682](https://github.com/mineadmin/mineadmin/commit/9bc0682242720649611a07a158bb57c2ac9c3495)) * fix aarch64 systeminfo ([8f92f27](https://github.com/mineadmin/mineadmin/commit/8f92f2716c14573040b606b2849fcaa7115da5fb)) * fix: 执行定时任务命令方式时make无法实例化ArrayInput问题 ([d1b2f1d](https://github.com/mineadmin/mineadmin/commit/d1b2f1d615799989c29ee472f476f88745e39c56)) * fix: 修复pr ([d6821f2](https://github.com/mineadmin/mineadmin/commit/d6821f2eba837de852df0c4a6df670b44f440488)) * fix php version info ([547c1c0](https://github.com/mineadmin/mineadmin/commit/547c1c0aea350d1db27f980ef82cab455b3f5ceb)) ### ♻️ Code Refactoring * refactor `changStatus.stub` template ([459ced9](https://github.com/mineadmin/mineadmin/commit/459ced9e8d5a0f5cc2465ea976c69d03b217b8cf)) * refactor ([8164d5c](https://github.com/mineadmin/mineadmin/commit/8164d5c29cd4dfff1429c40a4322bcaa0adcd9bf)) * refactor(用户管理): 选择部门后下级部门人员不展示 ([1332825](https://github.com/mineadmin/mineadmin/commit/1332825663455f0331ee430649d352193f6f59de)) * refactor: 优化安装时下载前端项目逻辑 ([a912001](https://github.com/mineadmin/mineadmin/commit/a912001167cee7aef25b7f2d3ec67cb03be26610)) * refactor: 定制任务的删除缓存注解移到service上面去 ([c21c586](https://github.com/mineadmin/mineadmin/commit/c21c586bbc8d7339b706b31b1cfb51d5874248ce)) * refactor: 获取必应背景图片改为使用file\_get\_contents函数,增强兼容性 ([9965eb7](https://github.com/mineadmin/mineadmin/commit/9965eb71bc3898def4140385cbbdc2e455f58c0b)) ## [v1.2.0] - 2023-04-13 ### ✨ Features * feat: 新增获取每日必应背景图 ([b4fc22c](https://github.com/mineadmin/mineadmin/commit/b4fc22cfc2ec83dafda33f3c3776c32d11ef463f)) * feat: 新增数据源功能,代码生成器可以生成远程表结构到本地数据库 ([c639e91](https://github.com/mineadmin/mineadmin/commit/c639e91f81b70eff60c11392f0013b7b17db6b2a)) * feat ([31076d9](https://github.com/mineadmin/mineadmin/commit/31076d922d90228f1867f701a1ca8ddb81039ca9)) * feat: 抛出的异常全部允许跨域 ([9b3970b](https://github.com/mineadmin/mineadmin/commit/9b3970b4a8aeb3f1e2b22ea671b5b7a801fe73de)) * feat:数据源crud ([b668146](https://github.com/mineadmin/mineadmin/commit/b668146c1f8a87954c0a8bdaa8b234bd3ed74fa4)) * feat: 添加数据源表迁移文件 ([a00c52f](https://github.com/mineadmin/mineadmin/commit/a00c52f1757b1793cfaa41d805baa8e0d44b0a46)) * feat: 添加迁移回滚命令 mine:migrate-rollback --name=模块名 ([295a682](https://github.com/mineadmin/mineadmin/commit/295a6826cb0fefd3a4e38b5a7a1ebc6ae5601441)) * feat: 代码生成器添加tag页配置方式及选项 ([fe2874c](https://github.com/mineadmin/mineadmin/commit/fe2874c7cf162ee11bf00a4c4bce7da46e485569)) * feat: 新增附件列表无权限验证接口 ([1207f0b](https://github.com/mineadmin/mineadmin/commit/1207f0bd8770baacdcfb104b070cddb206122a18)) * feat: 代码生成条件增加in和not in ([4ccd4c7](https://github.com/mineadmin/mineadmin/commit/4ccd4c7d8ccc58a8c1283e79268a09d0fefc260d)) ### 🐛 Bug Fixes * fix: 修复Auth注解只获取method参数的,未获取class的bug ([df597fd](https://github.com/mineadmin/mineadmin/commit/df597fd4f08f87124f7b10112c8b6c91feceabe8)) * fix: 修复古老时期因使用雪花id造成队列消息的一个小bug ([05120ef](https://github.com/mineadmin/mineadmin/commit/05120ef1ed45ecf05653e6bc03fc4d08a12d1b1d)) * update mine/Helper/MineCaptcha.php ([6ed715d](https://github.com/mineadmin/mineadmin/commit/6ed715dc4edde7728a319d3e5726bcc3f5c424af)) * fix: 修复应用未绑定某接口也可以访问的bug ([5c6bbdc](https://github.com/mineadmin/mineadmin/commit/5c6bbdc98e3383f936639abfe95b742800acac2c)) ### ♻️ Code Refactoring * refactor: 优化excel导出支持超过26列 ([4e4c2dd](https://github.com/mineadmin/mineadmin/commit/4e4c2dd7d10217f49632a404a76b1819b3cfeadd)) * refactor: 多模块按order排序,避免初始化安装系统时,先安装自定义模块 感谢 @裘牧 贡献的代码 ([2aa3d71](https://github.com/mineadmin/mineadmin/commit/2aa3d7150db516cea80d79025561f6bcfcc83a4a)) * refactor: api文档接口增加分组列表数据 ([2854a04](https://github.com/mineadmin/mineadmin/commit/2854a043efd1eca1bea0e9fd4741709bdbd3298f)) ## [v1.1.1] - 2023-03-02 ### ✨ Features * feat: 系统添加默认允许跨域 ([c2e7a8f](https://github.com/mineadmin/mineadmin/commit/c2e7a8f03d2de4bb22db83cb71558bd8eabfe427)) ### 🐛 Bug Fixes * fix apple m1 cpu info and memory info ([e691c51](https://github.com/mineadmin/mineadmin/commit/e691c51dc3dc0dfdfff33366d00252327deb35f8)) ### ♻️ Code Refactoring * refactor: 使用前端默认的搜索标签宽度 ([c63f807](https://github.com/mineadmin/mineadmin/commit/c63f8078bcc6bd528f9c07ca9710af46e429e5f0)) * refactor: 适配新版前端crud组件 ([6b495ee](https://github.com/mineadmin/mineadmin/commit/6b495eecbd74193673b72ee9dee2f1814c96a203)) * refactor: 优化删除方法,兼容删除缓存数据 ([5d77e3d](https://github.com/mineadmin/mineadmin/commit/5d77e3d551172da433f58ad5446d2a8ec139617b)) * refactor: 定时任务、字典相关再更新、删除等操作后更新缓存 ([2e19362](https://github.com/mineadmin/mineadmin/commit/2e1936235b5a51bf4f35c02908462b6765daf35b)) * refactor:代码生成器模型模板加上类型 ([e567ab0](https://github.com/mineadmin/mineadmin/commit/e567ab0e62400b9a3e0746d8af8d6b2aa87db778)) ## [v1.1.0] - 2023-01-04 ### ✨ Features * feat: 用户改为多部门,部门新增设置领导。PS:使用 php bin/hyperf.php mine:update 升级数据库 ([55ace59](https://github.com/mineadmin/mineadmin/commit/55ace59c14c9333aa07aa3110f71cffdc9f0d93e)) * feat: 增强DTO导出注解,支持字典翻译功能 ([7556e52](https://github.com/mineadmin/mineadmin/commit/7556e5284619f5e143d38ec4cc2fcda92a04354f)) * feat: 新增几个接口 ([2e9d03b](https://github.com/mineadmin/mineadmin/commit/2e9d03b830647badc01c21774add1536f84bf2a5)) * feat: 代码生成器新增排序选项 ([bd179fc](https://github.com/mineadmin/mineadmin/commit/bd179fcfa7dad0d5421e4e5ce031e47c386aaabe)) * feat: 新增用户删除监听,删除用户同时让当前活跃用户状态失效 ([55eae42](https://github.com/mineadmin/mineadmin/commit/55eae42493831fc6ad30890f296c2386152a6311)) * feat: 新增用户添加和删除事件 ([c68a7e4](https://github.com/mineadmin/mineadmin/commit/c68a7e4ccd94ea049286f77d6433a1e4307d9b57)) ### 🐛 Bug Fixes * fix: 修复新增用户可能出现的请求超时 ([b86f10d](https://github.com/mineadmin/mineadmin/commit/b86f10d8107c6f733850e25cd4c3da6fde4f9687)) * fix: 配置保存报类型错误的问题 ([cec974d](https://github.com/mineadmin/mineadmin/commit/cec974d6b0c17357924c19263ab2c39441bfe068)) * fix: 修复数据权限本部门及子部门使用like查询的问题 ([896deca](https://github.com/mineadmin/mineadmin/commit/896deca5fecfc83f9128c7271aa26415fdec015b)) * fix: 修复saveAspect在定时任务下,无法获取头信息导致任务执行失败 ([c7b602e](https://github.com/mineadmin/mineadmin/commit/c7b602e7544a817874f9e4b0a549111e04964a79)) * fix: 修复 DemoApi.php 调用函数名称拼写错误问 ([c4bc571](https://github.com/mineadmin/mineadmin/commit/c4bc5710aa7bc5aa4cd311ab97d7bd1378730409)) * fix: 修复本部门和子部门数据权限bug以及获取部门树数据非顶级不显示bug ([ee18aa5](https://github.com/mineadmin/mineadmin/commit/ee18aa5f47350dd3885b1b34b388413cd8744066)) * fix: 修复获取当前用户部门id返回值类型不对问题 ([a223d61](https://github.com/mineadmin/mineadmin/commit/a223d6168f575fb5a56256cb458ea2e925e85dec)) * fix:修复类型不匹配导致选择文件存储类型失败 ([5b12759](https://github.com/mineadmin/mineadmin/commit/5b127594279c1332656f7ae3579bbeca42cb71dd)) * fix:修复上传功能找不到配置项问题 ([b7e08a3](https://github.com/mineadmin/mineadmin/commit/b7e08a34d1b0b0f09cd6b6c12f26b6f3251c75a4)) * fix: 修复之前改表字段名导致选择上传存储模式失效问题 ([fb77739](https://github.com/mineadmin/mineadmin/commit/fb77739218a81642311ba6934b56c63b8e5cdf0f)) * fix:修复代码生成器生成密码组件formType属性错误问题 ([59f3d53](https://github.com/mineadmin/mineadmin/commit/59f3d53f890026159da6a9fec313f91c7814972d)) * fix: 修复优化Mine.php造成获取模块地址出错 ([a1f384a](https://github.com/mineadmin/mineadmin/commit/a1f384a930060a6d4e6e872a544537acb02c4276)) * fix: 修复服务监控某些情况下可能出现变量未定义 ([d0aaf6a](https://github.com/mineadmin/mineadmin/commit/d0aaf6a6f72f2eebaa76088a913d6356e6036c75)) * fix: 修复记录删除定时任务日志时,业务名称为未定义菜单问题 ([02962d3](https://github.com/mineadmin/mineadmin/commit/02962d355889244b806454ef31ecbc05a18fb6ff)) * fix: 修复生成控制器生成用户选择器组件名字拼写错误 ([8564a0a](https://github.com/mineadmin/mineadmin/commit/8564a0aed7a5ea7b034919af83d576efd9a47949)) * fix: 修复代码生成器生成日期时间组件为范围选择的时候无效问题 ([2cf5cb2](https://github.com/mineadmin/mineadmin/commit/2cf5cb2b93087902fb1c5279459bd670e50ca0e3)) * fix: 修复生成控制器注释生成错误 ([a9fd121](https://github.com/mineadmin/mineadmin/commit/a9fd121e90cb0fe616f0138675a017455128189c)) * fix: 修复缓存监控和在线用户权限标识代码问题 ([b62c973](https://github.com/mineadmin/mineadmin/commit/b62c9739ff711a103e7869d0a5c40acc574e51e8)) * fix: 修复代码生成器未勾选必填项无效问题 ([55785f5](https://github.com/mineadmin/mineadmin/commit/55785f58ecf4f0879436358f52d6ed65440dc585)) * fix:修复代码生成器生成删除接口拼写错误 ([f6d1002](https://github.com/mineadmin/mineadmin/commit/f6d100264efaa36da9af8bf0257d7d714dd621cf)) * fix: 修复代码生成器配置显示组件无效问题 ([b11fc19](https://github.com/mineadmin/mineadmin/commit/b11fc19c8197acb2f8db36a4820ba87a6d9a79a0)) * fix: 修复代码生成器生成日期时间组件某些选项无效的问题 ([d03b35d](https://github.com/mineadmin/mineadmin/commit/d03b35d407f89ebd2601cfda8704c4b303f4566f)) * fix: 修复phpoffice驱动设置宽度无效和报数组未定义问题 ([10c2535](https://github.com/mineadmin/mineadmin/commit/10c2535621862b28298b4e1b52dfa05797a3aad7)) * fix:修复代码生成器少个花括号 ([bc071aa](https://github.com/mineadmin/mineadmin/commit/bc071aacdc105e06c2ff00893b37aacef1bcb2aa)) * fix:修复代码生成器缺失生成导入和导出 ([d7d5402](https://github.com/mineadmin/mineadmin/commit/d7d54028a4e4cc5b7dbf752eaf29e8cd35fbe9ed)) ### ♻️ Code Refactoring * refactor ([11af477](https://github.com/mineadmin/mineadmin/commit/11af477459b5b5e6f190163da66c3f309ddef7ec)) * refactor: 更新获取模块名称的逻辑,修复notice提示的问题 ([d0be1f7](https://github.com/mineadmin/mineadmin/commit/d0be1f7acd1cf3767aabe38abf111cf7e11411ec)) * refactor: 更新获取模块名称大小写逻辑 ([66128b2](https://github.com/mineadmin/mineadmin/commit/66128b2fe2c550826321bf889b4b0aa4cc7c58c1)) * refactor: 设置菜单权限获取数据逻辑变更,只能看到自己有权限的菜单 ([52d6bdc](https://github.com/mineadmin/mineadmin/commit/52d6bdce996d313e0829b37781df2bf4f1421499)) * refactor: 配置值适配最新的ma-form组件props ([5759776](https://github.com/mineadmin/mineadmin/commit/5759776d3fca124297d6c1ec44c6c8adf9ce2530)) * refactor: 优化表迁移创建结构 ([10c0ca8](https://github.com/mineadmin/mineadmin/commit/10c0ca8ea52b8b60485373ee1c8e80a3a6a23a5a)) * refactor: 优化代码生成器 ([7504422](https://github.com/mineadmin/mineadmin/commit/7504422cf999c4f6e3060c3c4758814aaf4709e1)) * refactor: 优化服务监控报错则返回无法获取信息 ([42ce9bc](https://github.com/mineadmin/mineadmin/commit/42ce9bc0f5fd1cdedc55e3d98d4d179b8f431eea)) * refactor: 更新README ([5553707](https://github.com/mineadmin/mineadmin/commit/5553707c9ff5913bb19cf2d90afd946ac7d2fe5d)) * refactor: 新增和保存切面优化 ([8af65a8](https://github.com/mineadmin/mineadmin/commit/8af65a8c40de07e1c23ab9c49d477a9342fa2343)) * refactor: 优化清空缓存 ([3ba8148](https://github.com/mineadmin/mineadmin/commit/3ba81485cdab5a486ab34b1fc6347b2d98fbe41f)) * refactor: 优化API返回数据类型格式,由自己控制 ([e260b91](https://github.com/mineadmin/mineadmin/commit/e260b913559eac1bdde85c02c0d6a6338f2c20f9)) * refactor: 优化获取缓存前缀赋予null默认值 ([b0e4514](https://github.com/mineadmin/mineadmin/commit/b0e4514fff4b4cba7878042911d54c09bf5d0a55)) * refactor: 升级依赖 ([95b785b](https://github.com/mineadmin/mineadmin/commit/95b785b4619d585cb38a4b3259a88c3d1627c84a)) * refactor: 优化Mine.php、MineController.php,删除$this->app()方法,内部调用改用container()函数 ([676f659](https://github.com/mineadmin/mineadmin/commit/676f65998d6fa836b9c68db614588e2976c9611c)) * refactor: 优化删除附件逻辑,改为删除附件时判断附件当时使用的存储方式。感谢@maimake贡献的代码 ([1d41597](https://github.com/mineadmin/mineadmin/commit/1d415972811de8046d99103a1423fbd3e2bfcbc0)) * refactor: README.md ([89d6e45](https://github.com/mineadmin/mineadmin/commit/89d6e45cc3a66f86ef36472b27e1b5243cd11eb1)) * refactor: vue生成模板更新 ([11848ff](https://github.com/mineadmin/mineadmin/commit/11848ff892ee0ce54cef1ab709fdbcfac295dafd)) * refactor: 代码生成器控制器生成列表添加父级权限 ([500be11](https://github.com/mineadmin/mineadmin/commit/500be11218505cb99e6f99bdf9cbabc564501534)) * refactor: 更新docker-composer ([51b6788](https://github.com/mineadmin/mineadmin/commit/51b6788200579af171c57eb839e3a73831bcfe0e)) * refactor: 更新所有权限注解的权限代码,以适配菜单只勾选父级菜单 ([ba44280](https://github.com/mineadmin/mineadmin/commit/ba44280d2510beae4e75b1a6c21b210193f46a84)) * refactor: 导出excel添加参数 ([9bda61a](https://github.com/mineadmin/mineadmin/commit/9bda61ad8e7f25e382d6530d8d81c79bf30bbaf3)) * refactor: 公共控制器增加登录和操作日志方法 ([f849b95](https://github.com/mineadmin/mineadmin/commit/f849b95e6f8cf0b528e0c4d26fbface2fb35bfec)) * refactor: 更换回阿里云的源 ([3125b07](https://github.com/mineadmin/mineadmin/commit/3125b07d87d36ed73385e371ab0b1e8be20c244b)) * refactor: 更新依赖 ([7716f3a](https://github.com/mineadmin/mineadmin/commit/7716f3a2a0ab82521e1ae2f257c86c0a0de1cb4e)) ## [v1.0.0] - 2022-08-24 ### 🐛 Bug Fixes * fix:修复代码生成一些配置无效问题 ([de1c39c](https://github.com/mineadmin/mineadmin/commit/de1c39cad23703bd683051166eb9c32a5dd62147)) * fix:修复代码生成缺少操作列参数 ([8b7b00e](https://github.com/mineadmin/mineadmin/commit/8b7b00e58988248d92d3f4cc921cc404a8119724)) * fix:修复时间搜索拼字符串缺少空格导致搜索为空 ([5e007d6](https://github.com/mineadmin/mineadmin/commit/5e007d65e5b92a252dd62e4c59cdf91021dc71fd)) * fix: 修复生成index.vue缺少引入Message ([8bafd25](https://github.com/mineadmin/mineadmin/commit/8bafd254236e4475726a726ede5bbb7a6ffafda6)) * fix: 还原api文件被意外替换 ([26032ac](https://github.com/mineadmin/mineadmin/commit/26032ac0706167c6490f62f4491b19498106f7df)) * fix: 移除无用指令 ([a9f7396](https://github.com/mineadmin/mineadmin/commit/a9f73960643ae2273381c89396933b950db34a8e)) * fix: 修复 inatall 安装失败 ([060f30b](https://github.com/mineadmin/mineadmin/commit/060f30b000a708f8ce2f5e0add68c769c9c43a5e)) * fix: 修复拼写错误 ([8160c8e](https://github.com/mineadmin/mineadmin/commit/8160c8e7bc5072a0e588e3b6e09d2edd3d5dfebf)) * fix: 兼容 php 8.1 ([6515688](https://github.com/mineadmin/mineadmin/commit/651568879703a52d334da026423121511c590613)) * fix: 兼容php8.1 ([6fe71ad](https://github.com/mineadmin/mineadmin/commit/6fe71ada2cb9f78558dfb3bf3428994986e89691)) * fixed the multiple primary key ([aafbc14](https://github.com/mineadmin/mineadmin/commit/aafbc14f40e2924984ae9f0e6642c80625e3d2b6)) ## [v0.7.2] - 2022-06-02 ## [v0.7.1] - 2022-05-31 ## [v0.7.0] - 2022-04-26 ## [v0.6.3] - 2022-04-12 ## [v0.6.2] - 2022-04-07 ## \[2.0.0-alpha.1] - 2023-12-19 ### ✨ Features * feat: UploadController.php 新增 showFile 方法,适配前端hash输入图片或文件 ([f029c32](https://github.com/mineadmin/mineadmin/commit/f029c32b2c283e62356f6013acbc2216b6fc0376)) * feat: 新增sys\_config() 和 sys\_group\_config() 函数 ([15985cf](https://github.com/mineadmin/mineadmin/commit/15985cff0eb228b6c490039e2dc65d177853e744)) * feat: 新增通用接口功能,变更版本为1.3.3 ([555de3e](https://github.com/mineadmin/mineadmin/commit/555de3e8ca846680901a82dce4a1321ff0d220d0)) * feat: mine 剥离 ([0e23e71](https://github.com/mineadmin/mineadmin/commit/0e23e719ecf7548141f0ecbbd2b3b4a5580104fd)) * feat: 安装项目命令新增下载前端项目代码到 ./web 目录下 ([80dab0e](https://github.com/mineadmin/mineadmin/commit/80dab0e7accb7deb5851a9783d30b61fd7dd643f)) * feat: 增加重启服务脚本 ([06870c1](https://github.com/mineadmin/mineadmin/commit/06870c13ba16efb4b67a76165b5ac72fa8da0517)) * feat: 添加敏感词过滤,后续待添加管理功能 ([228e1b7](https://github.com/mineadmin/mineadmin/commit/228e1b763d5d10ee797562cc2251f5f31d4314fb)) * feat: 新增获取每日必应背景图 ([b4fc22c](https://github.com/mineadmin/mineadmin/commit/b4fc22cfc2ec83dafda33f3c3776c32d11ef463f)) * feat: 新增数据源功能,代码生成器可以生成远程表结构到本地数据库 ([c639e91](https://github.com/mineadmin/mineadmin/commit/c639e91f81b70eff60c11392f0013b7b17db6b2a)) * feat ([31076d9](https://github.com/mineadmin/mineadmin/commit/31076d922d90228f1867f701a1ca8ddb81039ca9)) * feat: 抛出的异常全部允许跨域 ([9b3970b](https://github.com/mineadmin/mineadmin/commit/9b3970b4a8aeb3f1e2b22ea671b5b7a801fe73de)) * feat:数据源crud ([b668146](https://github.com/mineadmin/mineadmin/commit/b668146c1f8a87954c0a8bdaa8b234bd3ed74fa4)) * feat: 添加数据源表迁移文件 ([a00c52f](https://github.com/mineadmin/mineadmin/commit/a00c52f1757b1793cfaa41d805baa8e0d44b0a46)) * feat: 添加迁移回滚命令 mine:migrate-rollback --name=模块名 ([295a682](https://github.com/mineadmin/mineadmin/commit/295a6826cb0fefd3a4e38b5a7a1ebc6ae5601441)) * feat: 代码生成器添加tag页配置方式及选项 ([fe2874c](https://github.com/mineadmin/mineadmin/commit/fe2874c7cf162ee11bf00a4c4bce7da46e485569)) * feat: 新增附件列表无权限验证接口 ([1207f0b](https://github.com/mineadmin/mineadmin/commit/1207f0bd8770baacdcfb104b070cddb206122a18)) * feat: 代码生成条件增加in和not in ([4ccd4c7](https://github.com/mineadmin/mineadmin/commit/4ccd4c7d8ccc58a8c1283e79268a09d0fefc260d)) * feat: 系统添加默认允许跨域 ([c2e7a8f](https://github.com/mineadmin/mineadmin/commit/c2e7a8f03d2de4bb22db83cb71558bd8eabfe427)) * feat: 用户改为多部门,部门新增设置领导。PS:使用 php bin/hyperf.php mine:update 升级数据库 ([55ace59](https://github.com/mineadmin/mineadmin/commit/55ace59c14c9333aa07aa3110f71cffdc9f0d93e)) * feat: 增强DTO导出注解,支持字典翻译功能 ([7556e52](https://github.com/mineadmin/mineadmin/commit/7556e5284619f5e143d38ec4cc2fcda92a04354f)) * feat: 新增几个接口 ([2e9d03b](https://github.com/mineadmin/mineadmin/commit/2e9d03b830647badc01c21774add1536f84bf2a5)) * feat: 代码生成器新增排序选项 ([bd179fc](https://github.com/mineadmin/mineadmin/commit/bd179fcfa7dad0d5421e4e5ce031e47c386aaabe)) * feat: 新增用户删除监听,删除用户同时让当前活跃用户状态失效 ([55eae42](https://github.com/mineadmin/mineadmin/commit/55eae42493831fc6ad30890f296c2386152a6311)) * feat: 新增用户添加和删除事件 ([c68a7e4](https://github.com/mineadmin/mineadmin/commit/c68a7e4ccd94ea049286f77d6433a1e4307d9b57)) ### 🐛 Bug Fixes * fix library version ([5ebf0fb](https://github.com/mineadmin/mineadmin/commit/5ebf0fb321cc4f5fe99d6c6eb3f8183cb0d611ea)) * fix 适配3.1 ([e211f74](https://github.com/mineadmin/mineadmin/commit/e211f745ffd9548c44236531d739be54a260c9a2)) * fix 优化提示 ([6480ead](https://github.com/mineadmin/mineadmin/commit/6480eada83557d5cfa027aa2d6fea69ef61e6668)) * fix: 适配支持Hyperf 3.1 ([12d3953](https://github.com/mineadmin/mineadmin/commit/12d3953c34fb98198c9110b2588e189323ae8850)) * fix: 修复拼写错误 ([d24f85b](https://github.com/mineadmin/mineadmin/commit/d24f85ba5ca2fa28a1c12f64a7a7d1a6ed3bef85)) * fix: 修复获取配置文件信息拼写错误 ([d24f21a](https://github.com/mineadmin/mineadmin/commit/d24f21aebeb855fbe5c6c51efacef0f2cfa84469)) * fix: 修复查询字段名称写错的问题 ([a76e35b](https://github.com/mineadmin/mineadmin/commit/a76e35b7498483948c3a0100b039d5ee0ce67dc4)) * fix: 修复个人中心修改头像和资料会导致平权修改数据的漏洞 ([016f175](https://github.com/mineadmin/mineadmin/commit/016f175c1d53483da2e721a42a3e3a261f23cec6)) * fix: 修复个人中心获取登录和操作日志可平权查看数据的漏洞 ([12e5ca1](https://github.com/mineadmin/mineadmin/commit/12e5ca1d4bb229e44eed9a8c8c3d1287fb11d398)) * fix: 开启日志记录requestId ([4b04cad](https://github.com/mineadmin/mineadmin/commit/4b04cad52397e6172dbf8e4b72bf4b720c0cab74)) * fix: 修复上传的文件若在回收站则无法重新上传的问题 ([22267d1](https://github.com/mineadmin/mineadmin/commit/22267d1896567cf769fd3d559bd61413f4b2812d)) * fix: 修复更新系统配置时,提示 `config_select_data` 未定义的bug ([0cd2743](https://github.com/mineadmin/mineadmin/commit/0cd274349ae91c4f0558217e18e933533d82e627)) * fix: 创建setting\_datasource表之前,检查表是否存在 ([be0d45d](https://github.com/mineadmin/mineadmin/commit/be0d45d0050fc839b2f1e7d859406723f4d83b83)) * fix: 修复命名空间大小写问题 ([f63b596](https://github.com/mineadmin/mineadmin/commit/f63b5960e923497b37e6b14aa09330f07c18ec1c)) * fix: 修复系统配置对复选框支持不友好的问题 ([db6a335](https://github.com/mineadmin/mineadmin/commit/db6a3356554316d1e60992f82ae41e19925005b5)) * fix: 部门编辑报错 ([38293ff](https://github.com/mineadmin/mineadmin/commit/38293ff8997e99b03029038825052756e626d0d7)) * fix: 修复代码生成树表后添加数据时报错的问题 ps: composer update xmo/mine-core ([409000f](https://github.com/mineadmin/mineadmin/commit/409000fffcce6316e1cd33fd5e1c201bd9a3bca3)) * fix allow\_roles field cast to array ([33f6fd1](https://github.com/mineadmin/mineadmin/commit/33f6fd1e24e8801dce307490222bd179477782a6)) * fix: php 8.2 兼容 swoole>=4.4.6 PHP Deprecated: Swoole\Event::rshutdown(): ([13b9295](https://github.com/mineadmin/mineadmin/commit/13b92952ea36f7071be72125cbde0a5a7f031577)) * fix: 修复mine改成package后,生成代码时找不到模板文件 ([21c9ef7](https://github.com/mineadmin/mineadmin/commit/21c9ef76f2b8ef5664dbcf95ef6234d496711278)) * fix: 修复用户列表在使用表前缀后报表不存在的问题 ([c980163](https://github.com/mineadmin/mineadmin/commit/c980163a92cd3d3c8b44b9761c049e150c9934ca)) * fix: 移除配置项添加时,后端验证value必填 ([38d40fc](https://github.com/mineadmin/mineadmin/commit/38d40fcc265e2c85c5bb12a2809e0ee5cdba37d5)) * fix and refactor ([e92b6c5](https://github.com/mineadmin/mineadmin/commit/e92b6c5e615cd325a540ae07a712f5d178a52f61)) * fix:修复数据迁移表名错误 fix:安装时没有清空超管默认部门数据 ([1130d25](https://github.com/mineadmin/mineadmin/commit/1130d2560723934fb74428fb1020c9a3e79b41d4)) * fix: 修复用户列表在查询部门用户的情况下子部门出现重复数据问题 ([d47c768](https://github.com/mineadmin/mineadmin/commit/d47c768258cd775cff13e35d43cfabe9ee05942e)) * fix trim value is null ([9bc0682](https://github.com/mineadmin/mineadmin/commit/9bc0682242720649611a07a158bb57c2ac9c3495)) * fix aarch64 systeminfo ([8f92f27](https://github.com/mineadmin/mineadmin/commit/8f92f2716c14573040b606b2849fcaa7115da5fb)) * fix: 执行定时任务命令方式时make无法实例化ArrayInput问题 ([d1b2f1d](https://github.com/mineadmin/mineadmin/commit/d1b2f1d615799989c29ee472f476f88745e39c56)) * fix: 修复pr ([d6821f2](https://github.com/mineadmin/mineadmin/commit/d6821f2eba837de852df0c4a6df670b44f440488)) * fix php version info ([547c1c0](https://github.com/mineadmin/mineadmin/commit/547c1c0aea350d1db27f980ef82cab455b3f5ceb)) * fix: 修复Auth注解只获取method参数的,未获取class的bug ([df597fd](https://github.com/mineadmin/mineadmin/commit/df597fd4f08f87124f7b10112c8b6c91feceabe8)) * fix: 修复古老时期因使用雪花id造成队列消息的一个小bug ([05120ef](https://github.com/mineadmin/mineadmin/commit/05120ef1ed45ecf05653e6bc03fc4d08a12d1b1d)) * update mine/Helper/MineCaptcha.php ([6ed715d](https://github.com/mineadmin/mineadmin/commit/6ed715dc4edde7728a319d3e5726bcc3f5c424af)) * fix: 修复应用未绑定某接口也可以访问的bug ([5c6bbdc](https://github.com/mineadmin/mineadmin/commit/5c6bbdc98e3383f936639abfe95b742800acac2c)) * fix apple m1 cpu info and memory info ([e691c51](https://github.com/mineadmin/mineadmin/commit/e691c51dc3dc0dfdfff33366d00252327deb35f8)) * fix: 修复新增用户可能出现的请求超时 ([b86f10d](https://github.com/mineadmin/mineadmin/commit/b86f10d8107c6f733850e25cd4c3da6fde4f9687)) * fix: 配置保存报类型错误的问题 ([cec974d](https://github.com/mineadmin/mineadmin/commit/cec974d6b0c17357924c19263ab2c39441bfe068)) * fix: 修复数据权限本部门及子部门使用like查询的问题 ([896deca](https://github.com/mineadmin/mineadmin/commit/896deca5fecfc83f9128c7271aa26415fdec015b)) * fix: 修复saveAspect在定时任务下,无法获取头信息导致任务执行失败 ([c7b602e](https://github.com/mineadmin/mineadmin/commit/c7b602e7544a817874f9e4b0a549111e04964a79)) * fix: 修复 DemoApi.php 调用函数名称拼写错误问 ([c4bc571](https://github.com/mineadmin/mineadmin/commit/c4bc5710aa7bc5aa4cd311ab97d7bd1378730409)) * fix: 修复本部门和子部门数据权限bug以及获取部门树数据非顶级不显示bug ([ee18aa5](https://github.com/mineadmin/mineadmin/commit/ee18aa5f47350dd3885b1b34b388413cd8744066)) * fix: 修复获取当前用户部门id返回值类型不对问题 ([a223d61](https://github.com/mineadmin/mineadmin/commit/a223d6168f575fb5a56256cb458ea2e925e85dec)) * fix:修复类型不匹配导致选择文件存储类型失败 ([5b12759](https://github.com/mineadmin/mineadmin/commit/5b127594279c1332656f7ae3579bbeca42cb71dd)) * fix:修复上传功能找不到配置项问题 ([b7e08a3](https://github.com/mineadmin/mineadmin/commit/b7e08a34d1b0b0f09cd6b6c12f26b6f3251c75a4)) * fix: 修复之前改表字段名导致选择上传存储模式失效问题 ([fb77739](https://github.com/mineadmin/mineadmin/commit/fb77739218a81642311ba6934b56c63b8e5cdf0f)) * fix:修复代码生成器生成密码组件formType属性错误问题 ([59f3d53](https://github.com/mineadmin/mineadmin/commit/59f3d53f890026159da6a9fec313f91c7814972d)) * fix: 修复优化Mine.php造成获取模块地址出错 ([a1f384a](https://github.com/mineadmin/mineadmin/commit/a1f384a930060a6d4e6e872a544537acb02c4276)) * fix: 修复服务监控某些情况下可能出现变量未定义 ([d0aaf6a](https://github.com/mineadmin/mineadmin/commit/d0aaf6a6f72f2eebaa76088a913d6356e6036c75)) * fix: 修复记录删除定时任务日志时,业务名称为未定义菜单问题 ([02962d3](https://github.com/mineadmin/mineadmin/commit/02962d355889244b806454ef31ecbc05a18fb6ff)) * fix: 修复生成控制器生成用户选择器组件名字拼写错误 ([8564a0a](https://github.com/mineadmin/mineadmin/commit/8564a0aed7a5ea7b034919af83d576efd9a47949)) * fix: 修复代码生成器生成日期时间组件为范围选择的时候无效问题 ([2cf5cb2](https://github.com/mineadmin/mineadmin/commit/2cf5cb2b93087902fb1c5279459bd670e50ca0e3)) * fix: 修复生成控制器注释生成错误 ([a9fd121](https://github.com/mineadmin/mineadmin/commit/a9fd121e90cb0fe616f0138675a017455128189c)) * fix: 修复缓存监控和在线用户权限标识代码问题 ([b62c973](https://github.com/mineadmin/mineadmin/commit/b62c9739ff711a103e7869d0a5c40acc574e51e8)) * fix: 修复代码生成器未勾选必填项无效问题 ([55785f5](https://github.com/mineadmin/mineadmin/commit/55785f58ecf4f0879436358f52d6ed65440dc585)) * fix:修复代码生成器生成删除接口拼写错误 ([f6d1002](https://github.com/mineadmin/mineadmin/commit/f6d100264efaa36da9af8bf0257d7d714dd621cf)) * fix: 修复代码生成器配置显示组件无效问题 ([b11fc19](https://github.com/mineadmin/mineadmin/commit/b11fc19c8197acb2f8db36a4820ba87a6d9a79a0)) * fix: 修复代码生成器生成日期时间组件某些选项无效的问题 ([d03b35d](https://github.com/mineadmin/mineadmin/commit/d03b35d407f89ebd2601cfda8704c4b303f4566f)) * fix: 修复phpoffice驱动设置宽度无效和报数组未定义问题 ([10c2535](https://github.com/mineadmin/mineadmin/commit/10c2535621862b28298b4e1b52dfa05797a3aad7)) * fix:修复代码生成器少个花括号 ([bc071aa](https://github.com/mineadmin/mineadmin/commit/bc071aacdc105e06c2ff00893b37aacef1bcb2aa)) * fix:修复代码生成器缺失生成导入和导出 ([d7d5402](https://github.com/mineadmin/mineadmin/commit/d7d54028a4e4cc5b7dbf752eaf29e8cd35fbe9ed)) * fix:修复代码生成一些配置无效问题 ([de1c39c](https://github.com/mineadmin/mineadmin/commit/de1c39cad23703bd683051166eb9c32a5dd62147)) * fix:修复代码生成缺少操作列参数 ([8b7b00e](https://github.com/mineadmin/mineadmin/commit/8b7b00e58988248d92d3f4cc921cc404a8119724)) * fix:修复时间搜索拼字符串缺少空格导致搜索为空 ([5e007d6](https://github.com/mineadmin/mineadmin/commit/5e007d65e5b92a252dd62e4c59cdf91021dc71fd)) * fix: 修复生成index.vue缺少引入Message ([8bafd25](https://github.com/mineadmin/mineadmin/commit/8bafd254236e4475726a726ede5bbb7a6ffafda6)) * fix: 还原api文件被意外替换 ([26032ac](https://github.com/mineadmin/mineadmin/commit/26032ac0706167c6490f62f4491b19498106f7df)) * fix: 移除无用指令 ([a9f7396](https://github.com/mineadmin/mineadmin/commit/a9f73960643ae2273381c89396933b950db34a8e)) * fix: 修复 inatall 安装失败 ([060f30b](https://github.com/mineadmin/mineadmin/commit/060f30b000a708f8ce2f5e0add68c769c9c43a5e)) * fix: 兼容 php 8.1 ([6515688](https://github.com/mineadmin/mineadmin/commit/651568879703a52d334da026423121511c590613)) * fix: 兼容php8.1 ([6fe71ad](https://github.com/mineadmin/mineadmin/commit/6fe71ada2cb9f78558dfb3bf3428994986e89691)) * fixed the multiple primary key ([aafbc14](https://github.com/mineadmin/mineadmin/commit/aafbc14f40e2924984ae9f0e6642c80625e3d2b6)) ### ♻️ Code Refactoring * refactor: 更新mine-core核心包 ([059702d](https://github.com/mineadmin/mineadmin/commit/059702db5371a7995de0a3a259e939b033ab8a76)) * refactor: 关闭 buffer 输出大小限制 ([77731cf](https://github.com/mineadmin/mineadmin/commit/77731cfc33fd6a9d919836d6abd90cfc6f379587)) * refactor: 优化在开启数据权限后非超管账号添加部门时可选择父级部门为自身所在部门 ([d08e2db](https://github.com/mineadmin/mineadmin/commit/d08e2db3dc687a9e61fc03410033bd39bb713f85)) * refactor: 优化登录提示错误信息防止用户被枚举 ([25fa4d3](https://github.com/mineadmin/mineadmin/commit/25fa4d345888952deec2c5b8ca61b17819eb8128)) * refactor: 感谢最菜兄优化 `bin/reboot.php`,mine-core的amqp队列监听器移动到 App\System\Listener 下,升级mine-core ([b3362d9](https://github.com/mineadmin/mineadmin/commit/b3362d9cc6b0eae6f068796d94a2c0b6002901af)) * refactor: 业务里的isset替换为 !empty ([f724295](https://github.com/mineadmin/mineadmin/commit/f724295a2ef10c080331f5dcdbed7a9a302e9fec)) * refactor ([6fc5f01](https://github.com/mineadmin/mineadmin/commit/6fc5f01a2e2955b3b1a1818749dea4f745fc1b55)) * refactor: 优化api抛出异常信息提示 ([1ef5d1e](https://github.com/mineadmin/mineadmin/commit/1ef5d1e0c0d2929e47e6614a6787e46304f82359)) * refactor: 优化watch支持8.2,兼容8.0和8.1 ([8bcb7a4](https://github.com/mineadmin/mineadmin/commit/8bcb7a4a41beb8c6df67e7613b6be49e71a6a214)) * refactor `changStatus.stub` template ([459ced9](https://github.com/mineadmin/mineadmin/commit/459ced9e8d5a0f5cc2465ea976c69d03b217b8cf)) * refactor(用户管理): 选择部门后下级部门人员不展示 ([1332825](https://github.com/mineadmin/mineadmin/commit/1332825663455f0331ee430649d352193f6f59de)) * refactor: 优化安装时下载前端项目逻辑 ([a912001](https://github.com/mineadmin/mineadmin/commit/a912001167cee7aef25b7f2d3ec67cb03be26610)) * refactor: 定制任务的删除缓存注解移到service上面去 ([c21c586](https://github.com/mineadmin/mineadmin/commit/c21c586bbc8d7339b706b31b1cfb51d5874248ce)) * refactor: 获取必应背景图片改为使用file\_get\_contents函数,增强兼容性 ([9965eb7](https://github.com/mineadmin/mineadmin/commit/9965eb71bc3898def4140385cbbdc2e455f58c0b)) * refactor: 优化excel导出支持超过26列 ([4e4c2dd](https://github.com/mineadmin/mineadmin/commit/4e4c2dd7d10217f49632a404a76b1819b3cfeadd)) * refactor: 多模块按order排序,避免初始化安装系统时,先安装自定义模块 感谢 @裘牧 贡献的代码 ([2aa3d71](https://github.com/mineadmin/mineadmin/commit/2aa3d7150db516cea80d79025561f6bcfcc83a4a)) * refactor: api文档接口增加分组列表数据 ([2854a04](https://github.com/mineadmin/mineadmin/commit/2854a043efd1eca1bea0e9fd4741709bdbd3298f)) * refactor: 使用前端默认的搜索标签宽度 ([c63f807](https://github.com/mineadmin/mineadmin/commit/c63f8078bcc6bd528f9c07ca9710af46e429e5f0)) * refactor: 适配新版前端crud组件 ([6b495ee](https://github.com/mineadmin/mineadmin/commit/6b495eecbd74193673b72ee9dee2f1814c96a203)) * refactor: 优化删除方法,兼容删除缓存数据 ([5d77e3d](https://github.com/mineadmin/mineadmin/commit/5d77e3d551172da433f58ad5446d2a8ec139617b)) * refactor: 定时任务、字典相关再更新、删除等操作后更新缓存 ([2e19362](https://github.com/mineadmin/mineadmin/commit/2e1936235b5a51bf4f35c02908462b6765daf35b)) * refactor:代码生成器模型模板加上类型 ([e567ab0](https://github.com/mineadmin/mineadmin/commit/e567ab0e62400b9a3e0746d8af8d6b2aa87db778)) * refactor: 更新获取模块名称的逻辑,修复notice提示的问题 ([d0be1f7](https://github.com/mineadmin/mineadmin/commit/d0be1f7acd1cf3767aabe38abf111cf7e11411ec)) * refactor: 更新获取模块名称大小写逻辑 ([66128b2](https://github.com/mineadmin/mineadmin/commit/66128b2fe2c550826321bf889b4b0aa4cc7c58c1)) * refactor: 设置菜单权限获取数据逻辑变更,只能看到自己有权限的菜单 ([52d6bdc](https://github.com/mineadmin/mineadmin/commit/52d6bdce996d313e0829b37781df2bf4f1421499)) * refactor: 配置值适配最新的ma-form组件props ([5759776](https://github.com/mineadmin/mineadmin/commit/5759776d3fca124297d6c1ec44c6c8adf9ce2530)) * refactor: 优化表迁移创建结构 ([10c0ca8](https://github.com/mineadmin/mineadmin/commit/10c0ca8ea52b8b60485373ee1c8e80a3a6a23a5a)) * refactor: 优化代码生成器 ([7504422](https://github.com/mineadmin/mineadmin/commit/7504422cf999c4f6e3060c3c4758814aaf4709e1)) * refactor: 优化服务监控报错则返回无法获取信息 ([42ce9bc](https://github.com/mineadmin/mineadmin/commit/42ce9bc0f5fd1cdedc55e3d98d4d179b8f431eea)) * refactor: 更新README ([5553707](https://github.com/mineadmin/mineadmin/commit/5553707c9ff5913bb19cf2d90afd946ac7d2fe5d)) * refactor: 新增和保存切面优化 ([8af65a8](https://github.com/mineadmin/mineadmin/commit/8af65a8c40de07e1c23ab9c49d477a9342fa2343)) * refactor: 优化清空缓存 ([3ba8148](https://github.com/mineadmin/mineadmin/commit/3ba81485cdab5a486ab34b1fc6347b2d98fbe41f)) * refactor: 优化API返回数据类型格式,由自己控制 ([e260b91](https://github.com/mineadmin/mineadmin/commit/e260b913559eac1bdde85c02c0d6a6338f2c20f9)) * refactor: 优化获取缓存前缀赋予null默认值 ([b0e4514](https://github.com/mineadmin/mineadmin/commit/b0e4514fff4b4cba7878042911d54c09bf5d0a55)) * refactor: 升级依赖 ([95b785b](https://github.com/mineadmin/mineadmin/commit/95b785b4619d585cb38a4b3259a88c3d1627c84a)) * refactor: 优化Mine.php、MineController.php,删除$this->app()方法,内部调用改用container()函数 ([676f659](https://github.com/mineadmin/mineadmin/commit/676f65998d6fa836b9c68db614588e2976c9611c)) * refactor: 优化删除附件逻辑,改为删除附件时判断附件当时使用的存储方式。感谢@maimake贡献的代码 ([1d41597](https://github.com/mineadmin/mineadmin/commit/1d415972811de8046d99103a1423fbd3e2bfcbc0)) * refactor: README.md ([89d6e45](https://github.com/mineadmin/mineadmin/commit/89d6e45cc3a66f86ef36472b27e1b5243cd11eb1)) * refactor: vue生成模板更新 ([11848ff](https://github.com/mineadmin/mineadmin/commit/11848ff892ee0ce54cef1ab709fdbcfac295dafd)) * refactor: 代码生成器控制器生成列表添加父级权限 ([500be11](https://github.com/mineadmin/mineadmin/commit/500be11218505cb99e6f99bdf9cbabc564501534)) * refactor: 更新docker-composer ([51b6788](https://github.com/mineadmin/mineadmin/commit/51b6788200579af171c57eb839e3a73831bcfe0e)) * refactor: 更新所有权限注解的权限代码,以适配菜单只勾选父级菜单 ([ba44280](https://github.com/mineadmin/mineadmin/commit/ba44280d2510beae4e75b1a6c21b210193f46a84)) * refactor: 导出excel添加参数 ([9bda61a](https://github.com/mineadmin/mineadmin/commit/9bda61ad8e7f25e382d6530d8d81c79bf30bbaf3)) * refactor: 公共控制器增加登录和操作日志方法 ([f849b95](https://github.com/mineadmin/mineadmin/commit/f849b95e6f8cf0b528e0c4d26fbface2fb35bfec)) * refactor: 更换回阿里云的源 ([3125b07](https://github.com/mineadmin/mineadmin/commit/3125b07d87d36ed73385e371ab0b1e8be20c244b)) * refactor: 更新依赖 ([7716f3a](https://github.com/mineadmin/mineadmin/commit/7716f3a2a0ab82521e1ae2f257c86c0a0de1cb4e)) [Unreleased]: https://github.com/mineadmin/mineadmin/compare/v3.0.6...HEAD [v.1.1.2]: https://github.com/mineadmin/mineadmin/compare/v3.0.6...v.1.1.2 [v3.0.6]: https://github.com/mineadmin/mineadmin/compare/v3.0.5...v3.0.6 [v3.0.5]: https://github.com/mineadmin/mineadmin/compare/v3.0.4...v3.0.5 [v3.0.4]: https://github.com/mineadmin/mineadmin/compare/v3.0.3...v3.0.4 [v3.0.3]: https://github.com/mineadmin/mineadmin/compare/v3.0.2...v3.0.3 [v3.0.2]: https://github.com/mineadmin/mineadmin/compare/v3.0.1...v3.0.2 [v3.0.1]: https://github.com/mineadmin/mineadmin/compare/v3.0-RC...v3.0.1 [v3.0-RC]: https://github.com/mineadmin/mineadmin/compare/v3.0...v3.0-RC [v3.0]: https://github.com/mineadmin/mineadmin/compare/v2.0.3...v3.0 [v2.0.3]: https://github.com/mineadmin/mineadmin/compare/v2.0.2...v2.0.3 [v2.0.2]: https://github.com/mineadmin/mineadmin/compare/v2.0.1.1...v2.0.2 [v2.0.1.1]: https://github.com/mineadmin/mineadmin/compare/v2.0.1...v2.0.1.1 [v2.0.1]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-beta.6...v2.0.1 [v2.0.0-beta.6]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-beta.5...v2.0.0-beta.6 [v2.0.0-beta.5]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-beta.4...v2.0.0-beta.5 [v2.0.0-beta.4]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-beta.3...v2.0.0-beta.4 [v2.0.0-beta.3]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-beta.2...v2.0.0-beta.3 [v2.0.0-beta.2]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-beta.1...v2.0.0-beta.2 [v2.0.0-beta.1]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-beta...v2.0.0-beta.1 [v2.0.0-beta]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-alpha.5...v2.0.0-beta [v2.0.0-alpha.5]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-alpha.4...v2.0.0-alpha.5 [v2.0.0-alpha.4]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-alpha.3...v2.0.0-alpha.4 [v2.0.0-alpha.3]: https://github.com/mineadmin/mineadmin/compare/v2.0.0-alpha.2...v2.0.0-alpha.3 [v2.0.0-alpha.2]: https://github.com/mineadmin/mineadmin/compare/v2.0-stable...v2.0.0-alpha.2 [v2.0-stable]: https://github.com/mineadmin/mineadmin/compare/v2.0-RC.1...v2.0-stable [v2.0-RC.1]: https://github.com/mineadmin/mineadmin/compare/v1.4.13...v2.0-RC.1 [v1.4.13]: https://github.com/mineadmin/mineadmin/compare/v1.4.12...v1.4.13 [v1.4.12]: https://github.com/mineadmin/mineadmin/compare/v1.4.11...v1.4.12 [v1.4.11]: https://github.com/mineadmin/mineadmin/compare/v1.4.1...v1.4.11 [v1.4.1]: https://github.com/mineadmin/mineadmin/compare/v1.4.x...v1.4.1 [v1.4.x]: https://github.com/mineadmin/mineadmin/compare/v1.3.3...v1.4.x [v1.3.3]: https://github.com/mineadmin/mineadmin/compare/v1.3.0...v1.3.3 [v1.3.0]: https://github.com/mineadmin/mineadmin/compare/v1.2.1...v1.3.0 [v1.2.1]: https://github.com/mineadmin/mineadmin/compare/v1.2.0...v1.2.1 [v1.2.0]: https://github.com/mineadmin/mineadmin/compare/v1.1.1...v1.2.0 [v1.1.1]: https://github.com/mineadmin/mineadmin/compare/v1.1.0...v1.1.1 [v1.1.0]: https://github.com/mineadmin/mineadmin/compare/v1.0.0...v1.1.0 [v1.0.0]: https://github.com/mineadmin/mineadmin/compare/v0.7.2...v1.0.0 [v0.7.2]: https://github.com/mineadmin/mineadmin/compare/v0.7.1...v0.7.2 [v0.7.1]: https://github.com/mineadmin/mineadmin/compare/v0.7.0...v0.7.1 [v0.7.0]: https://github.com/mineadmin/mineadmin/compare/v0.6.3...v0.7.0 [v0.6.3]: https://github.com/mineadmin/mineadmin/compare/v0.6.2...v0.6.3 [v0.6.2]: https://github.com/mineadmin/mineadmin/compare/2.0.0-alpha.1...v0.6.2 --- --- url: /v3/plugin/configProvider.md --- # ConfigProvider 说明 如何配置ConfigProvider,发布应用自己的配置文件 *** ## 机制说明 **本套机制衍生于Hyperf的ConfigProvier机制** `ConfigProvider` 机制对于 `Hyperf` 组件化来说是个非常重要的机制,`组件间的解耦` 和 `组件的独立性` 以及 `组件的可重用性` 都是基于这个机制才得以实现。 简单来说,就是每个组件都会提供一个 `ConfigProvider`,通常是在组件的根目录提供一个 `ConfigProvider` 的类,`ConfigProvider` 会提供对应组件的所有配置信息,这些信息都会被 `Hyperf` 框架在启动时加载,最终 `ConfigProvider` 内的配置信息会被合并到 `Hyperf\Contract\ConfigInterface` 对应的实现类去,从而实现各个组件在 `Hyperf` 框架下使用时要进行的配置初始化。 `ConfigProvider` 本身不具备任何依赖,不继承任何的抽象类和不要求实现任何的接口,只需提供一个 `__invoke` 方法并返回一个对应配置结构的数组即可。 ## 发布自己的配置文件 只需要在数组结构中定义了 `publish` 项,设置好以下几个参数即可,再安装`MineAdmin`应用时,这些配置文件会自动发布到`config/autoload`目录里去 具体的可查看后面的示例代码 * id * description * source * destination ::: tip 合并到配置文件并不是物理合并,而是在系统启动时,hyperf把配置在内存中合并了,可通过获取配置文件的函数打印就明白了。 ::: ## ConfigProvider 示例 以下是一个示例 ```php [ConfigProvider.php] [ 'scan' => [ 'paths' => [ __DIR__, ], ], ], // 合并到 config/autoload/dependencies.php 文件 'dependencies' => [], // 默认 Command 的定义,换个方式理解也就是与 config/autoload/commands.php 对应 'commands' => [], // 与 commands 类似 'listeners' => [], // 组件默认配置文件,即执行命令后会把 source 的对应的文件复制为 destination 对应的的文件 'publish' => [ [ 'id' => 'config', 'description' => 'description of this config file.', // 描述 // 建议默认配置放在 publish 文件夹中,文件命名和组件名称相同 'source' => __DIR__ . '/../publish/appstore.php', // 对应的配置文件路径 'destination' => BASE_PATH . '/config/autoload/appstore.php', // 复制为这个路径下的该文件 ], ], ]; } } ``` --- --- url: /v3/front/high/hooks.md --- # Hooks MineAdmin 提供了一系列强大的自定义 Hooks,这些 Hooks 封装了常用的功能和逻辑,让开发者能够轻松地在 Vue 3 组件中复用代码。本文档将详细介绍每个 Hook 的用法、参数、返回值以及实际应用场景。 ## useCache() 用于浏览器缓存操作的 Hook,支持 localStorage 和 sessionStorage,并提供了过期时间设置功能。 **源码路径:** `/web/src/hooks/useCache.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useCache.ts) ### 类型定义 ```typescript export type CacheType = 'localStorage' | 'sessionStorage' export interface CacheOptions { /** * 超时时间,秒。 * 默认无限大。 */ exp?: number /** * 为true时:当超过最大容量导致无法继续插入数据操作时,先清空缓存中已超时的内容后再尝试插入数据操作。 * 默认为true。 */ force?: boolean } ``` ### 参数 | 参数 | 类型 | 默认值 | 描述 | |------|------|--------|------| | type | CacheType | 'localStorage' | 缓存类型,可选 localStorage 或 sessionStorage | ### 返回值 | 属性 | 类型 | 描述 | |------|------|------| | cache | WebStorageCache | 底层的 WebStorageCache 实例 | | prefix | string | 缓存键名前缀 | | set | Function | 设置缓存 | | get | Function | 获取缓存 | | remove | Function | 删除缓存 | | removeAllExpires | Function | 删除所有过期缓存 | | touch | Function | 更新缓存过期时间 | ### 使用示例 ```typescript import useCache from '@/hooks/useCache' // 使用 localStorage(默认) const { set, get, remove, removeAllExpires, touch } = useCache() // 使用 sessionStorage const sessionCache = useCache('sessionStorage') // 设置缓存(不过期) set('userInfo', { name: 'MineAdmin', role: 'admin' }) // 设置缓存(30秒后过期) set('tempData', 'temporary value', { exp: 30 }) // 获取缓存 const userInfo = get('userInfo', null) // 删除特定缓存 remove('tempData') // 删除所有过期缓存 removeAllExpires() // 更新缓存过期时间(延长60秒) touch('userInfo', 60) ``` ### 实际应用场景 ```typescript // 在用户登录组件中使用 const { set, get } = useCache() // 保存用户登录信息 const saveUserInfo = (userInfo: any) => { set('userInfo', userInfo, { exp: 24 * 60 * 60 }) // 24小时后过期 } // 获取用户信息 const getUserInfo = () => { return get('userInfo', null) } ``` ## useDialog() 用于创建对话框的 Hook,提供了完整的对话框生命周期管理,支持自定义标题、属性和事件回调。 **源码路径:** `/web/src/hooks/useDialog.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useDialog.ts) ### 类型定义 ```typescript export interface UseDialogExpose { on: { ok?: (...args: any[]) => void cancel?: (...args: any[]) => void } Dialog: Component open: (...args: any[]) => void close: () => void setTitle: (title: string) => void setAttr: (attr: Record) => void } ``` ### 参数 | 参数 | 类型 | 默认值 | 描述 | |------|------|--------|------| | dialogProps | Record\ | null | null | 对话框初始属性配置 | ### 返回值 | 属性 | 类型 | 描述 | |------|------|------| | on | Object | 事件回调配置 | | Dialog | Component | 对话框组件 | | open | Function | 打开对话框 | | close | Function | 关闭对话框 | | setTitle | Function | 设置对话框标题 | | setAttr | Function | 设置对话框属性 | ### 使用示例 ```typescript import useDialog from '@/hooks/useDialog' export default defineComponent({ setup() { // 创建对话框实例 const { Dialog, open, close, setTitle, on } = useDialog({ width: '500px', draggable: true }) // 配置事件回调 on.ok = () => { console.log('用户点击了确定') close() } on.cancel = () => { console.log('用户点击了取消') return true // 返回 true 允许关闭 } // 打开对话框 const openDialog = () => { setTitle('编辑用户信息') open({ userId: 123 }) } return { Dialog, openDialog } }, render() { return (
打开对话框
这里是对话框内容
) } }) ``` ### 实际应用场景 ```vue ``` ## useEcharts() 用于集成 ECharts 图表库的 Hook,提供了主题切换和图表初始化功能。 **源码路径:** `/web/src/hooks/useEcharts.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useEcharts.ts) ### 导出函数 | 函数 | 类型 | 描述 | |------|------|------| | useEcharts | Function | 来自 @mineadmin/echarts 的 useEcharts 函数 | | themeMode | Function | 获取当前主题模式 | ### 使用示例 ```typescript import { useEcharts, themeMode } from '@/hooks/useEcharts' export default defineComponent({ setup() { const chartRef = ref() onMounted(async () => { // 初始化图表 const chart = await useEcharts(chartRef.value) // 配置图表选项 const option = { title: { text: '销售数据' }, theme: themeMode(), // 使用当前主题 xAxis: { type: 'category', data: ['一月', '二月', '三月', '四月', '五月'] }, yAxis: { type: 'value' }, series: [{ data: [820, 932, 901, 934, 1290], type: 'bar' }] } chart.setOption(option) }) return { chartRef } }, render() { return
} }) ``` ### 实际应用场景 ```vue ``` ## useForm() 用于表单操作的 Hook,提供了表单实例的获取和操作功能。 **源码路径:** `/web/src/hooks/useForm.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useForm.ts) ### 参数 | 参数 | 类型 | 描述 | |------|------|------| | refName | string | 表单引用名称 | ### 返回值 返回一个 Promise,解析为 `MaFormExpose` 类型的表单实例。 ### 使用示例 ```typescript import useForm from '@/hooks/useForm' export default defineComponent({ setup() { const formRef = ref() // 获取表单实例 const getFormInstance = async () => { try { const formInstance = await useForm('userForm') // 使用表单实例进行操作 await formInstance.validate() const formData = formInstance.getFieldsValue() console.log('表单数据:', formData) } catch (error) { console.error('表单验证失败:', error) } } return { formRef, getFormInstance } } }) ``` ### 实际应用场景 ```vue ``` ## useTable() 用于表格操作的 Hook,提供了表格实例的获取和操作功能。 **源码路径:** `/web/src/hooks/useTable.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useTable.ts) ### 参数 | 参数 | 类型 | 描述 | |------|------|------| | refName | string | 表格引用名称 | ### 返回值 返回一个 Promise,解析为 `MaTableExpose` 类型的表格实例。 ### 使用示例 ```typescript import useTable from '@/hooks/useTable' export default defineComponent({ setup() { // 获取表格实例 const getTableInstance = async () => { try { const tableInstance = await useTable('userTable') // 刷新表格数据 await tableInstance.refresh() // 获取选中的行 const selectedRows = tableInstance.getSelectedRows() console.log('选中的行:', selectedRows) // 清空选择 tableInstance.clearSelection() } catch (error) { console.error('获取表格实例失败:', error) } } return { getTableInstance } } }) ``` ### 实际应用场景 ```vue ``` ## useLocalTrans() 用于本地化翻译的 Hook,提供了基于 vue-i18n 的翻译功能。 **源码路径:** `/web/src/hooks/useLocalTrans.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useLocalTrans.ts) ### 参数 | 参数 | 类型 | 默认值 | 描述 | |------|------|--------|------| | key | any | null | null | 翻译键名,为 null 时返回翻译函数 | ### 返回值 * 当 `key` 为 null 时,返回翻译函数 `ComposerTranslation` * 当 `key` 有值时,返回翻译后的字符串 ### 使用示例 ```typescript import { useLocalTrans } from '@/hooks/useLocalTrans' export default defineComponent({ setup() { // 获取翻译函数 const t = useLocalTrans() // 直接翻译 const title = useLocalTrans('user.title') const message = useLocalTrans('user.welcome', { name: 'MineAdmin' }) return { t, title, message } }, render() { return (

{this.title}

{this.message}

{this.t("user.description")}

) } }) ``` ### 实际应用场景 ```vue ``` ## useMessage() 用于消息提示的 Hook,封装了 Element Plus 的消息组件,提供统一的消息提示接口。 **源码路径:** `/web/src/hooks/useMessage.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useMessage.ts) ### 返回值 | 方法 | 参数 | 描述 | |------|------|------| | info | (content: string) | 显示信息消息 | | error | (content: string) | 显示错误消息 | | success | (content: string) | 显示成功消息 | | warning | (content: string) | 显示警告消息 | | alert | (content: string) | 显示提示框 | | alertError | (content: string) | 显示错误提示框 | | alertSuccess | (content: string) | 显示成功提示框 | | alertWarning | (content: string) | 显示警告提示框 | | notify | (content: string, args?: Record\) | 显示通知 | | notifyError | (content: string) | 显示错误通知 | | notifySuccess | (content: string) | 显示成功通知 | | notifyWarning | (content: string) | 显示警告通知 | | confirm | (content: string, tip?: string) | 显示确认框 | | delConfirm | (content?: string, tip?: string) | 显示删除确认框 | | exportConfirm | (content?: string, tip?: string) | 显示导出确认框 | | prompt | (content: string, defaultValue?: string, tip?: string, inputValidator?: MessageBoxInputValidator) | 显示输入框 | ### 使用示例 ```typescript import { useMessage } from '@/hooks/useMessage' export default defineComponent({ setup() { const message = useMessage() const handleSuccess = () => { message.success('操作成功!') } const handleError = () => { message.error('操作失败,请重试') } const handleConfirm = async () => { try { await message.confirm('确定要执行此操作吗?') console.log('用户确认了操作') } catch { console.log('用户取消了操作') } } const handleDelete = async () => { try { await message.delConfirm() console.log('执行删除操作') } catch { console.log('取消删除') } } return { handleSuccess, handleError, handleConfirm, handleDelete } } }) ``` ### 实际应用场景 ```vue ``` ## useTabCollection() 用于标签页收藏的 Hook,允许用户收藏和管理常用的标签页。 **源码路径:** `/web/src/hooks/useTabCollection.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useTabCollection.ts) ### 返回值 | 属性 | 类型 | 描述 | |------|------|------| | tabCollection | Ref\ | 收藏的标签页列表 | | addToCollection | Function | 添加标签页到收藏 | | removeCollection | Function | 从收藏中移除标签页 | ### 使用示例 ```typescript import useTabCollection from '@/hooks/useTabCollection' export default defineComponent({ setup() { const { tabCollection, addToCollection, removeCollection } = useTabCollection() // 添加当前标签页到收藏 const addCurrentTab = () => { addToCollection() // 不传参数会自动获取当前标签页 } // 添加指定标签页到收藏 const addSpecificTab = () => { const tab = { name: 'userList', title: '用户列表', path: '/user/list', fullPath: '/user/list?status=active', icon: 'user' } addToCollection(tab) } // 移除收藏 const removeTab = (tab) => { removeCollection(tab) } return { tabCollection, addCurrentTab, addSpecificTab, removeTab } } }) ``` ### 实际应用场景 ```vue ``` ## useImageViewer() 用于图片预览的 Hook,基于 Element Plus 的 ImageViewer 组件,支持多图片浏览。 **源码路径:** `/web/src/hooks/useImageViewer.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useImageViewer.ts) ### 类型定义 ```typescript type Options = Partial> ``` ### 参数 | 参数 | 类型 | 描述 | |------|------|------| | images | string\[] | 图片地址数组 | | options | Options | 图片查看器配置选项 | ### 使用示例 ```typescript import { useImageViewer } from '@/hooks/useImageViewer' export default defineComponent({ setup() { const previewImages = () => { const images = [ 'https://example.com/image1.jpg', 'https://example.com/image2.jpg', 'https://example.com/image3.jpg' ] useImageViewer(images, { initialIndex: 0, // 初始显示第一张图片 zIndex: 3000, // 设置 z-index hideOnClickModal: true // 点击遮罩层关闭 }) } return { previewImages } } }) ``` ### 实际应用场景 ```vue ``` ## useResourcePicker() 用于资源选择的 Hook,提供了文件、图片等资源的选择功能。 **源码路径:** `/web/src/hooks/useResourcePicker.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useResourcePicker.ts) ### 类型定义 ```typescript export type WithOnEventListeners = { [K in keyof T as `on${Capitalize}`]?: T[K]; } type Options = Partial> ``` ### 参数 | 参数 | 类型 | 描述 | |------|------|------| | options | Options | 资源选择器配置选项 | ### 使用示例 ```typescript import { useResourcePicker } from '@/hooks/useResourcePicker' export default defineComponent({ setup() { const selectSingleFile = () => { useResourcePicker({ multiple: false, defaultFileType: 'image', onConfirm: (resources) => { console.log('选择的资源:', resources) // 处理选中的资源 if (resources.length > 0) { const selectedFile = resources[0] console.log('文件名:', selectedFile.origin_name) console.log('文件URL:', selectedFile.url) } }, onCancel: () => { console.log('用户取消了选择') } }) } const selectMultipleImages = () => { useResourcePicker({ multiple: true, limit: 5, // 最多选择5个文件 defaultFileType: 'image', onConfirm: (resources) => { console.log('选择的图片:', resources) // 批量处理图片 resources.forEach(image => { console.log(`图片: ${image.origin_name}, URL: ${image.url}`) }) } }) } return { selectSingleFile, selectMultipleImages } } }) ``` ### 实际应用场景 ```vue ``` ## useWatermark() 用于添加水印的 Hook,支持文字水印的添加和清除,自动适配深色和浅色主题。 **源码路径:** `/web/src/hooks/useWatermark.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useWatermark.ts) ### 参数 | 参数 | 类型 | 默认值 | 描述 | |------|------|--------|------| | appendEl | HTMLElement | null | document.body | 水印添加到的目标元素 | ### 返回值 | 方法 | 参数 | 描述 | |------|------|------| | setWatermark | (str: string | string\[]) | 设置水印文字 | | clear | () | 清除水印 | ### 使用示例 ```typescript import useWatermark from '@/hooks/useWatermark' export default defineComponent({ setup() { // 默认添加到 document.body const { setWatermark, clear } = useWatermark() // 添加到指定元素 const containerRef = ref() const { setWatermark: setContainerWatermark, clear: clearContainer } = useWatermark(containerRef.value) onMounted(() => { // 设置单行水印 setWatermark('MineAdmin 内部系统') // 设置多行水印 setWatermark(['MineAdmin', '管理系统', '2024']) }) onUnmounted(() => { // 组件卸载时清除水印 clear() }) return { containerRef, setWatermark, clear } } }) ``` ### 实际应用场景 ```vue ``` ## useThemeColor() 用于主题颜色管理的 Hook,提供了颜色设置、转换和主题初始化功能。 **源码路径:** `/web/src/hooks/useThemeColor.ts`\ **GitHub 链接:** [查看源码](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useThemeColor.ts) ### 返回值 | 方法 | 参数 | 描述 | |------|------|------| | initThemeColor | () | 初始化主题颜色 | | setThemeColor | (color: string) | 设置主题颜色 | | hexToRgb | (str: string) | 将十六进制颜色转换为RGB | | rgbToHex | (a: number, b: number, c: number) | 将RGB颜色转换为十六进制 | | darken | (color: string, level: number) | 使颜色变暗 | | lighten | (color: string, level: number) | 使颜色变亮 | ### 使用示例 ```typescript import useThemeColor from '@/hooks/useThemeColor' export default defineComponent({ setup() { const { initThemeColor, setThemeColor, hexToRgb, rgbToHex, darken, lighten } = useThemeColor() onMounted(() => { // 初始化主题颜色 initThemeColor() }) const changeThemeColor = (color: string) => { setThemeColor(color) } const colorConversion = () => { // 颜色转换示例 const hex = '#409eff' const rgb = hexToRgb(hex) // [64, 158, 255] const hexBack = rgbToHex(rgb[0], rgb[1], rgb[2]) // '#409eff' // 颜色调节 const darkerColor = darken(hex, 0.2) // 变暗20% const lighterColor = lighten(hex, 0.2) // 变亮20% console.log({ hex, rgb, hexBack, darkerColor, lighterColor }) } return { changeThemeColor, colorConversion } } }) ``` ### 实际应用场景 ```vue ``` ## 总结 1. **数据管理**:`useCache()` 用于缓存管理 2. **UI 交互**:`useDialog()`、`useMessage()`、`useImageViewer()` 等用于用户界面交互 3. **业务功能**:`useForm()`、`useTable()` 用于表单和表格操作 4. **主题定制**:`useThemeColor()`、`useWatermark()` 用于主题和视觉效果 5. **资源管理**:`useResourcePicker()` 用于文件资源选择 6. **本地化**:`useLocalTrans()` 用于多语言翻译 7. **用户体验**:`useTabCollection()` 用于标签页收藏管理 这些 Hooks 都遵循 Vue 3 Composition API 的设计模式,提供了良好的类型支持和开发体验。开发者可以根据项目需求选择合适的 Hooks,也可以组合使用多个 Hooks 来实现复杂的业务逻辑。 所有 Hooks 的源码都托管在 [GitHub](https://github.com/mineadmin/mineadmin) 上,开发者可以查看详细的实现代码和参与贡献。 --- --- url: /backend/frameworks/hyperf/3.1.md --- # Hyperf 3.1 实现 Hyperf 3.1 是 MineAdmin 3.x 当前稳定的后端实现之一。它运行在 Swoole/Swow 协程环境中,并通过 Hyperf 的中间件、事件、异常处理、配置加载和组件生态落地 MineAdmin 的公共契约。 ## 实现边界 Hyperf 实现负责说明框架相关的细节: * 应用启动、命令入口和请求生命周期。 * `config/autoload` 配置、服务注册和中间件顺序。 * Hyperf 事件、队列、日志、异常处理和文件系统接入。 * MineAdmin Swagger 注解与 Hyperf Swagger 的集成方式。 * `hyperf/translation` 在业务多语言中的使用方式。 数据模型、后台路由、接口元数据、响应结构和前台模板对接的稳定约定,请先阅读 [公共契约](/v3/backend/contracts/)。 ## 文档目录 * [目录结构](./base/structure.md) * [生命周期](./base/lifecycle.md) * [路由与 API 文档](./base/router.md) * [错误处理](./base/error-handler.md) * [日志](./base/logger.md) * [事件](./base/event-handler.md) * [文件上传](./base/upload.md) * [多语言](./base/lang.md) * [用户认证](./security/passport.md) * [用户授权(RBAC)](./security/access.md) * [获取客户端 IP](./security/client-ip.md) * [数据权限](./data-permission/overview.md) ## 适用场景 如果你的项目使用 MineAdmin 3.x 默认后端,或者需要了解协程环境下的具体实现方式,请阅读本节。Laravel 或未来其他后端实现会复用同一套公共契约,但在生命周期、中间件、ORM 和配置方式上使用各自框架的实现文档说明。 --- --- url: /backend/frameworks/hyperf/3.2.md --- # Hyperf 3.2 实现 Hyperf 3.2 是 MineAdmin 3.x 当前 latest 后端实现。它运行在 Swoole/Swow 协程环境中,并通过 Hyperf 的中间件、事件、异常处理、配置加载和组件生态落地 MineAdmin 的公共契约。 ## 实现边界 Hyperf 实现负责说明框架相关的细节: * 应用启动、命令入口和请求生命周期。 * `config/autoload` 配置、服务注册和中间件顺序。 * Hyperf 事件、队列、日志、异常处理和文件系统接入。 * MineAdmin Swagger 注解与 Hyperf Swagger 的集成方式。 * `hyperf/translation` 在业务多语言中的使用方式。 数据模型、后台路由、接口元数据、响应结构和前台模板对接的稳定约定,请先阅读 [公共契约](/v3/backend/contracts/)。 ## 文档目录 * [目录结构](./base/structure.md) * [生命周期](./base/lifecycle.md) * [路由与 API 文档](./base/router.md) * [错误处理](./base/error-handler.md) * [日志](./base/logger.md) * [事件](./base/event-handler.md) * [文件上传](./base/upload.md) * [多语言](./base/lang.md) * [用户认证](./security/passport.md) * [用户授权(RBAC)](./security/access.md) * [获取客户端 IP](./security/client-ip.md) * [数据权限](./data-permission/overview.md) ## 适用场景 如果你的项目使用 MineAdmin 3.x 默认后端,或者需要了解协程环境下的具体实现方式,请阅读本节。Laravel 或未来其他后端实现会复用同一套公共契约,但在生命周期、中间件、ORM 和配置方式上使用各自框架的实现文档说明。 --- --- url: /backend/frameworks/hyperf.md --- # Hyperf latest Hyperf latest 当前指向 Hyperf `3.2` 实现文档。`3.1` 和 `3.2` 当前章节结构一致,后续会按实际版本差异分别维护。 ## 当前版本 * [Hyperf 3.2](/backend/frameworks/hyperf/3.2/):当前 latest。 * [Hyperf 3.1](/backend/frameworks/hyperf/3.1/):稳定实现。 ## 快速入口 * [目录结构](/backend/frameworks/hyperf/3.2/base/structure) * [生命周期](/backend/frameworks/hyperf/3.2/base/lifecycle) * [路由与 API 文档](/backend/frameworks/hyperf/3.2/base/router) * [错误处理](/backend/frameworks/hyperf/3.2/base/error-handler) * [用户认证](/backend/frameworks/hyperf/3.2/security/passport) * [数据权限](/backend/frameworks/hyperf/3.2/data-permission/overview) --- --- url: /v3/front/high/tsx.md --- # JSX 和 TSX 开发 在 `3.0` 的前端中,路由视图不仅支持 `vue`,也支持 **`jsx、tsx`** 作为视图文件,给开发者提供不同的选择, 当然在 `vue` 文件内也可以写 `tsx` 或 `jsx`,同时还可以保持传统写法。 我们强烈建议把 `vue` 的 `script` 的 `lang` 属性设置为 `tsx` ```vue ``` :::info 会发现,与普通写法没有什么大的差别,但在 `script` 标签里直接写 `
` 之类的标签时候,就会发现特别方便。 ::: 以上仅仅是简单示例,下面分享几个学习的地方: * [vue3.0 的官方插件 babel-plugin-jsx 语法教学](https://github.com/vuejs/babel-plugin-jsx#syntax) * [拥抱 Vue3 系列之 JSX 语法](https://juejin.cn/post/6846687592138670094) --- --- url: /backend/frameworks/laravel/1.0.md --- # Laravel 1.0 实现 Laravel `1.0` 是 MineAdmin 后端多实现方案的预留入口。第一阶段只建立文档结构,不补齐完整实现细节。 ## 目标 Laravel 实现需要复用 [公共契约](/v3/backend/contracts/) 中的数据模型、后台路由、接口元数据、响应结构和前台模板对接约定。这样同一套 MineAdmin 前台模板可以连接 Hyperf 或 Laravel 后端,而不需要维护两套前台逻辑。 ## 预计覆盖范围 后续补齐 Laravel `1.0` 实现时,建议按以下主题组织: * 应用启动与请求生命周期。 * 服务容器、服务提供器和配置加载。 * 中间件注册、认证和权限校验。 * Eloquent 模型、仓储层和事务处理。 * OpenAPI/Swagger 元数据生成。 * 异常处理、日志、事件、队列和文件上传。 * 多语言消息加载与客户端语言识别。 ## 当前状态 当前页面只作为规划入口。正式使用 Laravel `1.0` 实现前,请以 [Hyperf latest](/backend/frameworks/hyperf/) 和公共契约为准。 ::: warning 说明 当前文档中的用户认证、用户授权、获取客户端 IP 和数据权限章节均为 Hyperf 实现内容。Laravel 实现暂未提供这些章节。 ::: --- --- url: /v3/front/component/ma-echarts.md --- # MaEcharts 基于 [echarts](https://echarts.apache.org/zh/index.html) 的封装,提供更简单的使用方式。 :::tip 说明 如果单独使用 `@mineadmin/echarts` 依赖的话,需要将 `echarts` 对象注册绑定到 `app.config.globalProperties.$echarts` 上。但在 `MineAdmin` 中已经自动注册了。 ::: ## 使用 --- --- url: /libs/ma-form/latest.md --- # MaForm `@mineadmin/form` 是基于 Vue 3、Element Plus 和 TSX 封装的配置化表单库。它把 `ElForm`、`ElFormItem` 和常见数据录入组件组合成一个可通过 `items` 数组动态渲染的表单组件,适合后台管理系统中的新增、编辑、筛选、高级配置表单等场景。 本文按 `@mineadmin/form` 当前源码整理。 ## 当前版本 * 文档版本:`latest` * 源码版本:`1.0.57` * 包名:`@mineadmin/form` * 版本策略:独立发版,不跟随 MineAdmin 主产品大版本 * peer dependency:`element-plus` ## 安装 ::: code-group ```bash [pnpm] pnpm add @mineadmin/form element-plus ``` ```bash [npm] npm install @mineadmin/form element-plus ``` ```bash [yarn] yarn add @mineadmin/form element-plus ``` ::: 在应用入口注册插件: ```ts import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import MaForm from '@mineadmin/form' import '@mineadmin/form/dist/index.css' import App from './App.vue' const app = createApp(App) app.use(ElementPlus) app.use(MaForm) app.mount('#app') ``` SSR 场景可以开启延迟客户端渲染: ```ts app.use(MaForm, { ssr: true }) ``` ## 快速开始 `MaForm` 接收一个对象模型和一组表单项配置。每个 `item` 通过 `prop` 绑定模型路径,通过 `render` 指定要渲染的 Element Plus 组件或自定义组件。 ```vue ``` `prop` 支持点路径,内部通过 `lodash-es` 的 `get` 和 `set` 读写模型: ```ts const model = ref({ user: { profile: { nickname: '', }, }, }) const items: MaFormItem[] = [ { label: '昵称', prop: 'user.profile.nickname', render: 'input', }, ] ``` ## Props | 参数 | 说明 | 类型 | 默认值 | |------|------|------|--------| | `v-model` / `modelValue` | 表单数据模型,支持对象与嵌套路径 | `MaModel` | `{}` | | `options` | 表单配置,包含 Element Plus `ElForm` 属性和 MaForm 扩展属性 | `MaFormOptions` | `{}` | | `items` | 表单项配置数组 | `MaFormItem[]` | `[]` | ## MaFormOptions `MaFormOptions` 继承了 Element Plus Form 的常用配置,并额外提供加载态、布局和页脚渲染能力。 | 参数 | 说明 | 类型 | |------|------|------| | `rules` | 表单验证规则,对应 `ElForm` 的 `rules` | `FormRules` | | `inline` | 是否使用行内表单模式 | `boolean` | | `labelPosition` | 标签位置 | `'left' \| 'right' \| 'top'` | | `labelWidth` | 标签宽度 | `string \| number` | | `labelSuffix` | 标签后缀 | `string` | | `hideRequiredAsterisk` | 是否隐藏必填星号 | `boolean` | | `showMessage` | 是否显示校验错误信息 | `boolean` | | `inlineMessage` | 是否以行内形式展示校验信息 | `boolean` | | `statusIcon` | 是否显示校验反馈图标 | `boolean` | | `validateOnRuleChange` | `rules` 改变后是否立即验证 | `boolean` | | `size` | 表单内组件尺寸 | `'' \| 'large' \| 'default' \| 'small'` | | `disabled` | 是否禁用表单内所有组件 | `boolean` | | `scrollToError` | 校验失败时是否滚动到第一个错误项 | `boolean` | | `scrollIntoViewOptions` | 滚动到错误项时的 `scrollIntoView` 配置 | `Record \| boolean` | | `onValidate` | 任一表单项完成校验后的回调 | `(prop, isValid, message) => void` | | `loading` | 是否显示容器加载态 | `boolean` | | `loadingConfig` | 加载态配置 | `LoadingConfig` | | `containerClass` | 表单外层容器 class | `string` | | `layout` | 布局模式,未设置时按 `flex` 处理 | `'flex' \| 'grid'` | | `flex` | `flex` 布局参数,透传给 `el-row` | `object` | | `grid` | `grid` 布局参数,实际透传给 `el-space` | `object` | | `footerSlot` | 使用 TSX/JSX 配置页脚内容 | `() => any` | ### LoadingConfig | 参数 | 说明 | 类型 | |------|------|------| | `text` | 加载文案 | `string` | | `spinner` | 自定义加载图标 class | `string` | | `svg` | 自定义 SVG 加载图标 | `string` | | `viewBox` | 自定义 SVG 的 viewBox | `string` | | `background` | 背景遮罩颜色 | `string` | | `customClass` | 自定义加载层 class | `string` | ## 布局 默认布局是 `flex`,每个表单项会包裹在 `el-col` 中,`item.cols` 透传给 `el-col`。 ```ts const options: MaFormOptions = { layout: 'flex', flex: { gutter: 16, justify: 'start', align: 'top', }, } const items: MaFormItem[] = [ { label: '姓名', prop: 'name', render: 'input', cols: { xs: 24, sm: 12 } }, { label: '手机', prop: 'mobile', render: 'input', cols: { xs: 24, sm: 12 } }, ] ``` `grid` 布局使用 `el-space` 渲染表单项,适合固定间距、自动换行或纵向排列的场景。 ```ts const options: MaFormOptions = { layout: 'grid', grid: { wrap: true, fill: true, fillRatio: 30, style: { width: '100%' }, }, } ``` ## MaFormItem | 参数 | 说明 | 类型 | |------|------|------| | `label` | 表单项标签 | `string \| (() => string)` | | `prop` | 模型字段路径,使用校验、重置等能力时建议必填 | `string \| (() => string)` | | `hide` | 是否隐藏该项,隐藏后保留数据与组件实例 | `boolean \| state` | | `show` | 是否渲染该项,不渲染时不会输出表单项 | `boolean \| state` | | `cols` | `flex` 布局下的 `el-col` 参数 | `object` | | `itemProps` | `ElFormItem` 属性,已移除 `label` 和 `prop` | `FormItem` | | `itemSlots` | `ElFormItem` 插槽配置 | `object` | | `render` | 字符串组件名、自定义渲染函数、组件或 VNode | `renderType` | | `renderProps` | 渲染组件的 props | `Record` | | `renderSlots` | 渲染组件的插槽 | `Record` | | `children` | 子表单项配置 | `MaFormItem[]` | `hide` 和 `show` 都可以接收函数,函数参数是当前表单项和整个表单模型: ```ts const items: MaFormItem[] = [ { label: '账号类型', prop: 'type', render: 'select', }, { label: '企业名称', prop: 'companyName', render: 'input', show: (_item, model) => model.type === 'company', }, { label: '内部备注', prop: 'remark', render: 'input', hide: (_item, model) => model.type !== 'internal', }, ] ``` ### itemProps 与辅助说明 `itemProps` 会透传给 `ElFormItem`,并额外支持 `help` 和 `extra` 文案。 ```ts const items: MaFormItem[] = [ { label: '邮箱', prop: 'email', render: 'input', itemProps: { rules: [ { required: true, message: '请输入邮箱', trigger: 'blur' }, { type: 'email', message: '邮箱格式不正确', trigger: 'blur' }, ], help: '用于接收系统通知', extra: '请使用常用邮箱', }, }, ] ``` `itemSlots` 可以覆盖表单项的 `default`、`label`、`error`、`help` 和 `extra`。 ```tsx const items: MaFormItem[] = [ { prop: 'password', render: 'input', renderProps: { type: 'password', showPassword: true }, itemSlots: { label: () => 登录密码, help: () => 至少 8 位,建议包含数字和字母, }, }, ] ``` ## 渲染组件 当 `render` 是字符串时,MaForm 会把首字母转成大写后到内置组件映射中查找 Element Plus 组件。 | render 值 | Element Plus 组件 | |-----------|-------------------| | `radio` / `radioButton` | `ElRadioGroup` | | `checkbox` / `checkboxButton` | `ElCheckboxGroup` | | `input` | `ElInput` | | `mention` | `ElMention` | | `autocomplete` | `ElAutocomplete` | | `inputNumber` | `ElInputNumber` | | `select` | `ElSelect` | | `selectV2` | `ElSelectV2` | | `treeSelect` | `ElTreeSelect` | | `cascader` | `ElCascader` | | `switch` | `ElSwitch` | | `slider` | `ElSlider` | | `timePicker` | `ElTimePicker` | | `datePicker` | `ElDatePicker` | | `timeSelect` | `ElTimeSelect` | | `rate` | `ElRate` | | `colorPicker` | `ElColorPicker` | | `transfer` | `ElTransfer` | :::tip `ComponentName` 类型中保留了 `Upload`,但当前内置 `componentMap` 未注册上传组件。上传类场景建议使用自定义渲染函数。 ::: ### renderProps `renderProps` 会传给实际渲染的组件,MaForm 会自动补上 `modelValue` 和 `onUpdate:modelValue`。 ```ts const items: MaFormItem[] = [ { label: '标题', prop: 'title', render: 'input', renderProps: { maxlength: 60, showWordLimit: true, placeholder: '请输入标题', }, }, ] ``` ### renderSlots 组件插槽通过 `renderSlots` 配置。适合给 `select`、`treeSelect` 等组件补充子节点。 ```tsx import { ElOption } from 'element-plus' const items: MaFormItem[] = [ { label: '状态', prop: 'status', render: 'select', renderProps: { clearable: true, placeholder: '请选择状态', }, renderSlots: { default: () => [ , , ], }, }, ] ``` ### 自定义渲染 `render` 也可以传函数,函数会收到当前 `item` 和完整 `formData`。 ```tsx import { ElInput, ElTag } from 'element-plus' const items: MaFormItem[] = [ { label: '邀请码', prop: 'inviteCode', render: ({ formData }) => ( {{ append: () => formData.inviteCode ? 已填写 : '待填写', }} ), }, ] ``` ### 接入 Table 组件 `@mineadmin/form` 当前源码没有在内置 `componentMap` 中注册 `table`,因此不能通过 `render: 'table'` 直接渲染表格。需要在应用里额外安装并注册表格组件,然后通过自定义渲染把 `MaTable`、`ElTable` 或业务表格组件放进表单项。 如果使用 `@mineadmin/table` 的 `MaTable`,需要先在入口完成注册;如果使用 Element Plus 的 `ElTable`,已随 `ElementPlus` 注册: ```ts import MaForm from '@mineadmin/form' import MaTable from '@mineadmin/table' app.use(MaForm) app.use(MaTable) ``` 表格组件通常不使用 `v-model`,需要显式把表单模型里的数组传给表格。单元格编辑、增删行等操作直接修改这个数组即可,最终提交时仍然从 `ma-form` 的 `v-model` 中读取完整数据。对于表格这类不需要 MaForm 自动注入 `modelValue` 的组件,推荐使用 `itemSlots.default` 填充表单项内容。 ```tsx import { computed, ref } from 'vue' import { ElButton, ElInput, ElInputNumber, ElTable, ElTableColumn } from 'element-plus' import type { MaFormItem, MaModel } from '@mineadmin/form' interface ProductRow { name: string quantity: number price: number } const model = ref({ products: [ { name: 'MineAdmin 专业版', quantity: 1, price: 2999 }, ], }) const rows = computed(() => model.value.products ?? []) const addRow = () => { rows.value.push({ name: '', quantity: 1, price: 0 }) } const items: MaFormItem[] = [ { label: '商品明细', prop: 'products', cols: { span: 24 }, itemProps: { help: '表格数据会保存在 model.products 中,可随表单一起校验和提交。', }, itemSlots: { default: () => { return (
{{ default: ({ row }: { row: ProductRow }) => ( row.name = String(value ?? '')} /> ), }} {{ default: ({ row }: { row: ProductRow }) => ( row.quantity = value ?? 1} /> ), }} {{ default: ({ row }: { row: ProductRow }) => ( row.price = value ?? 0} /> ), }} 新增一行
) }, }, }, ] ``` 如果你希望完全通过 `render` 维护表格,也可以在 `render` 函数中返回一个表格容器组件,并由该组件内部管理 `MaTable` 的 `data`、`columns` 和行编辑逻辑。 :::tip `MaForm` 会把 `modelValue` 和 `onUpdate:modelValue` 注入给最终渲染组件,但表格类组件更常见的是 `data`、`columns`、`options` 这类属性。接入这类组件时,以表单模型中的数组作为数据源会更直观,也能避免误以为 `render: 'table'` 是内置能力。 ::: ## 嵌套表单项 `children` 会在当前渲染组件的默认插槽内继续渲染一组子表单项,适合把一组字段包进卡片、折叠面板或自定义容器中。 ```tsx const items: MaFormItem[] = [ { prop: 'profile', render: () =>
, children: [ { label: '姓名', prop: 'profile.name', render: 'input' }, { label: '职位', prop: 'profile.title', render: 'input' }, ], }, ] ``` ## 页脚 可以使用组件插槽写页脚,也可以用 `options.footerSlot` 在配置中声明。 ```tsx const options: MaFormOptions = { footerSlot: () => (
取消 提交
), } ``` 模板插槽写法: ```vue ``` ## 暴露方法 通过组件 `ref` 可以访问 MaForm 暴露的方法。常用方式是使用 `useForm(refName)`,也可以直接使用 Vue 的模板 ref。 | 方法 | 说明 | |------|------| | `setLoadingState(loading)` | 设置 `options.loading` | | `setOptions(options)` | 合并更新表单配置 | | `getOptions()` | 获取当前表单配置 | | `setItems(items)` | 替换表单项数组 | | `getItems()` | 获取当前表单项数组 | | `appendItem(item)` | 追加一个表单项 | | `getItemByProp(prop)` | 按 `prop` 查找表单项,运行时找不到返回 `null` | | `getElFormRef()` | 获取底层 Element Plus `ElForm` 实例 | 当前源码还在运行时暴露了 `removeItem(prop)` 和 `isMobileState()`;如果在 TypeScript 中使用并遇到类型提示缺失,可以按项目需要扩展本地类型。 ## useForm `useForm(refName)` 会在组件挂载后按字符串 ref 查找 MaForm 实例,并返回一个 Promise。 ```vue ``` :::warning `useForm` 内部依赖 `getCurrentInstance()` 和 `onMounted()`,需要在组件 `setup` 阶段调用。 ::: ## 类型导出 `@mineadmin/form` 导出了常用类型,便于在业务项目中约束配置。 ```ts import type { ComponentName, FormItem, LoadingConfig, MaFormExpose, MaFormInstallOptions, MaFormItem, MaFormOptions, MaModel, renderType, state, } from '@mineadmin/form' ``` ## 与 Element Plus 的关系 MaForm 不重新实现表单校验和表单项能力,而是把配置透传到 Element Plus: * 表单级配置通过 `options` 传给 `ElForm` * 表单项级配置通过 `item.itemProps` 传给 `ElFormItem` * 组件级配置通过 `item.renderProps` 传给最终渲染组件 * 校验、重置、清除校验等操作通过 `getElFormRef()` 调用 Element Plus Form 实例方法 ```ts useForm('userForm').then(async (form) => { const elForm = form.getElFormRef() await elForm.validate() elForm.resetFields() }) ``` --- --- url: /libs/ma-pro-table/latest.md --- # MaProTable `@mineadmin/pro-table` 是基于 `@mineadmin/search` 和 `@mineadmin/table` 封装的高级表格组件,适合后台列表、分页查询、搜索筛选、跨页多选、操作列、工具栏扩展和右键菜单等数据管理场景。 本文基于 `@mineadmin/pro-table` `1.0.89` 源码整理。 ## 安装 ```bash pnpm add @mineadmin/pro-table @mineadmin/search @mineadmin/table @mineadmin/form element-plus @imengyu/vue3-context-menu ``` `ma-pro-table` 内部会渲染 `ma-search` 和 `ma-table`,而 `ma-search` 又依赖 `ma-form`,所以应用入口需要一起注册这些组件。 如果项目不需要行右键菜单,可以省略 `@imengyu/vue3-context-menu` 以及示例中的 `provider.contextMenu`。 ```ts import { createApp, markRaw } from 'vue' import ElementPlus from 'element-plus' import MaForm from '@mineadmin/form' import MaTable from '@mineadmin/table' import MaSearch from '@mineadmin/search' import MaProTable from '@mineadmin/pro-table' import ContextMenu from '@imengyu/vue3-context-menu' import 'element-plus/dist/index.css' import '@mineadmin/search/dist/style.css' import '@mineadmin/table/dist/style.css' import '@mineadmin/pro-table/dist/style.css' import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css' const app = createApp(App) app .use(ElementPlus) .use(MaForm) .use(MaTable) .use(MaSearch) .use(MaProTable, { ssr: false, provider: { app, contextMenu: markRaw(ContextMenu.showContextMenu), }, }) .mount('#app') ``` 如果在 SSR 场景使用,可以把 `ssr` 设为 `true`,组件会等到客户端挂载后再渲染主体内容。 ## 快速开始 下面示例展示了一个包含搜索、分页、行拖拽、操作列、工具栏插槽和右键菜单的完整 `ma-pro-table`。 ```vue ``` ## 示例演示 ### 基础用法 最简单的表格使用方式,包含搜索、分页和基本操作功能。 ### 高级搜索 展示多种搜索组件类型和复杂搜索逻辑,包含日期范围、数字范围、多选等场景。 ### 自定义操作 展示操作列、批量动作、条件显示和拖拽排序等交互配置。 ### 单元格渲染插件 演示内置 `tag` 插件以及自定义渲染插件的注册与使用。 ### 工具栏扩展 通过 `useProTableToolbar()` 和插槽扩展默认工具栏。 ### 数据管理 展示新增、编辑、删除、选中导出和统计联动等完整 CRUD 流程。 ### 响应式布局 演示不同宽度下的表格和搜索区域布局适配。 ## 组件参数 | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | `options` | 表格、搜索、请求、工具栏等运行配置 | `MaProTableOptions` | `{ tableOptions: {}, searchOptions: {}, searchFormOptions: {} }` | | `schema` | 搜索项与表格列配置 | `MaProTableSchema` | `{ searchItems: [], tableColumns: [] }` | ### MaProTableSchema | 字段 | 说明 | 类型 | | --- | --- | --- | | `searchItems` | 搜索表单项,透传给 `ma-search` 的 `search-items` | `MaSearchItem[]` | | `tableColumns` | 表格列配置,最终会转换为 `ma-table` 列 | `MaProTableColumns[]` | ### MaProTableColumns `MaProTableColumns` 继承 `ma-table` 的列配置,并额外扩展了以下能力: | 字段 | 说明 | | --- | --- | | `type` | 支持 `ma-table` 原有列类型,并扩展 `operation` 操作列、`sort` 行拖拽列 | | `toolHide` | 在默认列设置工具里隐藏该列 | | `cellRenderTo` | 使用已注册的单元格渲染插件 | | `cellRenderPro` | 自定义单元格渲染,参数会额外拿到 `MaProTableExpose` | | `headerRenderPro` | 自定义表头渲染,参数会额外拿到 `MaProTableExpose` | | `operationConfigure` | `type: 'operation'` 时的操作列配置 | | `children` | 多级表头 | ## 请求与分页 配置 `requestOptions.api` 后,组件会在挂载时自动请求数据,并在分页、搜索、重置时更新请求参数。 ```ts const options: MaProTableOptions = { requestOptions: { api: params => http.get('/system/user', { params }), autoRequest: true, requestParams: { status: 1, }, requestPage: { pageName: 'page', sizeName: 'page_size', size: 20, }, response: { dataKey: 'list', totalKey: 'total', }, responseDataHandler: response => response.list ?? [], }, } ``` 默认响应结构为: ```ts { data: { list: [], total: 0, }, } ``` | 配置 | 说明 | 默认值 | | --- | --- | --- | | `api` | 请求方法,组件会把合并后的分页、搜索和 `requestParams` 作为第一个参数传入 | - | | `autoRequest` | 是否挂载后自动请求 | `true` | | `response.dataKey` | 列表数据字段 | `list` | | `response.totalKey` | 总数字段 | `total` | | `requestPage.pageName` | 页码参数名 | `page` | | `requestPage.sizeName` | 每页条数参数名 | `page_size` | | `requestPage.size` | 默认每页条数 | `10` | | `requestParams` | 初始请求参数 | `{}` | | `responseDataHandler` | 响应数据后处理函数,需要返回最终表格数组 | - | 如果没有配置 `requestOptions.api`,组件会使用 `tableOptions.data` 作为静态数据。 ## 搜索 `schema.searchItems` 会交给 `ma-search` 渲染。`searchOptions` 会透传给 `ma-search`,`searchFormOptions` 会透传给 `ma-search` 内部的 `ma-form`。 ```ts const options: MaProTableOptions = { searchOptions: { show: true, fold: true, }, searchFormOptions: { labelWidth: '90px', }, onSearchSubmit: form => { return { ...form, searched_at: Date.now(), } }, onSearchReset: form => { return form }, } ``` `onSearchSubmit` 和 `onSearchReset` 的返回值会继续作为请求参数合并。如果只做副作用,也需要 `return form`。 ## 操作列 把列类型设为 `operation` 后,可以通过 `operationConfigure.actions` 配置行操作。 ```ts const schema: MaProTableSchema = { tableColumns: [ { type: 'operation', operationConfigure: { type: 'auto', fold: 1, actions: [ { name: 'edit', text: '编辑', order: 10, linkProps: { type: 'primary' }, show: ({ row }) => row.status !== 'locked', disabled: ({ row }) => row.status === 'disabled', onClick: ({ row }, proxy) => { console.log('编辑行', row) proxy.refresh() }, }, { name: 'delete', text: ({ row }) => `删除 ${row.username}`, order: 20, linkProps: { type: 'danger' }, onClick: ({ row }) => { console.log('删除行', row) }, }, ], }, }, ], } ``` | 配置 | 说明 | 默认值 | | --- | --- | --- | | `operationConfigure.type` | 展示方式:`auto` 自动折叠、`dropdown` 全部下拉、`tile` 全部平铺 | `auto` | | `operationConfigure.fold` | `auto` 模式下平铺显示几个操作,其余进入下拉 | `1` | | `actions[].name` | 操作标识 | - | | `actions[].text` | 文本或返回文本的函数 | `unknown` | | `actions[].icon` | 图标名或返回图标名的函数,需要注册 `provider.icon` | - | | `actions[].order` | 排序值,越小越靠前 | - | | `actions[].show` | 是否显示 | - | | `actions[].disabled` | 是否禁用 | - | | `actions[].onClick` | 点击回调,参数为表格渲染数据、`proxy` 和鼠标事件 | - | | `actions[].linkProps` | 透传给 `el-link` 的属性 | - | ## 行拖拽排序 添加 `type: 'sort'` 列后,组件会渲染拖拽手柄,并在拖拽结束后触发 `row-drag-sort`。 ```vue ``` 也可以通过 `options.on.rowDragSort` 接收同一个结果。 ## 跨页多选 跨页多选会把不同分页上的选择项合并到同一个数组里。实际使用时建议显式配置 `rowKey`,并提供 `onSelectionChange` 回调接收合并后的结果。 ```ts const selectedRows = ref([]) const options: MaProTableOptions = { selection: { crossPage: true, selectedText: '已选择 {number} 项', clearText: '清空', }, tableOptions: { rowKey: 'id', on: { onSelectionChange: rows => { selectedRows.value = rows }, }, }, } ``` ## 右键菜单 右键菜单需要在安装组件时提供 `provider.contextMenu`,然后在页面里开启 `rowContextMenu.enabled`。 ```ts const options: MaProTableOptions = { rowContextMenu: { enabled: true, items: [ { label: '刷新', icon: 'i-ri-refresh-line', onMenuClick: ({ row, proxy }, event) => { console.log(row, event) proxy.refresh() }, }, ], }, } ``` `ContextMenuItem` 支持 `label`、`icon`、`disabled`、`divided` 和 `onMenuClick`。 ## 单元格渲染插件 `cellRenderTo` 适合把常见单元格显示逻辑封装成可复用插件。组件内置了一个 `tag` 插件,会用 `el-tag` 渲染单元格内容。 ```ts const schema: MaProTableSchema = { tableColumns: [ { label: '状态', prop: 'status_text', cellRenderTo: { name: 'tag', props: { type: 'success', }, }, }, ], } ``` 当 `props.prop` 未传入时,组件会自动使用当前列的 `prop`。 ### 注册插件 ```ts import { h } from 'vue' import { ElTag } from 'element-plus' import { useProTableRenderPlugin, type MaProTableRenderPlugin, } from '@mineadmin/pro-table' const { addPlugin } = useProTableRenderPlugin() const statusPlugin: MaProTableRenderPlugin = { name: 'status-tag', render: (data, props, proxy) => { return h( ElTag, { type: data.row.status === 1 ? 'success' : 'info', ...props, }, { default: () => data.row[props?.prop ?? 'status_text'], }, ) }, } addPlugin(statusPlugin) ``` `useProTableRenderPlugin()` 提供: 当前 `1.0.89` 源码在执行 `cellRenderTo` 时传给插件的参数顺序是 `(data, props, proxy)`,其中 `props` 来自列配置的 `cellRenderTo.props`,`proxy` 是当前 `ma-pro-table` 实例。 | 方法 | 说明 | | --- | --- | | `addPlugin(plugin)` | 注册插件,同名插件不会重复注册 | | `removePlugin(name)` | 移除插件 | | `getPlugins()` | 获取所有插件 | | `getPluginByName(name)` | 按名称获取插件 | ## 工具栏 默认工具栏包含: | 名称 | 功能 | | --- | --- | | `mineProTableRefresh` | 刷新数据 | | `mineProTableSearch` | 显示或隐藏搜索区域 | | `mineProTablePrint` | 打印当前表格 | | `mineProTableSetting` | 列显示与固定列设置 | 可以通过 `toolStates` 在当前页面控制默认工具显示状态。 ```ts const options: MaProTableOptions = { toolbar: true, toolStates: { mineProTablePrint: false, }, } ``` ### 扩展工具栏 ```ts import ExportButton from './ExportButton.vue' import { useProTableToolbar } from '@mineadmin/pro-table' const { add } = useProTableToolbar() add({ name: 'export', order: 20, show: true, render: () => ExportButton, }) ``` 自定义工具组件会收到 `proxy` 参数。 默认安装参数中的 `provider.toolbars` 会被组件初始化为内置工具栏;需要扩展工具时,建议在组件上下文中使用 `useProTableToolbar()` 添加。 ```vue ``` `useProTableToolbar()` 提供: | 方法 | 说明 | | --- | --- | | `add(toolbar)` | 添加工具,同名工具不会重复注册 | | `remove(name)` | 移除工具 | | `get(name)` | 获取指定工具 | | `getAll()` | 获取全部工具 | | `hide(name)` | 全局隐藏工具 | | `show(name)` | 全局显示工具 | ## Header 与布局 ```ts const options: MaProTableOptions = { header: { show: true, mainTitle: '用户管理', subTitle: '维护账号、角色和状态', }, actionBtnPosition: 'auto', toolbar: true, adaptionOffsetBottom: 0, tableOptions: { adaption: true, }, } ``` | 配置 | 说明 | 默认值 | | --- | --- | --- | | `header.show` | 是否显示头部 | `false` | | `header.mainTitle` | 主标题 | `表格主标题` | | `header.subTitle` | 副标题 | 空字符串 | | `actionBtnPosition` | `actions` 插槽显示位置:`auto`、`header`、`table` | `auto` | | `toolbar` | 是否显示工具栏 | `true` | | `adaptionOffsetBottom` | 自适应高度额外底部偏移 | `0` | ## 插槽 | 插槽 | 说明 | | --- | --- | | `search` | 搜索表单默认内容,透传给 `ma-search` | | `searchActions` | 搜索按钮区域,透传给 `ma-search` | | `searchBeforeActions` | 搜索操作前置区域 | | `searchAfterActions` | 搜索操作后置区域 | | `actions` | 页面动作按钮区域,位置由 `actionBtnPosition` 决定 | | `headerTitle` | 替换默认头部标题 | | `tableHeader` | 替换整个头部 | | `headerRight` | 头部右侧扩展 | | `toolbarLeft` | 工具栏左侧扩展 | | `beforeToolbar` | 默认工具栏前置内容 | | `toolbar` | 替换默认工具栏 | | `afterToolbar` | 默认工具栏后置内容 | | `middle` | 搜索区与表格卡片之间的内容 | | `tableTop` | 表格卡片顶部内容 | | `tableCranny` | 工具栏和表格之间的内容 | | `default` | 默认插槽会继续传递给内部 `ma-table` | 除以上插槽外,传入 `ma-pro-table` 的其他插槽也会继续透传给内部 `ma-table`。 ## 事件 | 事件 | 说明 | | --- | --- | | `search-submit` | 搜索提交后触发,参数为最终搜索表单 | | `search-reset` | 搜索重置后触发,参数为最终搜索表单 | | `row-drag-sort` | 行拖拽排序结束后触发,参数为排序后的表格数据 | `ma-table` 原生事件建议通过 `options.tableOptions.on` 配置。 ## 暴露方法 通过模板引用可以调用组件暴露的方法。 ```vue ``` | 方法 | 说明 | | --- | --- | | `getSearchRef()` | 获取内部 `ma-search` 实例 | | `getTableRef()` | 获取内部 `ma-table` 实例 | | `getElTableStates()` | 获取内部 Element Plus Table store states | | `refresh()` | 按当前参数重新请求数据 | | `requestData()` | 请求数据;当 `autoRequest` 为 `false` 时首次调用会初始化分页并开启请求 | | `changeApi(api, isRequestNow)` | 更换请求方法,`isRequestNow` 默认 `true` | | `setRequestParams(params, isRequestNow)` | 合并请求参数,`isRequestNow` 默认 `false` | | `setTableColumns(cols)` | 重设表格列 | | `getTableColumns()` | 获取当前表格列 | | `setSearchForm(form)` | 设置搜索表单 | | `getSearchForm()` | 获取搜索表单 | | `search(params?)` | 合并当前搜索表单和额外参数后立即查询 | | `setProTableOptions(opts)` | 动态合并组件配置 | | `getProTableOptions()` | 获取当前组件配置 | | `resizeHeight()` | 重新计算自适应高度 | | `getCurrentId()` | 获取当前组件内部生成的 ID | ## 常见注意点 * `requestPage.sizeName` 当前源码默认值是 `page_size`,如果后端使用 `pageSize`,需要显式配置。 * `onSearchSubmit` 和 `onSearchReset` 会把返回值作为最终搜索参数;不要只写副作用后不返回表单。 * 开启跨页多选时,建议同时配置 `tableOptions.rowKey` 和 `tableOptions.on.onSelectionChange`。 * 类型里保留了 `options.id`,但当前组件会生成内部随机 ID;运行时请通过 `getCurrentId()` 获取。 * `cellRenderTo` 适合复用渲染逻辑;一次性的单元格展示可以直接使用 `cellRenderPro` 或 `ma-table` 原有 `cellRender`。 --- --- url: /libs/ma-search/latest.md --- # MaSearch `@mineadmin/search` 是基于 `@mineadmin/form` 和 Element Plus 封装的列表搜索面板组件。它把搜索字段、搜索/重置按钮、折叠展开、响应式列布局和运行时方法组织成一个独立库,适合后台列表页、筛选面板和 `ma-pro-table` 搜索区域。 ## 当前版本 * 文档版本:`latest` * 包名:`@mineadmin/search` * 源码版本:`1.0.59` * 版本策略:独立发版,不跟随 MineAdmin 主产品大版本 * peer dependency:`element-plus` ## 安装 ```bash pnpm add @mineadmin/search @mineadmin/form element-plus ``` `ma-search` 内部渲染 `ma-form`,因此应用入口需要同时注册 Element Plus、MaForm 和 MaSearch。 ```ts import { createApp } from 'vue' import ElementPlus from 'element-plus' import MaForm from '@mineadmin/form' import MaSearch from '@mineadmin/search' import 'element-plus/dist/index.css' import '@mineadmin/form/dist/index.css' import '@mineadmin/search/dist/index.css' import App from './App.vue' const app = createApp(App) app .use(ElementPlus) .use(MaForm) .use(MaSearch) .mount('#app') ``` SSR 场景可以开启客户端挂载后渲染: ```ts app.use(MaSearch, { ssr: true }) ``` ## 基础用法 `ma-search` 接收三组核心配置: * `options`:搜索面板自身配置,例如默认值、列数、折叠、按钮文案。 * `formOptions`:透传给内部 `ma-form` 的表单配置,例如 `labelWidth`。 * `searchItems`:搜索项配置,类型继承自 `MaFormItem`,额外扩展 `span`、`offset` 和 `hide`。 ```vue ``` 搜索项的 `prop` 支持点路径,字段值会写入对应的嵌套对象: ```ts const searchItems: MaSearchItem[] = [ { label: '用户名', prop: 'user.name', render: 'input' }, ] ``` 输入组件监听回车键,按下 `Enter` 会触发一次 `search` 事件。 ## 示例演示 ### 基础搜索 常规输入框、选择器、日期范围等搜索项组合。 ### 高级搜索 展示 TSX 自定义渲染、多选、级联等复杂筛选场景。 ### 折叠搜索 搜索项较多时,可以通过 `fold` 和 `foldRows` 控制初始展示数量。 ### 自定义操作区 使用 `actions`、`beforeActions` 和 `afterActions` 插槽扩展按钮区域。 ### 动态搜索项 通过暴露方法在运行时添加、删除或替换搜索项。 ### 响应式布局 使用 `cols` 配置不同断点下的搜索项列数。 ### 表格集成 把 `search` 和 `reset` 事件转换为列表查询参数。 ### 表单验证 配合内部 `ma-form` 的规则校验能力控制搜索提交。 ### 方法演示 演示 `setSearchForm`、`setOptions`、`appendItem`、按钮属性设置等实例方法。 ## Props | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | `options` | 搜索面板配置 | `MaSearchOptions` | `{}` | | `formOptions` | 内部 `ma-form` 配置 | `MaFormOptions` | `{}` | | `searchItems` | 搜索项配置数组 | `MaSearchItem[]` | `[]` | 组件上的其他属性会继续透传给内部 `ma-form`。 ## MaSearchOptions | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | `defaultValue` | 搜索表单默认值 | `Record` | `{}` | | `cols` | 响应式列数配置 | `Record<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl', number>` | 见下方断点 | | `fold` | 初始折叠配置。传 `true` 时挂载后会先收起超过 `foldRows` 的搜索项 | `boolean` | `false` | | `foldButtonShow` | 是否显示折叠/展开按钮 | `boolean` | `true` | | `foldRows` | 收起状态下保留的搜索项行数 | `number` | `2` | | `show` | 是否显示整个搜索面板,也可以传函数动态判断 | `boolean \| (() => boolean)` | `true` | | `text` | 搜索、重置、展开、折叠按钮文案 | `TextConfig` | - | | `searchBtnProps` | 透传给搜索按钮的 Element Plus Button 属性 | `Record` | - | | `resetBtnProps` | 透传给重置按钮的 Element Plus Button 属性 | `Record` | - | ### 响应式列 `cols` 根据窗口宽度计算当前网格列数。未配置某个断点时使用内置默认值。 | 断点 | 宽度范围 | 默认列数 | | --- | --- | --- | | `xs` | `< 768px` | `1` | | `sm` | `>= 768px` 且 `< 992px` | `2` | | `md` | `>= 992px` 且 `< 1200px` | `2` | | `lg` | `>= 1200px` 且 `< 1920px` | `3` | | `xl` | `>= 1920px` | `4` | ```ts const options: MaSearchOptions = { cols: { xs: 1, sm: 2, md: 3, lg: 4, xl: 6, }, } ``` ### 按钮文案 按钮文案需要使用函数返回字符串。 ```ts const options: MaSearchOptions = { text: { searchBtn: () => '查询', resetBtn: () => '清空', isFoldBtn: () => '收起', notFoldBtn: () => '更多', }, } ``` ### 折叠行为 组件挂载时会执行一次 `foldToggle()` 来初始化折叠状态: * `options.fold` 省略或为 `false`:初始展开全部搜索项。 * `options.fold` 为 `true`:初始只保留 `foldRows` 行内的搜索项,其余项收起。 * `getFold()` 返回的是当前运行状态,`true` 表示展开,`false` 表示收起。 ```ts const options: MaSearchOptions = { fold: true, foldRows: 1, } ``` ## MaSearchItem `MaSearchItem` 继承 `@mineadmin/form` 的 `MaFormItem`,所以 `label`、`prop`、`render`、`renderProps`、`renderSlots`、`itemProps`、`cols` 等能力与 MaForm 保持一致。 MaSearch 额外处理以下字段: | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | `hide` | 是否隐藏当前搜索项。收起/展开计算时会尊重该配置 | `boolean \| (() => boolean)` | `false` | | `span` | 当前项在搜索网格中跨几列 | `number` | `1` | | `offset` | 当前项左侧偏移几列 | `number` | `0` | ```ts const searchItems: MaSearchItem[] = [ { label: '关键词', prop: 'keyword', render: 'input', span: 2, renderProps: { clearable: true, placeholder: '请输入名称、编号或手机号', }, }, { label: '内部字段', prop: 'internal', render: 'input', hide: () => true, }, ] ``` ::: tip 组件内部会自动追加一个 `__MaSearchAction` 操作项,用来渲染搜索、重置和折叠按钮。搜索、重置以及 `getSearchForm()` 返回数据前都会移除这个内部字段。 ::: ## 事件 | 事件 | 说明 | 参数 | | --- | --- | --- | | `search` | 点击搜索按钮或在输入项中按下回车后触发 | `form: Record` | | `reset` | 点击重置按钮后触发 | `form: Record` | | `fold` | 折叠状态变化后触发 | `state: boolean` | ## 插槽 | 插槽 | 说明 | | --- | --- | | `default` | 透传给内部 `ma-form` 的默认插槽,适合直接编写自定义表单项 | | `actions` | 完全替换默认操作区域 | | `beforeActions` | 插入到搜索、重置按钮之前 | | `afterActions` | 插入到搜索、重置按钮之后 | ```vue ``` 如果需要完全接管按钮区域,可以使用 `actions`: ```vue ``` ## 暴露方法 | 方法 | 说明 | | --- | --- | | `getMaFormRef()` | 获取内部 `ma-form` 实例 | | `foldToggle()` | 切换展开/收起状态 | | `getFold()` | 获取当前运行状态,`true` 为展开 | | `setSearchForm(form)` | 合并设置搜索表单;传 `null` 时清空表单对象 | | `getSearchForm()` | 获取当前搜索表单数据 | | `setShowState(state)` | 设置搜索面板显示状态 | | `getShowState()` | 获取搜索面板显示状态 | | `setOptions(options)` | 合并更新搜索面板配置,并重新初始化搜索项 | | `getOptions()` | 获取搜索面板配置 | | `setFormOptions(options)` | 合并更新内部 `ma-form` 配置 | | `getFormOptions()` | 获取内部 `ma-form` 配置 | | `setItems(items)` | 替换搜索项数组 | | `getItems()` | 获取当前搜索项数组 | | `appendItem(item)` | 追加一个搜索项 | | `removeItem(prop)` | 按 `prop` 移除搜索项 | | `getItemByProp(prop)` | 按 `prop` 获取搜索项,未找到时返回 `null` | | `setSearchBtnProps(props)` | 合并设置搜索按钮属性 | | `setResetBtnProps(props)` | 合并设置重置按钮属性 | ```vue ``` ## 类型定义 ```ts export type MediaBreakPoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' export interface MaSearchOptions { defaultValue?: Record cols?: Record fold?: boolean foldButtonShow?: boolean foldRows?: number show?: boolean | (() => boolean) text?: { searchBtn?: () => string resetBtn?: () => string isFoldBtn?: () => string notFoldBtn?: () => string } searchBtnProps?: Record resetBtnProps?: Record } export interface MaSearchItem extends MaFormItem { hide?: boolean | (() => boolean) span?: number offset?: number } export interface MaSearchExpose { getMaFormRef: () => typeof MaForm foldToggle: () => void getFold: () => boolean setSearchForm: (form: null | Record) => void getSearchForm: () => Record setShowState: (state: boolean) => void getShowState: () => boolean setOptions: (opts: MaSearchOptions) => void getOptions: () => MaSearchOptions setFormOptions: (opts: MaFormOptions) => void getFormOptions: () => MaFormOptions setItems: (items: MaSearchItem[]) => void getItems: () => MaSearchItem[] appendItem: (item: MaSearchItem) => void removeItem: (prop: string) => void getItemByProp: (prop: string) => MaSearchItem | null setSearchBtnProps: (props: Record) => void setResetBtnProps: (props: Record) => void } ``` ## 使用建议 * 搜索项较多时,建议设置 `fold: true` 和 `foldRows`,让页面初始只展示高频字段。 * 列表页查询参数建议统一从 `search` 事件或 `getSearchForm()` 获取,不要直接读取内部 `ma-form` 模型。 * 复杂字段渲染优先复用 `ma-form` 的 `render`、`renderProps`、`renderSlots` 能力,MaSearch 只负责搜索面板布局和动作区域。 --- --- url: /libs/ma-table/latest.md --- # MaTable `@mineadmin/table` 是基于 Element Plus `el-table` 封装的基础表格组件库。它保留 Element Plus Table / TableColumn 的原生属性、事件和插槽,同时补充配置式列、分页、加载状态、自适应高度、列显隐、自定义渲染和运行时操作方法。 ## 安装 ```bash pnpm add @mineadmin/table element-plus ``` ## 全局注册 ```ts import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import MaTable from '@mineadmin/table' import App from './App.vue' const app = createApp(App) app.use(ElementPlus) app.use(MaTable, { ssr: false, options: {}, }) app.mount('#app') ``` `app.use(MaTable, options)` 支持: | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | `ssr` | SSR 场景下延迟到客户端挂载后再渲染表格。 | `boolean` | `false` | | `options` | 注入到组件中的默认表格配置。当前实现会在组件传入同名 `options` 键时参与合并,业务表格仍建议显式维护自己的 `options`。 | `MaTableOptions` | `{}` | ## 基础用法 `ma-table` 接收两个核心配置:`columns` 描述列,`options` 描述表格行为。`data`、`border`、`stripe` 等 Element Plus Table 属性也可以直接写在组件上,它们会透传给内部 `el-table`。 ```vue ``` ## Props | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | `columns` | 表格列配置,兼容 Element Plus `el-table-column` 属性,并扩展显隐、多级表头和渲染器能力。 | `MaTableColumns[]` | `[]` | | `options` | 表格配置,兼容 Element Plus `el-table` 属性,并扩展加载、分页、自适应高度等能力。 | `MaTableOptions` | `{}` | ::: tip 属性透传 除了 `columns` 和 `options`,传给 `ma-table` 的其他属性会继续传给内部 `el-table`。如果同一属性同时出现在 `options` 和组件属性上,组件属性会覆盖 `options`。 ::: ## MaTableOptions `MaTableOptions` 包含 Element Plus Table 的原生属性,例如 `data`、`height`、`stripe`、`border`、`rowKey`、`showSummary`、`summaryMethod`、`lazy`、`treeProps`、`spanMethod` 等。下面只列出 MaTable 扩展项。 | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | `containerHeight` | 类型中保留的容器高度配置;实际高度通常使用 Element Plus 的 `height`、`maxHeight` 或 MaTable 的 `adaption` 控制。 | `string` | - | | `loading` | 是否显示 Element Plus Loading 遮罩。 | `boolean` | `false` | | `loadingConfig` | Loading 指令配置。 | `LoadingConfig` | - | | `columnAlign` | 表格列默认对齐方式。 | `'left' \| 'center' \| 'right'` | `center` | | `headerAlign` | 表头默认对齐方式;列 `align` 与表格 `columnAlign` 的优先级更高。 | `'left' \| 'center' \| 'right'` | - | | `showOverflowTooltip` | 内容溢出时是否显示 tooltip。 | `boolean` | `true` | | `pagination` | Element Plus Pagination 配置与事件。 | `PaginationProps` | - | | `showPagination` | 是否显示内置分页器。需要同时配置 `pagination`。 | `boolean` | `false` | | `adaption` | 是否根据窗口高度自动设置表格高度。 | `boolean` | `false` | | `adaptionOffsetBottom` | 自适应高度时保留的底部偏移量。 | `number` | `70` | | `on` | Element Plus Table 事件集合,会展开到内部 `el-table`。 | `Record` | - | ### LoadingConfig | 参数 | 说明 | 类型 | | --- | --- | --- | | `text` | 加载文案。 | `string` | | `spinner` | 自定义加载图标类名。 | `string` | | `svg` | 自定义 SVG 加载图标。 | `string` | | `viewBox` | SVG 图标 `viewBox`。 | `string` | | `background` | 类型中声明的遮罩背景色。当前渲染逻辑未单独读取它,需要定制遮罩时建议配合 `customClass`。 | `string` | | `customClass` | 自定义 Loading class。 | `string` | ### PaginationProps 分页配置兼容 Element Plus Pagination。组件内部维护当前页,默认从第 `1` 页开始;需要主动跳页时可以调用 `setCurrentPage`,页码变化通常通过 `onChange`、`onCurrentChange` 等回调同步业务请求。 | 参数 | 说明 | 类型 | | --- | --- | --- | | `total` | 总条目数。 | `number` | | `pageSize` | 每页条数。 | `number` | | `currentPage` | 当前页码。 | `number` | | `pageSizes` | 每页条数选项。 | `number[]` | | `layout` | 分页布局,例如 `total, sizes, prev, pager, next, jumper`。 | `string` | | `background` | 是否为按钮添加背景色。 | `boolean` | | `onSizeChange` | 每页条数变化回调。 | `(value: number) => void` | | `onCurrentChange` | 当前页变化回调。 | `(value: number) => void` | | `onChange` | 页码或每页条数变化回调。 | `(currentPage: number, pageSize: number) => void` | ```ts const options: MaTableOptions = { showPagination: true, pagination: { currentPage: 1, pageSize: 10, total: 100, pageSizes: [10, 20, 50, 100], layout: 'total, sizes, prev, pager, next, jumper', onChange: (currentPage, pageSize) => { fetchList({ currentPage, pageSize }) }, }, } ``` ## MaTableColumns `MaTableColumns` 兼容 Element Plus TableColumn 的原生属性,例如 `label`、`prop`、`type`、`width`、`fixed`、`sortable`、`filters`、`filterMethod`、`formatter`、`align` 等。下面是 MaTable 扩展项。 | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | `hide` | 是否隐藏当前列。可以传布尔值,也可以传函数动态判断;函数运行时接收组件透传的 `attrs`。 | `boolean \| CallableFunction` | `false` | | `children` | 多级表头配置,内部渲染为嵌套 `el-table-column`。 | `MaTableColumns[]` | - | | `cellRender` | 自定义单元格渲染器,适合 TSX / JSX。 | `(data: TableColumnRenderer) => VNode \| string` | - | | `headerRender` | 自定义表头渲染器,适合 TSX / JSX。 | `(data: TableColumnRenderer) => VNode \| string` | - | ```tsx const columns: MaTableColumns[] = [ { type: 'selection', prop: 'selection', width: 56 }, { label: '用户信息', prop: 'profile', children: [ { label: '姓名', prop: 'name', align: 'left' }, { label: '手机号', prop: 'phone' }, ], }, { label: '状态', prop: 'status', headerRender: ({ column }) => {column.label}, cellRender: ({ row }) => ( {row.status === 1 ? '启用' : '停用'} ), }, ] ``` ### 渲染器参数 `cellRender` 和 `headerRender` 接收 `TableColumnRenderer`: | 参数 | 说明 | | --- | --- | | `row` | 当前行数据。表头渲染时可能不存在。 | | `column` | 当前 Element Plus 列上下文。 | | `$index` | 当前行或列索引。 | | `options` | 当前 MaTable 表格配置。 | | `attrs` | 传给 `ma-table` 的透传属性。 | ## 插槽 | 名称 | 说明 | 参数 | | --- | --- | --- | | `empty` | 空数据内容,覆盖默认 `ElEmpty`。 | - | | `append` | 表格尾部追加内容。 | - | | `pageLeft` | 分页区域左侧内容,常用于批量操作按钮。 | - | | `column-[prop]` | 指定列的单元格插槽。 | `{ row, column, $index }` | | `header-[prop]` | 指定列的表头插槽。 | `{ column, $index }` | | `default` | 默认单元格插槽,作为所有列的兜底渲染。 | `{ row, column, $index }` | | `header` | 默认表头插槽,作为所有列的兜底渲染。 | `{ column, $index }` | | `filterIcon` | 自定义筛选图标插槽。 | Element Plus 原生参数 | ```vue ``` ## 事件 MaTable 自身新增了一个事件: | 名称 | 说明 | 参数 | | --- | --- | --- | | `set-data-callback` | 调用 `setData` 后触发。 | `data: any[]` | Element Plus Table 事件通过 `options.on` 传入。事件名需要使用 Vue 组件属性格式,例如 `onSelectionChange`、`onRowClick`、`onSortChange`。 ```ts const options: MaTableOptions = { on: { onSelectionChange: (rows) => { selectedRows.value = rows }, onRowClick: (row) => { currentRow.value = row }, }, } ``` ## 暴露方法 通过模板 `ref` 可以访问 MaTable 暴露的方法。 | 方法 | 说明 | 参数 | 返回值 | | --- | --- | --- | --- | | `setData(data)` | 设置表格数据,并触发 `set-data-callback`。 | `any[]` | `void` | | `setPagination(pagination)` | 合并更新分页配置。 | `PaginationProps` | `void` | | `setCurrentPage(pager)` | 设置当前页码。 | `number` | `void` | | `getCurrentPage()` | 获取当前页码。 | - | `number` | | `setLoadingState(loading)` | 设置加载状态。 | `boolean` | `void` | | `setOptions(options)` | 合并更新表格配置。 | `MaTableOptions` | `void` | | `getOptions()` | 获取当前表格配置。 | - | `MaTableOptions` | | `setColumns(columns)` | 重设所有列。 | `MaTableColumns[]` | `void` | | `getColumns()` | 获取当前列配置。 | - | `MaTableColumns[]` | | `appendColumn(column)` | 追加一列。 | `MaTableColumns` | `void` | | `removeColumn(prop)` | 根据 `prop` 删除列。 | `string` | `void` | | `getColumnByProp(prop)` | 根据 `prop` 获取列配置;运行时未找到会返回 `null`。 | `string` | `MaTableColumns \| null` | | `getElTableRef()` | 获取内部 Element Plus Table 实例。 | - | `Ref` | ```vue ``` ## useTable `useTable(refName)` 需要在 `setup` 阶段调用。它会在组件挂载后按模板 ref 名称查找 MaTable 实例,并返回 `MaTableExpose`。如果找不到对应 ref,会抛出 `[@mineadmin/table]: not found ref for ma-table component`。 ```vue ``` ## 常见场景 ### 分页表格 设置 `showPagination: true` 并传入 `pagination` 后,MaTable 会在表格下方渲染 Element Plus Pagination。分页区域左侧可以通过 `pageLeft` 插槽放批量操作。 ### 排序 排序能力继承自 Element Plus TableColumn。列上配置 `sortable`、`sortMethod`、`sortBy`、`sortOrders`;远程排序使用 `sortable: 'custom'` 并在 `options.on` 中监听 `onSortChange`。 ### 筛选 筛选能力继承自 Element Plus TableColumn。列上配置 `filters`、`filterMethod`、`filterMultiple`、`filteredValue`,筛选变化可以通过 `onFilterChange` 处理。 ### 自定义渲染 简单内容可以使用 `column-[prop]` 插槽;需要更强类型或 TSX 表达能力时,使用 `cellRender` 和 `headerRender`。 ### 动态列 运行时可以通过 `setColumns`、`appendColumn`、`removeColumn` 维护列配置,也可以在列配置里用 `hide` 控制显隐。 ### 树形表格 配置 `rowKey`、`treeProps`、`defaultExpandAll` 即可展示层级数据;懒加载时继续使用 Element Plus 的 `lazy` 和 `load`。 ### 多选表格 列上配置 `type: 'selection'` 开启多选。需要跨页或数据刷新后保留选择时,配合 `rowKey` 和 `reserveSelection` 使用。 ### 响应式和自适应高度 开启 `adaption` 后,组件会根据窗口高度设置表格 `height`,并用 `adaptionOffsetBottom` 保留底部空间。加载状态可以通过 `loading` 或 `setLoadingState` 控制。 ## 使用建议 MaTable 的核心是把 Element Plus Table / TableColumn 配置集中到 `options` 和 `columns`: * 静态表格优先使用 `columns`、`options` 和组件属性完成声明式配置。 * 需要根据接口结果调整列或分页时,通过模板 `ref` 调用 `setData`、`setPagination`、`setColumns`。 * 需要访问原生 Element Plus Table 方法时,通过 `getElTableRef()` 获取内部表格实例。 * 复杂单元格优先使用 `cellRender` 或 `column-[prop]` 插槽,表头则使用 `headerRender` 或 `header-[prop]` 插槽。 ## 类型导出 `@mineadmin/table` 导出下列类型,业务项目可以直接复用: ```ts export type { MaTableInstallOptions, MaTableSetting, MaTableOptions, MaTableExpose, MaTableColumns, PaginationProps, LoadingConfig, TableColumnFilterPlacement, TableColumnSortOrders, TableColumnSortable, TableColumnRenderer, TableColumnScope, TableColumnFixed, TableColumnType, TableColumn, } from '@mineadmin/table' ``` ## 相关链接 * [Element Plus Table](https://element-plus.org/zh-CN/component/table.html) * [Element Plus Pagination](https://element-plus.org/zh-CN/component/pagination.html) * [@mineadmin/table 源码仓库](https://github.com/mineadmin/mineadmin-table) --- --- url: /v3/plugin/mineJson.md --- # mine.json 说明及示例 一个应用配置文件的完整示例以及说明 *** ## 属性列表说明 | 参数 | 说明 | 示例 | |----------------------|---------------------------------------|-----------------------| | name | 由 **用户名称空间/应用标识符** 组成 | mine-admin/apps-store | | description | 应用介绍 | MineAdmin应用市场可视化插件 | | version | 应用当前版本号 | 1.0.0 | | type | 应用类型:mixed(完整应用)、backend(后端)、frontend(前端) | mixed | | 1.0.0 | | author | 应用作者信息 | - | | package.dependencies | 应用前端依赖配置信息,可指定前端需要安装的依赖包及版本,在应用安装时会安装 | - | | composer | 后端composer配置,可查看下面详细表格 | - | ## composer配置说明 | 参数 | 说明 | 示例 | |----------|-----------------------|------------------------------------------------| | require | 设置后端依赖及版本,在应用安装时会执行安装 | "hyperf/async\_queue": "3.1.\*" **指定异步队列依赖和版本** | | psr-4 | 设置插件代码目录的命名空间 | "Plugin\MineAdmin\AppStore\\": "src" | | script | 执行的脚本命令 | 例如下面例文件,执行了发布异步队列的配置文件 | | config | hyperf配置文件服务提供器 | - | ## mine.json 文件内容 以下是一个示例 ```json [mine.json] { "name": "mine-admin/apps-store", "description": "MineAdmin应用市场可视化插件", "version": "1.0.0", "type": "mixed", "author": [ { "name": "zds", "role": "developer" } ], "package": { "dependencies": { } }, "composer": { "require": { }, "psr-4": { "Plugin\\MineAdmin\\AppStore\\": "src" }, "script": { "publishAsyncQueue": "php bin/hyperf.php vendor:publish hyperf/async-queue" }, "config": "Plugin\\MineAdmin\\AppStore\\ConfigProvider" } } ``` --- --- url: /v3/front/high/i18n.md --- # MineAdmin 国际化配置完整指南 MineAdmin 前端基于 Vue i18n v11 构建了完整的国际化解决方案,支持多语言切换、动态语言包加载和模块化翻译管理。本文档将详细介绍系统的国际化实现机制和使用方法。 ## 概述 ### 支持的语言 * 简体中文 (zh\_CN) - 默认语言 * 繁体中文 (zh\_TW) * 英文 (en) ### 核心特性 * 🌐 多语言动态切换 * 📦 模块化语言包管理 * ⚡ 动态语言包加载 * 🔧 开发工具集成 * 📱 全局和局部作用域支持 * 🎯 TypeScript 类型安全 ### 技术栈 * **Vue i18n**: 11.1.2 * **构建插件**: @intlify/unplugin-vue-i18n 6.0.3 * **语言格式**: YAML (支持 JSON) * **类型系统**: TypeScript ## 项目配置 ### 系统初始化 系统在 `/web/src/bootstrap.ts` 中初始化国际化配置: ```typescript // 源码位置: web/src/bootstrap.ts:44-67 // GitHub: https://github.com/mineadmin/mineadmin/blob/master/web/src/bootstrap.ts async function createI18nService(app: App) { // 自动扫描语言包文件 const locales: any[] = Object.entries(import.meta.glob('./locales/*.y(a)?ml')).map(([key]: any) => { const [, value, label] = key.match(/^.\/locales\/(\w+)\[([^[\]]+)\]\.yaml$/) return { label, value } }) useUserStore().setLocales(locales) // 处理消息格式 Object.keys(messages as any).map((name: string) => { const matchValue = name.match(/(\w+)/) as RegExpMatchArray | null if (messages && matchValue) { messages[matchValue[1]] = messages[name] delete messages[name] } }) // 创建 i18n 实例 app.use(createI18n({ legacy: false, // 使用 Composition API globalInjection: true, // 全局注入 fallbackLocale: 'zh_CN', // 回退语言 locale: useUserStore().getLanguage(), // 当前语言 silentTranslationWarn: true, // 静默翻译警告 silentFallbackWarn: true, // 静默回退警告 messages, // 语言包数据 })) } ``` ## 系统架构 ### 架构图 ```mermaid graph TB A[Vue App] --> B[Bootstrap] B --> C[I18n Service] C --> D[createI18n] D --> E[Global Messages] D --> F[Local Messages] E --> G[Core Locales] E --> H[Module Locales] E --> I[Plugin Locales] F --> J[Component i18n blocks] K[useTrans Hook] --> L[Global Translation] K --> M[Local Translation] N[useLocalTrans Hook] --> M G --> O[/src/locales/*.yaml] H --> P[/src/modules/*/locales/*.yaml] I --> Q[/src/plugins/*/locales/*.yaml] style A fill:#e1f5fe style C fill:#f3e5f5 style K fill:#fff3e0 style N fill:#fff3e0 ``` ### 文件结构 ``` web/src/ ├── locales/ # 核心语言包 │ ├── zh_CN[简体中文].yaml │ ├── en[English].yaml │ └── zh_TW[繁體中文].yaml ├── modules/ # 模块语言包 │ └── base/ │ └── locales/ │ ├── zh_CN[简体中文].yaml │ ├── en[English].yaml │ └── zh_TW[繁體中文].yaml ├── plugins/ # 插件语言包 │ └── mine-admin/ │ └── code-generator/ │ └── web/ │ └── locales/ │ ├── zh_CN[简体中文].yaml │ ├── en[English].yaml │ └── zh_TW[繁體中文].yaml └── hooks/ # 国际化 Hooks ├── auto-imports/ │ └── useTrans.ts # 自动导入的翻译 Hook └── useLocalTrans.ts # 局部翻译 Hook ``` ## 语言包管理 ### 文件命名规范 语言包文件必须遵循特定格式:`语言标识符[本语言名称].yaml` ```bash # 正确的命名格式 zh_CN[简体中文].yaml en[English].yaml zh_TW[繁體中文].yaml # 错误的命名格式 zh_CN.yaml # 缺少语言名称 en[英文].yaml # 语言名称错误 zh-CN[简体中文].yaml # 语言标识符格式错误 ``` ### 全局语言包 系统自动扫描以下三个位置的语言包: 1. **核心语言包**: `src/locales/` 2. **模块语言包**: `src/modules/<模块名>/locales/` 3. **插件语言包**: `src/plugins/<插件名>/locales/` ::: danger 重要提醒 **模块** 和 **插件** 下的语言包文件名必须与 `src/locales` 下的文件名完全一致,否则会出现找不到翻译键的控制台警告。 ::: ### 语言包内容示例 **核心语言包示例** (`src/locales/zh_CN[简体中文].yaml`): ```yaml # 登录表单 loginForm: loginButton: 登录 passwordPlaceholder: 请输入密码 codeLabel: 验证码 usernameLabel: 账户 # CRUD 操作 crud: cancel: 取消 save: 保存 ok: 确定 add: 新增 edit: 编辑 delete: 删除 createSuccess: 创建成功 updateSuccess: 修改成功 delMessage: 是否删除所选中数据? # 表单验证 form: pleaseInput: 请输入{msg} pleaseSelect: 请选择{msg} requiredInput: '{msg}必填' ``` ### 局部语言包 在 Vue 组件中定义局部语言包: ```vue zh_CN: welcome: 欢迎使用 description: 这是一个示例页面 zh_TW: welcome: 歡迎使用 description: 這是一個示例頁面 en: welcome: Welcome description: This is a sample page ``` ## Hook 系统详解 ### useTrans Hook **源码位置**: `web/src/hooks/auto-imports/useTrans.ts`\ **GitHub**: https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/auto-imports/useTrans.ts ```typescript import { useI18n } from 'vue-i18n' import type { ComposerTranslation } from 'vue-i18n' export interface TransType { globalTrans: ComposerTranslation localTrans: ComposerTranslation } export function useTrans(key: any | null = null): TransType | string | any { const global = useI18n() const local = useI18n({ inheritLocale: true, useScope: 'local', }) if (key === null) { return { localTrans: local.t, globalTrans: global.t, } } else { // 优先查找全局翻译,然后查找局部翻译 return global.te(key) ? global.t(key) : local.te(key) ? local.t(key) : key } } ``` ### useLocalTrans Hook **源码位置**: `web/src/hooks/useLocalTrans.ts`\ **GitHub**: https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useLocalTrans.ts ```typescript import { useI18n } from 'vue-i18n' import type { ComposerTranslation } from 'vue-i18n' export function useLocalTrans(key: any | null = null): string | ComposerTranslation | any { const { t } = useI18n({ inheritLocale: true, useScope: 'local', }) return key === null ? t as ComposerTranslation : t(key) as string } ``` ## 使用方法 ### 基础用法 #### 1. 全局和局部翻译对象 ```vue ``` #### 2. 直接调用翻译 ```typescript // 智能翻译:优先全局,回退到局部 const message = useTrans('crud.updateSuccess') // "修改成功" // 仅使用局部翻译 const localMessage = useLocalTrans('description') ``` ### 实际项目案例 **源码位置**: `web/src/modules/base/views/permission/user/index.vue`\ **GitHub**: https://github.com/mineadmin/mineadmin/blob/master/web/src/modules/base/views/permission/user/index.vue ```vue ``` ### 带参数的翻译 ```vue ``` ### 模板中直接使用 ```vue ``` ## 高级功能 ### 动态语言切换 ```typescript // 切换到英文 const { locale } = useI18n() locale.value = 'en' // 或使用用户存储 const userStore = useUserStore() userStore.setLanguage('en') ``` ### 检查翻译键是否存在 ```typescript const { te, t } = useI18n() // 检查键是否存在 if (te('some.key')) { const translation = t('some.key') } else { console.warn('Translation key not found: some.key') } ``` ### 复数处理 ```yaml # 语言包定义 en: cart: items: 'no items | one item | {count} items' zh_CN: cart: items: '{count} 个商品' ``` ```typescript // 使用 const itemCount = ref(5) const message = t('cart.items', itemCount.value) // "5 个商品" ``` ## 性能优化 ### 懒加载语言包 ```typescript // 按需加载语言包 const loadLanguage = async (locale: string) => { try { const messages = await import(`../locales/${locale}[${getLocaleName(locale)}].yaml`) const { global } = useI18n() global.setLocaleMessage(locale, messages.default) global.locale.value = locale } catch (error) { console.error(`Failed to load language pack: ${locale}`, error) } } ``` ### 缓存优化 ```typescript // 缓存翻译结果 const translationCache = new Map() const cachedT = (key: string, ...args: any[]) => { const cacheKey = `${key}-${JSON.stringify(args)}` if (translationCache.has(cacheKey)) { return translationCache.get(cacheKey) } const result = t(key, ...args) translationCache.set(cacheKey, result) return result } ``` ## 调试和故障排除 ### 常见问题 #### 1. 找不到翻译键 **问题**: 控制台出现警告 `Not found 'xxx' key in 'zh_CN' locale messages.` **解决方案**: ```typescript // 检查键是否存在 const { te, t } = useI18n() const safeT = (key: string, fallback: string = key) => { return te(key) ? t(key) : fallback } ``` #### 2. 模块语言包不生效 **问题**: 模块或插件的语言包没有被加载 **解决方案**: 1. 确保文件名格式正确:`zh_CN[简体中文].yaml` 2. 检查文件路径是否在扫描范围内 3. 验证 YAML 语法是否正确 #### 3. 类型错误 **问题**: TypeScript 类型检查失败 **解决方案**: ```typescript // 使用正确的类型定义 import type { TransType } from '@/hooks/auto-imports/useTrans.ts' const i18n = useTrans() as TransType const t = i18n.globalTrans // 类型安全的翻译函数 ``` ### 开发工具 #### IDE 插件推荐 * **VSCode**: [i18n Ally](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally) * **WebStorm**: [Easy i18n](https://plugins.jetbrains.com/plugin/16316-easy-i18n) #### 调试技巧 ```typescript // 启用详细日志 const i18n = createI18n({ // ... 其他配置 silentTranslationWarn: false, // 显示翻译警告 silentFallbackWarn: false, // 显示回退警告 missingWarn: true, // 显示缺失警告 fallbackWarn: true, // 显示回退警告 }) ``` ## 测试策略 ### 单元测试 ```typescript // tests/i18n.spec.ts import { mount } from '@vue/test-utils' import { createI18n } from 'vue-i18n' describe('i18n Component', () => { const i18n = createI18n({ legacy: false, locale: 'zh_CN', messages: { zh_CN: { hello: '你好' }, en: { hello: 'Hello' } } }) it('renders correct translation', () => { const wrapper = mount(TestComponent, { global: { plugins: [i18n] } }) expect(wrapper.text()).toContain('你好') }) it('switches language correctly', async () => { i18n.global.locale.value = 'en' await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('Hello') }) }) ``` ### E2E 测试 ```typescript // tests/e2e/i18n.spec.ts test('language switching works', async ({ page }) => { await page.goto('/login') // 检查默认语言 await expect(page.locator('.login-title')).toHaveText('登录') // 切换语言 await page.click('.language-switcher') await page.click('[data-lang="en"]') // 检查语言切换效果 await expect(page.locator('.login-title')).toHaveText('Login') }) ``` ## 扩展开发 ### 添加新语言 1. **创建语言包文件**: ```bash # 在 src/locales/ 下创建新语言文件 touch src/locales/ja[日本語].yaml ``` 2. **添加翻译内容**: ```yaml # ja[日本語].yaml loginForm: loginButton: ログイン passwordPlaceholder: パスワードを入力してください crud: save: 保存 cancel: キャンセル ``` 3. **更新语言配置**: ```typescript // 如果需要,在初始化时添加新语言支持 const supportedLocales = ['zh_CN', 'en', 'zh_TW', 'ja'] ``` ### 自定义翻译逻辑 ```typescript // 创建自定义翻译 Hook export function useCustomTrans() { const { t, te } = useI18n() return { t: (key: string, fallback?: string) => { if (te(key)) { return t(key) } // 自定义回退逻辑 if (fallback) { return fallback } // 记录缺失的翻译键 console.warn(`Missing translation: ${key}`) return key } } } ``` ## 最佳实践 ### 1. 键名命名规范 ```yaml # 推荐:使用命名空间和语义化命名 user: profile: title: 个人资料 edit: 编辑资料 management: title: 用户管理 create: 创建用户 # 避免:过于扁平的结构 userProfileTitle: 个人资料 userProfileEdit: 编辑资料 ``` ### 2. 组件设计 ```vue zh_CN: title: 用户信息 description: 用户ID:{id} en: title: User Info description: User ID: {id} ``` ### 3. 性能考虑 ```typescript // 推荐:缓存计算结果 const translatedOptions = computed(() => { return options.map(option => ({ ...option, label: t(`options.${option.key}`) })) }) // 避免:在渲染函数中重复调用翻译 // {{ options.map(opt => t(`options.${opt.key}`)) }} ``` --- --- url: /v3/guide/start/fast-install.md --- # MineAdmin 快速安装指南 ## 概述 MineAdmin 是一个基于 Hyperf 框架的企业级后台管理系统,采用前后端分离架构。本指南将指导您完成 MineAdmin 的快速安装和配置,帮助您在最短时间内搭建一个功能完整的管理系统。 ### 系统架构 * **后端**:基于 Hyperf 的 PHP 框架 * **前端**:基于 Vue.js 的现代化单页应用 * **数据库**:支持 MySQL、PostgreSQL 等 * **缓存**:支持 Redis * **容器化**:支持 Docker 和 Docker Compose ## 系统需求 ### 软件环境 #### 本地开发环境 * **PHP**:≥ 8.1 * **Composer**:≥ 2.0 * **Node.js**:≥ 16.0(推荐使用 LTS 版本) * **pnpm**:≥ 7.0 * **MySQL**:≥ 5.7 或 **PostgreSQL**:≥ 10 * **Redis**:≥ 5.0 * **Git**:用于版本控制 #### Docker 环境(推荐) * **Docker**:≥ 20.0 * **Docker Compose**:≥ 2.0 ::: tip 环境选择建议 * **新手用户**:推荐使用 Docker Compose,环境配置更加简单 * **开发者**:可根据需要选择本地环境或 Docker 环境 * **生产环境**:推荐使用 Docker 进行部署 ::: ## 安装方式选择 根据您的使用场景选择合适的安装方式: | 使用场景 | 推荐方式 | 优势 | 适用用户 | |---------|---------|------|---------| | 快速体验/学习 | Docker Compose | 一键部署,环境隔离 | 初学者 | | 开发调试 | 本地环境 | 灵活性高,便于调试 | 开发者 | | 生产部署 | Docker Build | 可定制,易扩展 | 运维人员 | ## 快速开始 ### 第一步:下载源码 #### 使用 Git 克隆(推荐) 确保已安装 [Git](https://git-scm.com/) 工具,然后执行以下命令: ```bash # 克隆主分支(标准版本) git clone https://github.com/mineadmin/MineAdmin.git # 或克隆到指定目录 git clone https://github.com/mineadmin/MineAdmin.git your-project-name ``` #### 分支选择指南 MineAdmin 提供两个主要分支,请根据您的需求选择: | 分支名称 | 特性描述 | 适用场景 | |---------|---------|---------| | `master` | 标准版本,包含核心功能 | 大多数应用场景 | | `master-department` | 增强版本,包含部门管理、岗位管理、数据权限等高级功能 | 需要复杂权限管理的企业应用 | ```bash # 切换到增强版本分支 git checkout master-department ``` ::: warning 重要提醒 请在项目开始前确定所需分支,避免后期迁移带来的不必要麻烦。两个分支的数据库结构和功能存在差异。 ::: #### 下载完成后的基础配置 ```bash # 进入项目目录 cd MineAdmin # 或 your-project-name # 复制环境配置文件 cp .env.example .env ``` ### 第二步:环境配置 打开 `.env` 文件,配置以下关键参数: ```ini # 应用配置 APP_NAME=MineAdmin APP_ENV=local APP_DEBUG=true # 数据库配置 DB_DRIVER=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=mineadmin DB_USERNAME=root DB_PASSWORD=your_password DB_CHARSET=utf8mb4 DB_COLLATION=utf8mb4_unicode_ci # Redis 配置 REDIS_HOST=127.0.0.1 REDIS_AUTH= REDIS_PORT=6379 REDIS_DB=0 # JWT 配置(需要手动生成) JWT_SECRET=your_jwt_secret_key ``` ::: tip 配置建议 * 生产环境请务必设置 `APP_DEBUG=false` * 建议使用强密码并定期更换数据库密码 * JWT\_SECRET 建议使用随机生成的复杂字符串 ::: ## 安装方式详解 ### 方式一:Docker Compose 安装(推荐新手) 这是最简单的安装方式,适合快速体验和开发环境。 #### 优势 * 环境隔离,不污染宿主机 * 一键启动所有服务 * 版本统一,避免环境差异 #### 安装步骤 1. **启动服务** ```bash # 后台启动所有服务 docker-compose up -d # 查看服务状态 docker-compose ps ``` 2. **等待服务就绪** 初次启动需要下载镜像,请耐心等待。您可以通过以下命令查看日志: ```bash # 查看所有服务日志 docker-compose logs -f # 查看特定服务日志 docker-compose logs -f mineadmin ``` 3. **进入容器执行初始化** ```bash # 进入应用容器 docker-compose exec mineadmin bash # 安装依赖(按场景二选一) # 开发环境: composer install -vvv # 生产环境安装: composer install --no-dev --optimize-autoloader # 数据库迁移 php bin/hyperf.php migrate # 数据填充 php bin/hyperf.php db:seed ``` ### 方式二:Docker 自构建 适合需要自定义镜像的高级用户。 ```bash # 构建镜像 docker build -t mineadmin:latest . # 启动容器 docker run -d \ --name mineadmin \ -p 9501:9501 \ -v $(pwd):/opt/www \ -e DB_HOST=your_db_host \ -e DB_DATABASE=mineadmin \ -e DB_USERNAME=your_username \ -e DB_PASSWORD=your_password \ -e REDIS_HOST=your_redis_host \ mineadmin:latest ``` ### 方式三:本地环境安装 适合需要深度开发和调试的开发者。 #### 前置条件检查 在开始安装前,请确认环境是否满足要求: ```bash # 检查 PHP 版本 php --version # 检查 Composer 版本 composer --version # 检查扩展 php -m | grep -E "(swoole|redis|pdo_mysql)" # 检查 Node.js 版本 node --version # 检查 pnpm 版本 pnpm --version ``` #### 后端安装 1. **安装 PHP 依赖** ```bash # 安装依赖(按场景二选一) # 开发环境: composer install -vvv # 生产环境安装: composer install --no-dev --optimize-autoloader ``` 2. **数据库初始化** ```bash # 创建数据库(可选,也可以手动创建) mysql -u root -p -e "CREATE DATABASE mineadmin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 执行数据库迁移 php bin/hyperf.php migrate # 填充初始数据 php bin/hyperf.php db:seed ``` 3. **启动后端服务** ```bash # 启动 Hyperf 服务 php bin/hyperf.php start ``` #### 前端安装 1. **环境准备** 推荐使用 [nvm](https://github.com/nvm-sh/nvm) 管理 Node.js 版本: ```bash # 安装并使用推荐的 Node.js 版本 nvm install 18 nvm use 18 # 全局安装 pnpm(如果还没安装) npm install -g pnpm ``` 2. **安装前端依赖** ```bash # 进入前端目录 cd web # 安装依赖 pnpm install # 启动开发服务器 pnpm dev ``` ## 验证安装 ### 检查服务状态 1. **后端服务验证** ```bash # 检查 Hyperf 服务是否正常启动 curl http://localhost:9501/health # 或使用浏览器访问 # http://localhost:9501 ``` 2. **前端服务验证** ```bash # 前端默认运行在 3000 端口 curl http://localhost:3000 # 或使用浏览器访问 # http://localhost:3000 ``` 3. **数据库连接验证** ```bash # 检查数据库连接 php bin/hyperf.php db:show ``` ### 登录系统 安装完成后,使用以下默认账户登录: * **管理员账户**:admin * **默认密码**:123456 ::: warning 安全提醒 首次登录后请立即修改默认密码,确保系统安全。 ::: ## 常见问题解决 ### 安装过程中的常见错误 #### 1. Composer 依赖安装失败 **错误现象**: ``` Your requirements could not be resolved to an installable set of packages. ``` **解决方案**: ```bash # 清除 Composer 缓存 composer clear-cache # 更新 Composer 到最新版本 composer self-update # 重新安装 composer install --ignore-platform-reqs ``` #### 2. 数据库连接失败 **错误现象**: ``` SQLSTATE[HY000] [2002] Connection refused ``` **解决方案**: 1. 检查数据库服务是否启动 2. 验证 `.env` 文件中的数据库配置 3. 确认数据库用户权限 ```bash # 测试数据库连接 mysql -h 127.0.0.1 -P 3306 -u root -p ``` #### 3. Redis 连接失败 **错误现象**: ``` Connection refused [tcp://127.0.0.1:6379] ``` **解决方案**: ```bash # 检查 Redis 服务状态 redis-cli ping # 启动 Redis 服务(根据系统不同) # Ubuntu/Debian sudo systemctl start redis-server # CentOS/RHEL sudo systemctl start redis # macOS brew services start redis ``` #### 4. 前端依赖安装缓慢 **解决方案**: ```bash # 使用淘宝镜像源 pnpm config set registry https://registry.npmmirror.com # 或使用 cnpm npm install -g cnpm --registry=https://registry.npmmirror.com cnpm install ``` #### 5. 端口占用问题 **检查端口占用**: ```bash # 检查 9501 端口(后端) lsof -i :9501 netstat -tulpn | grep :9501 # 检查 3000 端口(前端) lsof -i :3000 netstat -tulpn | grep :3000 ``` **解决方案**: * 停止占用端口的进程 * 或修改配置文件使用其他端口 ### 性能优化建议 #### 开发环境优化 ```bash # 开启 OPcache(PHP 配置) echo "opcache.enable=1" >> /etc/php/8.1/cli/conf.d/99-opcache.ini # 增加 PHP 内存限制 echo "memory_limit=512M" >> /etc/php/8.1/cli/conf.d/99-memory.ini ``` #### 生产环境优化 ```bash # 使用生产环境配置 composer install --no-dev --optimize-autoloader # 清除配置缓存 php bin/hyperf.php config:clear # 构建前端生产版本 cd web && pnpm build ``` --- --- url: /v3/plugin.md --- # MineAdmin 插件系统 MineAdmin 插件系统提供了强大的扩展能力,允许开发者创建可复用的功能模块,实现系统的模块化和可扩展性。 ## 插件系统架构 MineAdmin 的插件系统基于 Hyperf 框架的 ConfigProvider 机制,提供了完整的插件生命周期管理和自动化部署能力。 ```plantuml @startuml !define RECTANGLE class RECTANGLE "MineAdmin Core" as core { + bin/hyperf.php + Plugin::init() } RECTANGLE "Plugin Management" as mgmt { + App-Store Component + Extension Commands + Plugin Loader } RECTANGLE "Plugin Structure" as structure { + mine.json (配置文件) + src/ (后端代码) + web/ (前端代码) + Database/ (数据库) } RECTANGLE "Official Plugins" as official { + app-store } core --> mgmt : 插件初始化 mgmt --> structure : 加载插件 mgmt --> official : 管理官方插件 structure --> core : 注册服务 @enduml ``` ## 核心组件 ### 1. 插件加载器 * **文件**: `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) * **原理**: 通过 `Plugin::init()` 方法在应用启动时自动加载所有已安装的插件 * **实现**: 扫描 `plugin/` 目录下的所有插件并注册其 ConfigProvider ### 2. App-Store 组件 * **仓库**: [mineadmin/appstore](https://github.com/mineadmin/appstore) * **功能**: 提供插件的下载、安装、卸载、更新等管理功能 * **配置**: 通过 `ConfigProvider` 注册服务和配置 ### 3. 插件配置系统 * **核心文件**: `mine.json` * **原理**: 定义插件的元数据、依赖关系、安装脚本等信息 * **加载**: 在插件安装时解析并注册到系统中 ## 官方插件 MineAdmin 默认提供以下官方插件: | 插件名称 | 功能描述 | 仓库地址 | |---------|----------|----------| | app-store | 应用市场管理插件,提供插件的下载、安装、卸载、更新等管理功能 | [GitHub](https://github.com/mineadmin/appstore) | > 注:其他插件如代码生成器、定时任务管理等可通过应用市场获取或自行开发 ## 插件类型 MineAdmin 支持三种类型的插件: ### Mixed (混合型插件) 包含前端和后端完整功能的插件,提供完整的业务模块。 ### Backend (后端插件) 仅包含后端逻辑的插件,主要提供 API 服务和业务逻辑。 ### Frontend (前端插件) 仅包含前端界面的插件,主要提供用户界面组件。 ## 快速开始 ### 环境准备 开发 MineAdmin 插件需要: 1. **熟悉技术栈**:MineAdmin 和 Hyperf 框架 2. **获取 AccessToken**: * 登录 [MineAdmin 官网](https://www.mineadmin.com/login) * 进入个人中心 → [设置页面](https://www.mineadmin.com/member/setting) * 获取 AccessToken 3. **配置环境变量**: ```ini # .env 文件 MINE_ACCESS_TOKEN=你的AccessToken ``` ::: warning 注意 请妥善保管 AccessToken,避免泄露! ::: ### 开发者认证 * **本地开发**:无需认证,可自由开发和分发 * **应用市场发布**:需要开发者认证,联系 MineAdmin 团队开通权限 ## 相关文档 * [快速入门指南](./guide.md) - 创建第一个插件 * [开发指南](./develop.md) - 详细开发流程 * [插件结构](./structure.md) - 目录结构规范 * [生命周期管理](./lifecycle.md) - 安装卸载流程 * [API 参考](./api.md) - 接口文档 * [示例代码](./examples.md) - 实际案例 --- --- url: /v3/front/advanced/permission.md --- # MineAdmin 权限控制系统 ## 概述 MineAdmin 提供了一套完整的前端权限控制系统,实现了细粒度的权限管理。权限控制分为两个层面: :::tip 权限架构概览 * **路由级权限**:基于后端返回的菜单数据控制页面访问权限 * **内容级权限**:通过助手函数、指令和组件控制页面内容的显示和隐藏 权限系统与后端 Hyperf 框架深度集成,确保前后端权限控制的一致性。 ::: ### 权限类型 MineAdmin 支持三种细粒度的权限控制: | 权限类型 | 判断依据 | 应用场景 | 实现方式 | |---------|---------|---------|---------| | **权限码权限** | 菜单的 `name` 字段 | 功能模块权限控制 | 函数、指令、组件 | | **角色权限** | 角色的 `code` 字段 | 基于职责的权限控制 | 函数、指令 | | **用户权限** | 用户的 `username` 字段 | 特定用户权限控制 | 函数、指令 | ::: info 实现原理 权限系统基于用户登录后获取的权限数据,通过对比当前用户拥有的权限码、角色码和用户标识来判断是否有权限访问特定功能。权限数据存储在前端状态管理中,实现高效的权限验证。 ::: ## 权限助手函数 ### 函数引入和基本用法 MineAdmin 提供三个核心权限判断函数,位于 `web/src/utils/permission/` 目录下: ```javascript // 权限码检查函数 import hasAuth from '@/utils/permission/hasAuth' // 角色检查函数 import hasRole from '@/utils/permission/hasRole' // 用户检查函数 import hasUser from '@/utils/permission/hasUser' ``` ::: tip 函数位置说明 **源码路径**: * GitHub: `https://github.com/mineadmin/mineadmin/tree/master/web/src/utils/permission/` * 本地开发: `/web/src/utils/permission/` 这些函数已在全局注册,支持在组件中直接调用。 ::: ### 业务逻辑中使用 ```vue ``` ### 模板中使用 ```vue ``` ### 函数参数说明 所有权限函数都支持以下两种参数格式: ```javascript // 字符串格式 - 单一权限检查 hasAuth('user:list') hasRole('admin') hasUser('admin') // 数组格式 - 多权限检查(OR逻辑) hasAuth(['user:list', 'user:create', 'user:edit']) hasRole(['admin', 'manager', 'supervisor']) hasUser(['admin', 'root', 'system']) ``` ::: warning 注意事项 * 数组参数采用 **OR 逻辑**,即只要满足其中任一条件即返回 `true` * 如需 **AND 逻辑**,请使用多个函数调用组合:`hasAuth('a') && hasAuth('b')` * 权限码建议采用 `模块:操作` 的命名规范,如 `user:list`、`role:create` ::: ### 路由权限参数 权限函数支持第二个可选参数 `checkRoute`,用于是否同时检查路由权限: ```javascript // 第二个参数默认为 false,仅检查功能权限 hasAuth('user:list', false) // 设置为 true 时,同时检查路由权限 hasAuth('user:list', true) ``` ## 权限指令 MineAdmin 提供了三个权限指令,简化了模板中的权限控制。指令位于 `web/src/directives/permission/` 目录下: ::: tip 指令源码位置 **GitHub路径**: * `https://github.com/mineadmin/mineadmin/tree/master/web/src/directives/permission/auth/` * `https://github.com/mineadmin/mineadmin/tree/master/web/src/directives/permission/role/` * `https://github.com/mineadmin/mineadmin/tree/master/web/src/directives/permission/user/` **本地路径**:`/web/src/directives/permission/` ::: ### 指令使用方式 ```vue ``` ### 指令 vs 函数对比 | 方式 | 优势 | 适用场景 | 示例 | |------|------|----------|------| | **指令方式** | 简洁直观,自动控制元素显示/隐藏 | 简单的权限控制,静态权限检查 | `v-auth="'user:list'"` | | **函数方式** | 灵活性高,支持复杂逻辑判断 | 业务逻辑中的权限判断,动态权限检查 | `v-if="hasAuth('a') && hasRole('b')"` | ::: warning 指令使用注意事项 * 指令采用 **OR 逻辑**,数组中任一条件满足即显示元素 * 指令直接控制 DOM 元素的显示/隐藏,无权限时元素不会渲染 * 复杂的权限逻辑组合建议使用函数方式而非指令 ::: ## MaAuth 权限组件 ### 组件介绍 `MaAuth` 组件是 MineAdmin 提供的权限控制组件,适用于大范围内容的权限控制。相比函数和指令,组件方式更适合复杂的权限展示逻辑。 ::: info 组件源码位置 **GitHub 路径**:`https://github.com/mineadmin/mineadmin/tree/master/web/src/components/ma-auth/index.vue` **本地路径**:`/web/src/components/ma-auth/index.vue` 该组件已全局注册,在任何 Vue 组件中都可直接使用,无需手动导入。 ::: ### 基本使用 ```vue ``` ### 无权限时的提示 组件提供了 `#notAuth` 插槽,用于自定义无权限时的显示内容: ```vue ``` ### 高级用法 #### 嵌套权限控制 ```vue ``` #### 与其他组件结合 ```vue ``` ### 组件参数 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `value` | `string \| string[]` | `[]` | 需要验证的权限码,支持字符串或数组 | ### 组件插槽 | 插槽名 | 说明 | 参数 | |--------|------|------| | `default` | 有权限时显示的内容 | - | | `notAuth` | 无权限时显示的内容 | - | ### 组件 vs 其他方式对比 | 方式 | 适用场景 | 优势 | 劣势 | |------|----------|------|------| | **MaAuth 组件** | 大块内容权限控制、需要无权限提示 | 支持插槽自定义、代码结构清晰 | 稍显冗余 | | **权限指令** | 简单元素权限控制 | 简洁直观 | 不支持无权限提示 | | **权限函数** | 复杂业务逻辑权限判断 | 灵活性最高 | 需要手动处理显示逻辑 | ## 路由权限控制 ### 静态路由权限配置 MineAdmin 支持在路由级别进行权限控制,通过在路由的 `meta` 属性中配置权限参数来实现访问控制。 ::: tip 路由权限机制 **控制范围**:仅对带组件页面的路由生效,不包含按钮等页面内元素 **检查时机**:路由跳转时自动检查权限 **权限验证失败**:显示 403 页面 **源码位置**:`/web/src/router/` - 路由配置和权限守卫逻辑 ::: ### 路由权限配置语法 在路由配置文件中,通过 `meta` 对象配置权限参数: ```javascript // 示例路由配置 const routes = [ { path: '/user', name: 'User', component: () => import('@/views/user/index.vue'), meta: { // 权限码控制 - 需要用户管理权限 auth: ['user:list', 'user:manage'], // 角色控制 - 需要管理员或超级管理员角色 role: ['admin', 'SuperAdmin'], // 用户控制 - 特定用户可访问 user: ['admin', 'root'] } }, { path: '/system', name: 'System', component: () => import('@/views/system/index.vue'), meta: { // 只需要权限码 auth: ['system:config'] } }, { path: '/public', name: 'Public', component: () => import('@/views/public/index.vue'), meta: { // 不配置权限参数或设置为空数组,表示无权限限制 auth: [] } } ] ``` ### 权限参数说明 | 参数 | 类型 | 说明 | 逻辑关系 | |------|------|------|----------| | `auth` | `string[]` | 权限码数组,基于菜单权限控制 | OR(满足任一权限即可) | | `role` | `string[]` | 角色码数组,基于用户角色控制 | OR(满足任一角色即可) | | `user` | `string[]` | 用户名数组,基于特定用户控制 | OR(满足任一用户即可) | ::: warning 权限配置注意事项 * 所有权限参数类型必须为 `string[]`(字符串数组) * 同一路由可同时配置多种权限类型,关系为 **AND 逻辑** * 不配置权限参数或设置为空数组 `[]` 表示无权限限制 * 权限验证失败会自动跳转到 403 页面 ::: ### 实际应用场景 #### 用户管理模块 ```javascript // 用户管理相关路由 const userRoutes = [ { path: '/user', name: 'UserManagement', component: () => import('@/views/user/index.vue'), meta: { title: '用户管理', auth: ['user:list'] // 需要用户列表权限 }, children: [ { path: 'create', name: 'UserCreate', component: () => import('@/views/user/create.vue'), meta: { title: '新增用户', auth: ['user:create'] // 需要用户创建权限 } }, { path: 'edit/:id', name: 'UserEdit', component: () => import('@/views/user/edit.vue'), meta: { title: '编辑用户', auth: ['user:edit'] // 需要用户编辑权限 } } ] } ] ``` #### 系统管理模块 ```javascript // 系统管理 - 需要多重权限验证 const systemRoutes = [ { path: '/system', name: 'SystemManagement', component: () => import('@/views/system/index.vue'), meta: { title: '系统管理', auth: ['system:config'], // 需要系统配置权限 role: ['SuperAdmin'] // 且需要超级管理员角色 } }, { path: '/logs', name: 'SystemLogs', component: () => import('@/views/logs/index.vue'), meta: { title: '系统日志', auth: ['log:operation', 'log:login'], // 需要操作日志或登录日志权限 role: ['admin', 'auditor'] // 且需要管理员或审计员角色 } } ] ``` #### 特殊权限控制 ```javascript // 开发调试页面 - 仅特定用户可访问 const devRoutes = [ { path: '/dev-tools', name: 'DevTools', component: () => import('@/views/dev/index.vue'), meta: { title: '开发工具', user: ['admin', 'developer'], // 仅管理员和开发者用户可访问 auth: ['dev:tools'] // 且需要开发工具权限 } } ] ``` ### 权限验证流程 ```mermaid graph TD A[用户访问路由] --> B{路由是否配置权限?} B -->|否| C[直接允许访问] B -->|是| D{检查权限码auth} D -->|不满足| E[跳转403页面] D -->|满足| F{检查角色role} F -->|不满足| E F -->|满足| G{检查用户user} G -->|不满足| E G -->|满足| H[允许访问] C --> I[渲染页面组件] H --> I E --> J[显示403无权限页面] ``` ### 权限守卫实现 MineAdmin 的路由权限守卫逻辑位于路由配置中,核心实现逻辑: ```javascript // 路由守卫示例(简化版本) router.beforeEach((to, from, next) => { const { auth, role, user } = to.meta || {} // 无权限限制,直接通过 if (!auth?.length && !role?.length && !user?.length) { return next() } // 检查权限码 if (auth?.length && !hasAuth(auth)) { return next({ name: '403' }) } // 检查角色 if (role?.length && !hasRole(role)) { return next({ name: '403' }) } // 检查用户 if (user?.length && !hasUser(user)) { return next({ name: '403' }) } next() }) ``` ## 最佳实践 ### 权限粒度建议 1. **页面级权限**:使用路由 meta 配置 2. **功能级权限**:使用 MaAuth 组件 3. **元素级权限**:使用权限指令 4. **逻辑级权限**:使用权限函数 ### 权限命名规范 ```javascript // 推荐的权限码命名规范 - 模块:操作 格式 const permissionCodes = [ 'user:list', // 用户列表 'user:create', // 用户创建 'user:edit', // 用户编辑 'user:delete', // 用户删除 'role:manage', // 角色管理 'system:config', // 系统配置 'log:operation', // 操作日志 'log:login' // 登录日志 ] // 角色命名建议 const roleCodes = [ 'SuperAdmin', // 超级管理员 'admin', // 管理员 'manager', // 经理 'operator', // 操作员 'viewer' // 观察者 ] ``` ### 性能优化建议 1. **避免深层嵌套**:过多的权限组件嵌套会影响性能 2. **合理缓存**:权限数据应适当缓存,避免频繁请求 3. **按需加载**:结合路由懒加载,仅加载有权限的页面组件 4. **权限预检**:在数据请求前进行权限预检,避免无效请求 ### 常见问题和解决方案 #### 1. 权限验证失效 **问题**:权限函数返回 `false`,但实际应该有权限 **解决方案**: * 检查用户登录状态和权限数据是否正确加载 * 确认权限码、角色码、用户名拼写正确 * 查看浏览器控制台是否有相关错误信息 #### 2. 403 页面频繁出现 **问题**:用户访问页面时经常看到 403 错误页面 **解决方案**: * 检查路由 meta 配置是否过于严格 * 确认用户角色和权限分配是否合理 * 考虑添加默认权限或降低权限要求 #### 3. 权限组件不生效 **问题**:MaAuth 组件没有正确控制内容显示 **解决方案**: ```vue 内容 内容 ``` ## 权限系统架构图 ```mermaid graph TB A[用户登录] --> B[获取权限数据] B --> C[存储到状态管理] subgraph 权限控制层 D[路由权限守卫] E[权限助手函数] F[权限指令] G[MaAuth组件] end C --> D C --> E C --> F C --> G D --> H[页面访问控制] E --> I[业务逻辑控制] F --> J[元素显示控制] G --> K[内容块控制] H --> L[渲染页面/403页面] I --> M[执行业务逻辑] J --> N[显示/隐藏元素] K --> O[显示内容/无权限提示] ``` ### 核心特性 * **多层次权限控制**:从路由到元素的全方位权限管控 * **三种权限类型**:权限码、角色、用户三种粒度的权限验证 * **多种实现方式**:函数、指令、组件三种使用方式满足不同场景 * **易于集成**:与 Vue 3 和 Element Plus 深度集成,使用简便 ### 源码位置总结 | 功能 | GitHub 路径 | 本地路径 | |------|-------------|----------| | 权限函数 | `https://github.com/mineadmin/mineadmin/tree/master/web/src/utils/permission/` | `/web/src/utils/permission/` | | 权限指令 | `https://github.com/mineadmin/mineadmin/tree/master/web/src/directives/permission/` | `/web/src/directives/permission/` | | 权限组件 | `https://github.com/mineadmin/mineadmin/tree/master/web/src/components/ma-auth/` | `/web/src/components/ma-auth/` | | 路由配置 | `https://github.com/mineadmin/mineadmin/tree/master/web/src/router/` | `/web/src/router/` | ### 选择建议 根据不同的应用场景选择合适的权限控制方式: * **页面级控制** → 路由 meta 配置 * **大块内容控制** → MaAuth 组件 * **简单元素控制** → 权限指令 * **复杂业务逻辑** → 权限函数 通过合理使用这些权限控制工具,可以构建出安全、易维护的前端权限管理系统。 --- --- url: /v3/backend/base/event-handler.md --- # 事件 本文已迁移到 [Hyperf 事件](/backend/frameworks/hyperf/3.2/base/event-handler)。 新的后端文档按 [公共契约](/v3/backend/contracts/) 和 [框架实现](/backend/frameworks/hyperf/) 组织。旧地址保留用于兼容历史链接。 --- --- url: /backend/frameworks/hyperf/3.1/base/event-handler.md --- # 事件处理器 MineAdmin 的事件系统基于 [hyperf/event](https://github.com/hyperf/event) 构建,提供了强大的事件驱动机制。事件系统允许你在应用程序的不同部分之间进行松耦合的通信,提高代码的可维护性和扩展性。 ## 概述 事件系统采用观察者模式,包含以下核心概念: * **事件(Event)**:表示系统中发生的某个动作或状态变化 * **监听器(Listener)**:响应特定事件的处理逻辑 * **事件调度器(Dispatcher)**:负责事件的分发和监听器的调用 ## 系统内置监听器 MineAdmin 提供了多个内置监听器来处理系统核心功能: | 监听器 | 功能描述 | 状态 | 配置文件位置 | |---------------------------------|------------------------------------------------------------|------|------------| | ErrorExceptionHandler | 错误异常处理器,将符合条件的错误转换为异常抛出 | 默认启用 | `config/autoload/exceptions.php` | | UploadSubscriber | 文件上传事件订阅器,处理文件上传相关的业务逻辑 | 默认启用 | `config/autoload/listeners.php` | | BootApplicationSubscriber | 应用启动订阅器,在程序启动时注册数据库迁移和种子文件目录 | 默认启用 | `config/autoload/listeners.php` | | DbQueryExecutedSubscriber | 数据库查询监听器,根据环境配置记录和输出 SQL 执行信息 | 可配置 | `config/autoload/listeners.php` | | FailToHandleSubscriber | 命令执行失败监听器,当控制台命令执行失败时记录错误信息 | 默认启用 | `config/autoload/listeners.php` | | ResumeExitCoordinatorSubscriber | 进程退出协调器,处理 Worker 进程的优雅退出 | 默认启用 | `config/autoload/listeners.php` | | QueueHandleSubscriber | 队列处理监听器,监听队列任务的执行状态并记录相关信息 | 默认启用 | `config/autoload/listeners.php` | | RegisterBlueprintListener | 蓝图注册监听器,用于注册新的 API 蓝图方法 | 默认启用 | `config/autoload/listeners.php` | ## 创建自定义事件 ### 1. 定义事件类 创建事件类文件 `app/Event/UserRegisteredEvent.php`: ```php sendWelcomeEmail($event->email, $event->username); // 记录用户注册日志 $this->logUserRegistration($event->userId, $event->username); // 初始化用户默认设置 $this->initializeUserSettings($event->userId); } } private function sendWelcomeEmail(string $email, string $username): void { // 邮件发送逻辑 } private function logUserRegistration(int $userId, string $username): void { // 日志记录逻辑 } private function initializeUserSettings(int $userId): void { // 用户设置初始化逻辑 } } ``` ### 3. 触发事件 在业务代码中触发事件: ```php createUser($userData); // 触发用户注册事件 $event = new UserRegisteredEvent( userId: $userId, username: $userData['username'], email: $userData['email'], extra: ['ip' => $this->getClientIp()] ); $this->eventDispatcher->dispatch($event); return $userId; } private function createUser(array $userData): int { // 实际的用户创建逻辑 return 123; // 示例返回值 } private function getClientIp(): string { // 获取客户端 IP 地址 return '192.168.1.1'; // 示例返回值 } } ``` ## 异步事件处理 对于耗时的事件处理,可以使用队列进行异步处理: ```php handleAsync($event->userId, $event->email); } } #[AsyncQueueMessage] public function handleAsync(int $userId, string $email): void { // 这里的代码将在队列中异步执行 // 例如:发送复杂的欢迎邮件、生成报告等 sleep(5); // 模拟耗时操作 echo "异步处理用户 {$userId} 的注册后续任务完成\n"; } } ``` ## 事件监听器优先级 可以通过设置优先级来控制监听器的执行顺序: ```php handleEvent($event); } catch (\Throwable $e) { // 记录错误日志,但不阻断其他监听器 logger()->error('事件处理失败', [ 'event' => get_class($event), 'listener' => static::class, 'error' => $e->getMessage(), ]); } } ``` ### 4. 性能优化 * 对于非关键业务使用异步队列处理 * 避免在监听器中执行过重的数据库操作 * 合理使用事件优先级避免依赖关系复杂化 ## 调试和测试 ### 事件测试示例 ```php container); // 模拟监听器处理 $listener->process($event); // 断言处理结果 $this->assertTrue(true); // 根据实际业务逻辑编写断言 } } ``` ## 相关文档 * [Hyperf 事件文档](https://hyperf.wiki/3.1/#/zh-cn/event) * [异步队列文档](https://hyperf.wiki/3.1/#/zh-cn/async-queue) --- --- url: /backend/frameworks/hyperf/3.2/base/event-handler.md --- # 事件处理器 MineAdmin 的事件系统基于 [hyperf/event](https://github.com/hyperf/event) 构建,提供了强大的事件驱动机制。事件系统允许你在应用程序的不同部分之间进行松耦合的通信,提高代码的可维护性和扩展性。 ## 概述 事件系统采用观察者模式,包含以下核心概念: * **事件(Event)**:表示系统中发生的某个动作或状态变化 * **监听器(Listener)**:响应特定事件的处理逻辑 * **事件调度器(Dispatcher)**:负责事件的分发和监听器的调用 ## 系统内置监听器 MineAdmin 提供了多个内置监听器来处理系统核心功能: | 监听器 | 功能描述 | 状态 | 配置文件位置 | |---------------------------------|------------------------------------------------------------|------|------------| | ErrorExceptionHandler | 错误异常处理器,将符合条件的错误转换为异常抛出 | 默认启用 | `config/autoload/exceptions.php` | | UploadSubscriber | 文件上传事件订阅器,处理文件上传相关的业务逻辑 | 默认启用 | `config/autoload/listeners.php` | | BootApplicationSubscriber | 应用启动订阅器,在程序启动时注册数据库迁移和种子文件目录 | 默认启用 | `config/autoload/listeners.php` | | DbQueryExecutedSubscriber | 数据库查询监听器,根据环境配置记录和输出 SQL 执行信息 | 可配置 | `config/autoload/listeners.php` | | FailToHandleSubscriber | 命令执行失败监听器,当控制台命令执行失败时记录错误信息 | 默认启用 | `config/autoload/listeners.php` | | ResumeExitCoordinatorSubscriber | 进程退出协调器,处理 Worker 进程的优雅退出 | 默认启用 | `config/autoload/listeners.php` | | QueueHandleSubscriber | 队列处理监听器,监听队列任务的执行状态并记录相关信息 | 默认启用 | `config/autoload/listeners.php` | | RegisterBlueprintListener | 蓝图注册监听器,用于注册新的 API 蓝图方法 | 默认启用 | `config/autoload/listeners.php` | ## 创建自定义事件 ### 1. 定义事件类 创建事件类文件 `app/Event/UserRegisteredEvent.php`: ```php sendWelcomeEmail($event->email, $event->username); // 记录用户注册日志 $this->logUserRegistration($event->userId, $event->username); // 初始化用户默认设置 $this->initializeUserSettings($event->userId); } } private function sendWelcomeEmail(string $email, string $username): void { // 邮件发送逻辑 } private function logUserRegistration(int $userId, string $username): void { // 日志记录逻辑 } private function initializeUserSettings(int $userId): void { // 用户设置初始化逻辑 } } ``` ### 3. 触发事件 在业务代码中触发事件: ```php createUser($userData); // 触发用户注册事件 $event = new UserRegisteredEvent( userId: $userId, username: $userData['username'], email: $userData['email'], extra: ['ip' => $this->getClientIp()] ); $this->eventDispatcher->dispatch($event); return $userId; } private function createUser(array $userData): int { // 实际的用户创建逻辑 return 123; // 示例返回值 } private function getClientIp(): string { // 获取客户端 IP 地址 return '192.168.1.1'; // 示例返回值 } } ``` ## 异步事件处理 对于耗时的事件处理,可以使用队列进行异步处理: ```php handleAsync($event->userId, $event->email); } } #[AsyncQueueMessage] public function handleAsync(int $userId, string $email): void { // 这里的代码将在队列中异步执行 // 例如:发送复杂的欢迎邮件、生成报告等 sleep(5); // 模拟耗时操作 echo "异步处理用户 {$userId} 的注册后续任务完成\n"; } } ``` ## 事件监听器优先级 可以通过设置优先级来控制监听器的执行顺序: ```php handleEvent($event); } catch (\Throwable $e) { // 记录错误日志,但不阻断其他监听器 logger()->error('事件处理失败', [ 'event' => get_class($event), 'listener' => static::class, 'error' => $e->getMessage(), ]); } } ``` ### 4. 性能优化 * 对于非关键业务使用异步队列处理 * 避免在监听器中执行过重的数据库操作 * 合理使用事件优先级避免依赖关系复杂化 ## 调试和测试 ### 事件测试示例 ```php container); // 模拟监听器处理 $listener->process($event); // 断言处理结果 $this->assertTrue(true); // 根据实际业务逻辑编写断言 } } ``` ## 相关文档 * [Hyperf 事件文档](https://hyperf.wiki/3.1/#/zh-cn/event) * [异步队列文档](https://hyperf.wiki/3.1/#/zh-cn/async-queue) --- --- url: /backend/frameworks/hyperf/3.1/data-permission/example.md --- # 使用示例 在代码中使用数据权限,目前支持多种方式进行动态开启与关闭。以下举几种常见的开发案例 ## 对整个模型进行数据权限控制 以默认的 `User` 模型为例,假设我们需要对 `User` 模型进行数据权限控制。 可以在 `User` 模型中 use `DataScopes` trait 来启用数据权限作用域。 ```php // /mineadmin/app/Library/DataPermission/Scope/DataScope.php use App\Library\DataPermission\Scope\DataScopes; class User extends Model { // 使用数据权限作用域 use DataScopes; // 其他代码... } ``` 这样会使所有对 `User` 模型的查询都自动应用数据权限控制。 ## 对某个代码块进行数据权限控制 得益于 Hyperf AOP 特性,我们可以在类或者类方法上使用 [DataScope](https://github.com/mineadmin/MineAdmin/blob/master-department/app/Library/DataPermission/Attribute/DataScope.php) 注解来对指定的代码块开启数据权限控制。 以自带的用户模块的[分页列表](https://github.com/mineadmin/MineAdmin/blob/master-department/app/Service/Permission/UserService.php#L93~L100)为例 ```php // /mineadmin/app/Service/Permission/UserService.php:94-98 class UserService { #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['user'], createdByColumn: 'id' )] public function page(array $params, int $page = 1, int $pageSize = 10): array { return parent::page($params, $page, $pageSize); } } ``` 这样在调用 `page` 方法时,数据权限会自动应用到查询中。 ## 对指定的 ORM Query 进行权限控制 在某些特定场景中,需要更加细化的进行权限隔离。此时可以通过 `Factory::make()->build()` 方法来对指定的 Query 进行限制 ```php // 手动使用 Factory 进行权限过滤 use App\Library\DataPermission\Factory; use App\Model\Permission\User; class DemoService { public function test(User $user): void { $userQuery = User::query(); // 对 UserQuery 单独进行数据隔离拼接 Factory::make()->build($userQuery->getQuery(), $user); $result = $userQuery->get(); } } ``` ## 使用 Context 配置数据权限 ### 基本配置方式 ```php // /mineadmin/app/Library/DataPermission/Context.php use App\Library\DataPermission\Context; use App\Library\DataPermission\ScopeType; // 设置部门字段名称 Context::setDeptColumn('department_id'); // 设置创建人字段名称 Context::setCreatedByColumn('creator'); // 设置隔离方式 Context::setScopeType(ScopeType::DEPT_CREATED_BY); // 只对指定表进行数据隔离 Context::setOnlyTables(['user', 'orders']); $query = User::query(); Factory::make()->build($query->getQuery(), $user); ``` ### DataScope 注解完整配置 ```php // /mineadmin/app/Library/DataPermission/Attribute/DataScope.php #[DataScope( // 部门字段名称(默认:dept_id) deptColumn: 'department_id', // 创建人字段名称(默认:created_by) createdByColumn: 'creator_id', // 隔离方式(默认:ScopeType::DEPT_CREATED_BY) scopeType: ScopeType::DEPT_SELF, // 只对指定表生效(默认:null,对所有表生效) onlyTables: ['orders', 'order_items'] )] public function getFilteredData(): Collection { return Order::with('items')->get(); } ``` ## 策略类型使用示例 ### DEPT\_SELF - 本部门权限 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Model/Enums/DataPermission/PolicyType.php #[DataScope(scopeType: ScopeType::DEPT)] public function getDepartmentUsers(): Collection { // 自动过滤为当前用户所在部门的用户 return User::query()->get(); } ``` ### DEPT\_TREE - 部门树权限 ```php #[DataScope(scopeType: ScopeType::DEPT)] public function getDepartmentTreeUsers(): Collection { // 自动包含当前部门及所有子部门的用户 return User::query()->get(); } ``` ### CREATED\_BY - 创建人权限 ```php #[DataScope(scopeType: ScopeType::CREATED_BY)] public function getMyCreatedData(): Collection { // 只返回当前用户创建的数据 return Document::query()->get(); } ``` ### 组合权限使用 ```php // AND 条件:既要是本部门,又要是自己创建的 #[DataScope(scopeType: ScopeType::DEPT_CREATED_BY)] public function getMyDeptData(): Collection { return Document::query()->get(); } // OR 条件:本部门的或者自己创建的 #[DataScope(scopeType: ScopeType::DEPT_OR_CREATED_BY)] public function getDeptOrMyData(): Collection { return Document::query()->get(); } ``` ## 自定义函数策略 ### 创建自定义函数 ```php // /mineadmin/config/autoload/department/custom.php return [ 'my_custom_filter' => function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) { // 自定义业务逻辑 $createdByColumn = Context::getCreatedByColumn(); $deptColumn = Context::getDeptColumn(); switch ($scopeType) { case ScopeType::CREATED_BY: $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); break; case ScopeType::DEPT_CREATED_BY: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT_OR_CREATED_BY: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); $builder->orWhere($createdByColumn, $user->id); break; } } ]; ``` ### 使用自定义函数 要使用自定义函数,需要设置策略类型为 `CUSTOM_FUNC` 并在策略的 `value` 字段中指定函数名: ```php // 在数据库中创建策略记录 Policy::create([ 'user_id' => $userId, 'policy_type' => PolicyType::CustomFunc, 'value' => ['my_custom_filter'], // 自定义函数名 'is_default' => true ]); ``` ## 数据权限结合业务场景 ### 订单管理系统 ```php class OrderService { // 普通员工只能看到自己创建的订单 #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['orders'] )] public function getMyOrders(): Collection { return Order::query()->get(); } // 部门经理可以看到整个部门的订单 #[DataScope( scopeType: ScopeType::DEPT, onlyTables: ['orders'] )] public function getDepartmentOrders(): Collection { return Order::query()->get(); } // 区域经理可以看到部门树的所有订单 #[DataScope( scopeType: ScopeType::DEPT, onlyTables: ['orders'] )] public function getRegionOrders(): Collection { return Order::query()->get(); } } ``` ### 文档管理系统 ```php class DocumentService { // 文档权限:部门共享 + 个人私有 #[DataScope( scopeType: ScopeType::DEPT_OR_CREATED_BY, deptColumn: 'department_id', createdByColumn: 'author_id' )] public function getAccessibleDocuments(): Collection { return Document::query() ->where('status', 'published') ->get(); } } ``` ## 多表关联查询 当涉及多表关联时,可以指定 `onlyTables` 参数来控制哪些表应用数据权限: ```php #[DataScope( scopeType: ScopeType::DEPT_SELF, onlyTables: ['orders'] // 只对 orders 表应用权限过滤 )] public function getOrdersWithCustomers(): Collection { return Order::query() ->join('customers', 'orders.customer_id', '=', 'customers.id') ->select('orders.*', 'customers.name as customer_name') ->get(); } ``` ## 注意事项 ::: warning 协程上下文 需要注意,不管哪种使用方式。如果新开一个协程,都要重新设置一遍才能起效 ```php // ❌ 错误:协程中权限丢失 Co::create(function () { // 权限配置已丢失,可能返回错误数据 $data = User::query()->get(); }); // ✅ 正确:重新设置协程权限 Co::create(function () use ($user) { // 需要重新设置上下文 Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); Context::setScopeType(ScopeType::DEPT_SELF); $data = User::query()->get(); }); ``` ::: ## 完整 API 参考 ### DataScope 注解参数 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `deptColumn` | string | `'dept_id'` | 部门字段名称 | | `createdByColumn` | string | `'created_by'` | 创建人字段名称 | | `scopeType` | ScopeType | `ScopeType::DEPT_CREATED_BY` | 权限范围类型 | | `onlyTables` | array|null | `null` | 只对指定表生效,null则对所有表生效 | ### Context 类方法 | 方法 | 说明 | |------|------| | `Context::setDeptColumn(string)` | 设置部门字段名 | | `Context::setCreatedByColumn(string)` | 设置创建人字段名 | | `Context::setScopeType(ScopeType)` | 设置权限范围类型 | | `Context::setOnlyTables(array)` | 设置只对指定表生效 | | `Context::getDeptColumn()` | 获取部门字段名 | | `Context::getCreatedByColumn()` | 获取创建人字段名 | | `Context::getScopeType()` | 获取权限范围类型 | | `Context::getOnlyTables()` | 获取指定表列表 | ### Factory 类方法 | 方法 | 说明 | |------|------| | `Factory::make()` | 创建工厂实例 | | `Factory::build(Builder, User)` | 对查询构建器应用权限过滤 | ### ScopeType 枚举值 | 值 | 说明 | |----|------| | `ScopeType::DEPT` | 只根据部门过滤 | | `ScopeType::CREATED_BY` | 只根据创建人过滤 | | `ScopeType::DEPT_CREATED_BY` | 根据部门 AND 创建人过滤 | | `ScopeType::DEPT_OR_CREATED_BY` | 根据部门 OR 创建人过滤 | ### PolicyType 枚举值 | 值 | 说明 | |----|------| | `PolicyType::DeptSelf` | 本部门 | | `PolicyType::DeptTree` | 本部门及子部门 | | `PolicyType::All` | 全部数据 | | `PolicyType::Self` | 仅本人 | | `PolicyType::CustomDept` | 自定义部门 | | `PolicyType::CustomFunc` | 自定义函数 | --- --- url: /backend/frameworks/hyperf/3.2/data-permission/example.md --- # 使用示例 在代码中使用数据权限,目前支持多种方式进行动态开启与关闭。以下举几种常见的开发案例 ## 对整个模型进行数据权限控制 以默认的 `User` 模型为例,假设我们需要对 `User` 模型进行数据权限控制。 可以在 `User` 模型中 use `DataScopes` trait 来启用数据权限作用域。 ```php // /mineadmin/app/Library/DataPermission/Scope/DataScope.php use App\Library\DataPermission\Scope\DataScopes; class User extends Model { // 使用数据权限作用域 use DataScopes; // 其他代码... } ``` 这样会使所有对 `User` 模型的查询都自动应用数据权限控制。 ## 对某个代码块进行数据权限控制 得益于 Hyperf AOP 特性,我们可以在类或者类方法上使用 [DataScope](https://github.com/mineadmin/MineAdmin/blob/master-department/app/Library/DataPermission/Attribute/DataScope.php) 注解来对指定的代码块开启数据权限控制。 以自带的用户模块的[分页列表](https://github.com/mineadmin/MineAdmin/blob/master-department/app/Service/Permission/UserService.php#L93~L100)为例 ```php // /mineadmin/app/Service/Permission/UserService.php:94-98 class UserService { #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['user'], createdByColumn: 'id' )] public function page(array $params, int $page = 1, int $pageSize = 10): array { return parent::page($params, $page, $pageSize); } } ``` 这样在调用 `page` 方法时,数据权限会自动应用到查询中。 ## 对指定的 ORM Query 进行权限控制 在某些特定场景中,需要更加细化的进行权限隔离。此时可以通过 `Factory::make()->build()` 方法来对指定的 Query 进行限制 ```php // 手动使用 Factory 进行权限过滤 use App\Library\DataPermission\Factory; use App\Model\Permission\User; class DemoService { public function test(User $user): void { $userQuery = User::query(); // 对 UserQuery 单独进行数据隔离拼接 Factory::make()->build($userQuery->getQuery(), $user); $result = $userQuery->get(); } } ``` ## 使用 Context 配置数据权限 ### 基本配置方式 ```php // /mineadmin/app/Library/DataPermission/Context.php use App\Library\DataPermission\Context; use App\Library\DataPermission\ScopeType; // 设置部门字段名称 Context::setDeptColumn('department_id'); // 设置创建人字段名称 Context::setCreatedByColumn('creator'); // 设置隔离方式 Context::setScopeType(ScopeType::DEPT_CREATED_BY); // 只对指定表进行数据隔离 Context::setOnlyTables(['user', 'orders']); $query = User::query(); Factory::make()->build($query->getQuery(), $user); ``` ### DataScope 注解完整配置 ```php // /mineadmin/app/Library/DataPermission/Attribute/DataScope.php #[DataScope( // 部门字段名称(默认:dept_id) deptColumn: 'department_id', // 创建人字段名称(默认:created_by) createdByColumn: 'creator_id', // 隔离方式(默认:ScopeType::DEPT_CREATED_BY) scopeType: ScopeType::DEPT_SELF, // 只对指定表生效(默认:null,对所有表生效) onlyTables: ['orders', 'order_items'] )] public function getFilteredData(): Collection { return Order::with('items')->get(); } ``` ## 策略类型使用示例 ### DEPT\_SELF - 本部门权限 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Model/Enums/DataPermission/PolicyType.php #[DataScope(scopeType: ScopeType::DEPT)] public function getDepartmentUsers(): Collection { // 自动过滤为当前用户所在部门的用户 return User::query()->get(); } ``` ### DEPT\_TREE - 部门树权限 ```php #[DataScope(scopeType: ScopeType::DEPT)] public function getDepartmentTreeUsers(): Collection { // 自动包含当前部门及所有子部门的用户 return User::query()->get(); } ``` ### CREATED\_BY - 创建人权限 ```php #[DataScope(scopeType: ScopeType::CREATED_BY)] public function getMyCreatedData(): Collection { // 只返回当前用户创建的数据 return Document::query()->get(); } ``` ### 组合权限使用 ```php // AND 条件:既要是本部门,又要是自己创建的 #[DataScope(scopeType: ScopeType::DEPT_CREATED_BY)] public function getMyDeptData(): Collection { return Document::query()->get(); } // OR 条件:本部门的或者自己创建的 #[DataScope(scopeType: ScopeType::DEPT_OR_CREATED_BY)] public function getDeptOrMyData(): Collection { return Document::query()->get(); } ``` ## 自定义函数策略 ### 创建自定义函数 ```php // /mineadmin/config/autoload/department/custom.php return [ 'my_custom_filter' => function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) { // 自定义业务逻辑 $createdByColumn = Context::getCreatedByColumn(); $deptColumn = Context::getDeptColumn(); switch ($scopeType) { case ScopeType::CREATED_BY: $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); break; case ScopeType::DEPT_CREATED_BY: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT_OR_CREATED_BY: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); $builder->orWhere($createdByColumn, $user->id); break; } } ]; ``` ### 使用自定义函数 要使用自定义函数,需要设置策略类型为 `CUSTOM_FUNC` 并在策略的 `value` 字段中指定函数名: ```php // 在数据库中创建策略记录 Policy::create([ 'user_id' => $userId, 'policy_type' => PolicyType::CustomFunc, 'value' => ['my_custom_filter'], // 自定义函数名 'is_default' => true ]); ``` ## 数据权限结合业务场景 ### 订单管理系统 ```php class OrderService { // 普通员工只能看到自己创建的订单 #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['orders'] )] public function getMyOrders(): Collection { return Order::query()->get(); } // 部门经理可以看到整个部门的订单 #[DataScope( scopeType: ScopeType::DEPT, onlyTables: ['orders'] )] public function getDepartmentOrders(): Collection { return Order::query()->get(); } // 区域经理可以看到部门树的所有订单 #[DataScope( scopeType: ScopeType::DEPT, onlyTables: ['orders'] )] public function getRegionOrders(): Collection { return Order::query()->get(); } } ``` ### 文档管理系统 ```php class DocumentService { // 文档权限:部门共享 + 个人私有 #[DataScope( scopeType: ScopeType::DEPT_OR_CREATED_BY, deptColumn: 'department_id', createdByColumn: 'author_id' )] public function getAccessibleDocuments(): Collection { return Document::query() ->where('status', 'published') ->get(); } } ``` ## 多表关联查询 当涉及多表关联时,可以指定 `onlyTables` 参数来控制哪些表应用数据权限: ```php #[DataScope( scopeType: ScopeType::DEPT_SELF, onlyTables: ['orders'] // 只对 orders 表应用权限过滤 )] public function getOrdersWithCustomers(): Collection { return Order::query() ->join('customers', 'orders.customer_id', '=', 'customers.id') ->select('orders.*', 'customers.name as customer_name') ->get(); } ``` ## 注意事项 ::: warning 协程上下文 需要注意,不管哪种使用方式。如果新开一个协程,都要重新设置一遍才能起效 ```php // ❌ 错误:协程中权限丢失 Co::create(function () { // 权限配置已丢失,可能返回错误数据 $data = User::query()->get(); }); // ✅ 正确:重新设置协程权限 Co::create(function () use ($user) { // 需要重新设置上下文 Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); Context::setScopeType(ScopeType::DEPT_SELF); $data = User::query()->get(); }); ``` ::: ## 完整 API 参考 ### DataScope 注解参数 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `deptColumn` | string | `'dept_id'` | 部门字段名称 | | `createdByColumn` | string | `'created_by'` | 创建人字段名称 | | `scopeType` | ScopeType | `ScopeType::DEPT_CREATED_BY` | 权限范围类型 | | `onlyTables` | array|null | `null` | 只对指定表生效,null则对所有表生效 | ### Context 类方法 | 方法 | 说明 | |------|------| | `Context::setDeptColumn(string)` | 设置部门字段名 | | `Context::setCreatedByColumn(string)` | 设置创建人字段名 | | `Context::setScopeType(ScopeType)` | 设置权限范围类型 | | `Context::setOnlyTables(array)` | 设置只对指定表生效 | | `Context::getDeptColumn()` | 获取部门字段名 | | `Context::getCreatedByColumn()` | 获取创建人字段名 | | `Context::getScopeType()` | 获取权限范围类型 | | `Context::getOnlyTables()` | 获取指定表列表 | ### Factory 类方法 | 方法 | 说明 | |------|------| | `Factory::make()` | 创建工厂实例 | | `Factory::build(Builder, User)` | 对查询构建器应用权限过滤 | ### ScopeType 枚举值 | 值 | 说明 | |----|------| | `ScopeType::DEPT` | 只根据部门过滤 | | `ScopeType::CREATED_BY` | 只根据创建人过滤 | | `ScopeType::DEPT_CREATED_BY` | 根据部门 AND 创建人过滤 | | `ScopeType::DEPT_OR_CREATED_BY` | 根据部门 OR 创建人过滤 | ### PolicyType 枚举值 | 值 | 说明 | |----|------| | `PolicyType::DeptSelf` | 本部门 | | `PolicyType::DeptTree` | 本部门及子部门 | | `PolicyType::All` | 全部数据 | | `PolicyType::Self` | 仅本人 | | `PolicyType::CustomDept` | 自定义部门 | | `PolicyType::CustomFunc` | 自定义函数 | --- --- url: /v3/backend/contracts.md --- # 公共契约 公共契约用于描述 MineAdmin 后端实现之间必须保持一致的部分。无论底层使用 Hyperf、Laravel,还是未来其他语言或框架,面向前台模板、插件和外部系统时都应遵守这些约定。 ## 契约范围 | 契约 | 说明 | |------|------| | [数据模型](./data-model.md) | 核心业务实体、关系和字段语义保持一致。 | | [后台路由](./routing.md) | 后台资源、权限标识、操作语义和认证边界保持一致。 | | [接口元数据](./api-metadata.md) | OpenAPI/Swagger 元数据描述同一套请求和响应。 | | [响应结构](./response.md) | API 返回结构、业务状态码、错误消息和分页载荷保持一致。 | | [前台模板对接](./frontend-template.md) | 前台模板通过稳定接口契约对接不同后端实现。 | ## 与框架实现的关系 公共契约只描述“必须一致”的行为,不绑定具体框架 API。框架实现文档负责说明这些契约在对应框架中的落地方式,例如中间件注册、服务容器、生命周期、事件系统、异常处理和文件上传配置。 当前稳定实现是 [Hyperf 实现](/backend/frameworks/hyperf/),Laravel 入口已在 [Laravel 实现](/backend/frameworks/laravel/1.0/) 中预留。 --- --- url: /v3/guide/introduce/mineadmin.md --- # 关于 MineAdmin MineAdmin 是一个基于 Hyperf 框架的企业级后台管理系统,专为现代化应用开发而打造。如果您在进行后台框架相关的调研或技术选型,这篇文章将帮助您全面了解 MineAdmin 的核心优势、技术特色和完整功能体系。 ## 项目概述 MineAdmin 是一个现代化、高性能的后台管理系统解决方案,采用前后端分离架构,致力于为开发者提供开箱即用的企业级应用开发平台。系统具备完善的权限管理、模块化设计和丰富的业务组件,能够显著提升开发效率。 ## 长期且稳定 自 [2021 年 10 月 14 日](https://github.com/mineadmin/MineAdmin/commit/670f6439ba2a6fe8181bbf138c247bfb1d26601c)首次发布以来,`MineAdmin 已经走过了 {{ daysPassed }} 天的发展历程`。我们始终坚持长期稳定的开发路线,严格把控代码质量,确保每个版本都经过充分的测试和验证。 **稳定性承诺:** * 持续的版本迭代与维护 * 严格的代码审查机制 * 完善的单元测试覆盖 * 向下兼容性保障 * 活跃的社区支持 ::: tip 为什么选择 MineAdmin 我们致力于为个人开发者及企业团队提供一款现代化、简洁且高效的后台管理系统。凭借以下核心优势,助您在项目开发中事半功倍: **技术先进性** * 采用最新的技术栈:Hyperf 3.x、PHP 8.x、Vue 3.x、Vite 5.x 等,确保您的项目技术栈始终保持先进 * 基于 Swoole 协程的高性能架构,支持高并发业务场景 * TypeScript 全栈支持,提供更好的类型安全和开发体验 **代码品质保障** * 严格遵循 PSR 标准和最佳实践规范 * 模块化架构设计,确保代码的一致性和可维护性 * 完善的错误处理和日志记录机制 **灵活适应性** * 支持个人开发者的轻量级应用和初创项目的快速搭建 * 满足企业级应用的复杂业务需求和定制化要求 * 实现快速开发、快速部署、快速迭代的完整开发周期 ::: ## 技术架构 ### 后端技术栈 * **框架核心**:Hyperf 3.x - 基于 Swoole 的高性能 PHP 框架 * **语言版本**:PHP 8.1+ - 支持最新语言特性 * **数据库**:MySQL 8.0+ / PostgreSQL - 支持主流关系型数据库 * **缓存系统**:Redis - 高性能缓存和会话存储 * **权限控制**:基于 Casbin 的 RBAC 权限管理 * **API 文档**:Swagger/OpenAPI 自动生成 ### 前端技术栈 * **核心框架**:Vue 3.x - 组合式 API 和响应式系统 * **构建工具**:Vite 5.x - 极速的前端构建工具 * **UI 组件库**:Element Plus - 企业级组件库 * **状态管理**:Pinia - 轻量级状态管理方案 * **路由系统**:Vue Router 4.x - 官方路由解决方案 * **类型支持**:TypeScript - 完整的类型系统支持 ## 核心功能特性 ### 用户与权限管理 * **用户管理**:完整的用户生命周期管理,包括注册、认证、资料维护等 * **角色管理**:灵活的角色定义和权限分配,支持菜单权限和数据权限 * **菜单管理**:动态菜单配置,支持前端路由和按钮级权限控制 * **部门管理**:树形部门结构,支持数据权限的层级控制 ### 系统监控与日志 * **操作日志**:详细记录用户操作行为,支持审计追溯 * **登录日志**:用户登录记录和安全监控 * **系统监控**:服务器性能监控和应用状态监控 * **异常监控**:系统异常捕获和报警机制 ### 开发工具与扩展 * **代码生成器**:基于数据库表结构自动生成 CRUD 代码(前后端) * **API 文档**:自动生成和维护的 API 接口文档 * **数据字典**:统一的数据字典管理和维护 * **系统配置**:可视化的系统参数配置管理 ### 应用生态系统 * **插件市场**:丰富的插件生态,支持功能模块的快速集成 * **模板系统**:多种业务模板,加速项目初始化 * **用户中心**:独立的用户服务模块,支持个人信息管理和扩展功能 ## 发展历程与版本演进 ### 技术演进轨迹 **后端架构升级** * **v0.x-v1.x**:基于 Hyperf 2.x 的初期版本,确立核心架构 * **v2.x-v3.x**:跟随 Hyperf 3.x 升级,引入更多现代化特性 * **当前版本**:基于最新技术栈的全面重构版本 **前端技术变迁** * **早期版本**:基于 SCUI 开源项目的快速原型 * **中期发展**:基于 Arco Design 的自研前端框架 * **现代版本**:采用 Vue 3 + Vite + TypeScript 的现代化架构 ### 架构优化历程 我们在系统发展过程中不断进行架构优化和功能精简: 1. **代码重构**:全面重构前后端代码,提升代码质量和可维护性 2. **功能精简**:移除冗余功能,专注于核心业务场景 3. **性能优化**:基于实际使用场景进行性能调优 4. **用户体验提升**:持续改进界面设计和交互体验 ### 发展愿景 我们的目标是打造一个让开发者能够: * **快速上手**:降低学习成本,提供完善的文档和示例 * **专注业务**:减少基础设施开发,专注于业务逻辑实现 * **创造价值**:为企业和品牌提供稳定可靠的技术支撑 ## 适用场景 MineAdmin 适用于以下典型场景: * **企业内部管理系统**:OA、CRM、ERP 等企业管理应用 * **内容管理平台**:网站后台、内容发布系统 * **数据分析平台**:报表系统、数据可视化平台 * **电商管理系统**:商品管理、订单处理、用户管理 * **多租户 SaaS 应用**:支持多租户架构的云服务应用 通过 MineAdmin,您可以快速构建功能完善、性能卓越的现代化管理系统,专注于业务创新而非基础架构开发。 --- --- url: /v3/plugin/create.md --- # 创建应用 创建一个MineAdmin应用 ## [命令创建](./command.md#创建一个插件) MineAdmin可以通过命令行创建一个应用,首先,将当前命令行目录定位到我们的项目根目录,然后输入下面这行命令: ```shell php bin/hyperf.php mine-extension:create test/demo --name test --type mix --author zds --description 这是一个混合插件 ``` 执行此命令行后将会创建一个 plugin/test/demo 插件目录,[目录规范](./structure.md) ## 上传应用 进入 [应用发布页面](https://www.mineadmin.com/member/createApp),上传应用压缩包(.zip)格式,然后填写相关信息即可,然后等待管理员审核 --- --- url: /v3/backend/contracts/frontend-template.md --- # 前台模板对接契约 前台模板对接契约用于保证同一套 MineAdmin 前台模板可以连接不同后端实现。只要后端实现遵守公共契约,前台不应因为底层框架不同而维护多套请求逻辑。 ## 对接边界 前台模板主要依赖以下稳定能力: * 登录、刷新 Token、退出登录等认证接口。 * 用户信息、菜单、权限按钮和角色能力。 * 后台资源的列表、详情、创建、更新、删除接口。 * 数据权限过滤后的业务数据。 * 文件上传、附件访问和导入导出接口。 * 统一响应结构和错误提示规则。 ## 模板一致性 不同后端实现需要保持以下一致: * 接口路径和 HTTP 方法一致。 * 权限标识和菜单编码一致。 * 请求参数和响应字段一致。 * 分页、筛选、排序的参数语义一致。 * 文件上传字段、返回附件结构和访问地址语义一致。 ## 框架差异处理 框架差异应收敛在后端实现层,例如路由注册方式、中间件名称、ORM 事件、异常处理器、队列和文件系统配置。前台模板不应感知这些差异。 当某个框架实现无法完全复用公共契约时,需要在对应实现页明确标注差异,并同步说明前台模板是否需要额外适配。 --- --- url: /v3/plugin/front/develop.md --- # 前端开发规范 *** 以[创建插件章节](../create.md){style="color: green;" .custom-class #custom-id}为例,前端目录保持以下规范 1. `plugin/test/demo/web` 目录为前端目录 2. 此目录下的所有文件在插件安装时会复制(如果文件存在则会覆盖)到指定的前端目录中 --- --- url: /v3/front/advanced/module.md --- # 前端模块化系统 ::: tip 说明 MineAdmin 前端采用模块化架构,将**视图文件、API接口、国际化文件**等按功能进行模块化管理,提供清晰的代码组织结构。 ::: ## 模块系统架构 MineAdmin 的前端模块系统主要分为两个层面: 1. **核心模块系统** (`src/modules/`) - 业务功能模块 2. **插件系统** (`src/plugins/`) - 可扩展插件模块 ### 核心模块目录结构 ``` web/src/modules/ └── base/ # 基础核心模块 ├── api/ # API 接口定义 │ ├── attachment.ts # 附件管理接口 │ ├── log.ts # 日志管理接口 │ ├── menu.ts # 菜单管理接口 │ ├── permission.ts # 权限管理接口 │ ├── role.ts # 角色管理接口 │ └── user.ts # 用户管理接口 ├── locales/ # 国际化语言包 │ ├── en[English].yaml │ ├── zh_CN[简体中文].yaml │ └── zh_TW[繁體中文].yaml └── views/ # 视图组件 ├── dashboard/ # 仪表盘 ├── dataCenter/ # 数据中心 ├── log/ # 日志管理 ├── login/ # 登录页面 ├── permission/ # 权限管理 ├── uc/ # 用户中心 └── welcome/ # 欢迎页 ``` **源码位置:** * GitHub: * 本地路径: `mineadmin/web/src/modules` ## Base 模块详解 `base` 模块是系统的核心基础模块,包含了 MineAdmin 的所有基础功能:登录认证、权限管理、用户管理、菜单管理、日志监控等。 ### API 接口层设计 以用户管理 API 为例,展示标准的接口设计模式: ```typescript // web/src/modules/base/api/user.ts import type { PageList, ResponseStruct } from '#/global' export interface UserVo { id?: number username?: string user_type?: number nickname?: string phone?: string email?: string avatar?: string signed?: string dashboard?: string status?: 1 | 2 login_ip?: string login_time?: string backend_setting?: Record remark?: string password?: string } export interface UserSearchVo { username?: string nickname?: string phone?: string email?: string status?: number } // 标准 CRUD 接口 export function page(data: UserSearchVo): Promise>> { return useHttp().get('/admin/user/list', { params: data }) } export function create(data: UserVo): Promise> { return useHttp().post('/admin/user', data) } export function save(id: number, data: UserVo): Promise> { return useHttp().put(`/admin/user/${id}`, data) } export function deleteByIds(ids: number[]): Promise> { return useHttp().delete('/admin/user', { data: ids }) } ``` **源码位置:** * GitHub: * 本地路径: `mineadmin/web/src/modules/base/api/user.ts` ### 国际化支持 模块内的国际化文件采用 YAML 格式,支持多语言: ```yaml # web/src/modules/base/locales/zh_CN[简体中文].yaml baseUserManage: avatar: 头像 username: 用户名 nickname: 昵称 phone: 手机 email: 邮箱 password: 密码 userType: 用户类型 role: 角色 signed: 个人签名 mainTitle: 用户管理 subTitle: 提供用户添加、编辑、删除功能,超管不可修改。 baseRoleManage: mainTitle: 角色管理 subTitle: 提供用户角色、权限设置 name: 角色名称 code: 角色标识 permission: 权限菜单 setPermission: 赋予权限 ``` **源码位置:** * GitHub: * 本地路径: `mineadmin/web/src/modules/base/locales/zh_CN[简体中文].yaml` ## 插件系统 MineAdmin 还提供了强大的插件系统,位于 `src/plugins/` 目录。插件系统允许开发者创建独立的功能模块。 ### 插件目录结构 ``` web/src/plugins/ └── mine-admin/ # 官方插件命名空间 ├── app-store/ # 应用市场插件 │ ├── api/ # 插件 API │ ├── views/ # 插件视图 │ ├── utils/ # 插件工具 │ ├── style/ # 插件样式 │ └── index.ts # 插件入口 ├── basic-ui/ # 基础 UI 组件库 └── demo/ # 演示插件 ``` ### 插件配置示例 ```typescript // web/src/plugins/mine-admin/app-store/index.ts import type { Plugin } from '#/global' const pluginConfig: Plugin.PluginConfig = { install() { console.log('MineAdmin应用市场已启动') }, config: { enable: import.meta.env.DEV, info: { name: 'mine-admin/app-store', version: '1.0.0', author: 'X.Mo', description: '提供应用市场功能', }, }, views: [ { name: 'MineAppStoreRoute', path: '/appstore', meta: { title: '应用市场', badge: () => 'Hot', i18n: 'menu.appstore', icon: 'vscode-icons:file-type-azure', type: 'M', hidden: true, subForceShow: true, breadcrumbEnable: true, copyright: false, cache: true, }, component: () => import('./views/index.vue'), }, ], } export default pluginConfig ``` **源码位置:** * GitHub: * 本地路径: `mineadmin/web/src/plugins/mine-admin/app-store/index.ts` ### 插件注册机制 系统通过 Provider 服务自动扫描和注册插件: ```typescript // web/src/provider/plugins/index.ts const pluginList = {} async function getPluginList() { // 自动扫描插件目录 const plugins = import.meta.glob('../../plugins/*/*/index.ts') const sortedPlugins: any[] = [] for (const path in plugins) { const { default: plugin }: any = await plugins[path]() sortedPlugins.push(plugin) } // 按优先级排序插件 sort(sortedPlugins, f => f.config.info.order ?? 0, true).map((item) => { pluginList[item.config.info.name] = item }) } const provider: ProviderService.Provider = { name: 'plugins', async init() { await getPluginList() await getPluginConfig() }, setProvider(app: App) { app.config.globalProperties.$plugins = pluginList app.config.globalProperties.$pluginsConfig = pluginConfig }, getProvider(): any { return useGlobal().$plugins }, } ``` **源码位置:** * GitHub: * 本地路径: `mineadmin/web/src/provider/plugins/index.ts` ## 模块开发规范 ### 1. 创建新模块 当需要开发新功能时,建议创建独立模块而不是在 `base` 模块下添加: ```bash web/src/modules/ └── your-module/ ├── api/ # API 接口定义 │ └── index.ts ├── locales/ # 国际化文件 │ ├── en[English].yaml │ ├── zh_CN[简体中文].yaml │ └── zh_TW[繁體中文].yaml ├── views/ # 视图组件 │ └── index.vue └── types/ # 类型定义(可选) └── index.ts ``` ### 2. API 接口规范 每个模块的 API 接口应该: * 定义清晰的 TypeScript 接口类型 * 使用统一的 `useHttp()` 方法 * 遵循 RESTful API 设计原则 * 包含完整的 CRUD 操作 ### 3. 国际化规范 * 使用 YAML 格式 * 采用层级结构组织翻译键 * 为每个支持的语言创建对应文件 * 翻译键命名采用驼峰式 ### 4. 视图组件规范 * 使用 Vue 3 Composition API * 支持响应式设计 * 遵循 Element Plus 设计规范 * 组件应具备良好的可维护性 ## TypeScript 类型支持 系统提供了完整的 TypeScript 类型定义,包括插件配置、路由元信息等: ```typescript // types/global.d.ts declare namespace Plugin { interface Info { name: string version: string author: string description: string order?: number } interface Config { info: Info enable: boolean } interface PluginConfig { install: (app: App) => void config: Config views?: Views[] hooks?: { start?: (config: Config) => any | void setup?: () => any | void registerRoute?: (router: Router, routesRaw: Route.RouteRecordRaw[]) => any | void // ... 更多钩子 } } } ``` **源码位置:** * GitHub: * 本地路径: `mineadmin/web/types/global.d.ts` ## 最佳实践 ::: tip 开发建议 1. **模块职责单一**:每个模块专注于特定的业务领域 2. **API 接口统一**:使用标准化的接口设计模式 3. **国际化完整**:为所有文本内容提供多语言支持 4. **类型安全**:充分利用 TypeScript 类型系统 5. **插件优先**:对于可选功能,优先考虑插件方式实现 ::: ::: warning 注意事项 * 避免在 `base` 模块中添加业务特定功能 * 新模块应该保持独立性,减少与其他模块的耦合 * 插件的启用/禁用不应影响系统核心功能 * 所有模块都应该支持国际化 ::: --- --- url: /v3/front/advanced/cache.md --- # 前端缓存系统 MineAdmin 前端提供了完整的缓存系统,包括页面缓存、数据缓存和浏览器存储缓存等多层缓存策略。通过合理使用缓存机制,可以显著提升应用性能和用户体验。 ## 缓存类型概述 * **页面缓存**: 基于 Vue 的 `keep-alive` 机制,缓存页面组件状态 * **数据缓存**: 缓存 API 请求结果和用户数据 * **存储缓存**: 基于 localStorage/sessionStorage 的持久化缓存 * **路由缓存**: 缓存路由状态和标签页信息 ## 页面缓存 (Keep-Alive) 页面缓存基于 Vue 的 `keep-alive` 机制实现,用于缓存页面组件状态,避免重复渲染和数据请求。 ### 启用页面缓存 要启用页面缓存,需要满足以下三个条件: 1. **设置路由元信息**: 在路由的 `meta.cache` 属性设置为 `true` 2. **定义组件名称**: 在页面组件中使用 `defineOptions` 定义组件名称 3. **保持单一根节点**: 页面模板必须有且只有一个根节点 ### 实现示例 ```vue ``` ### 路由配置 #### 静态路由 ```typescript // src/router/static-routes/userRoute.ts export default { name: 'UserManagement', path: '/user/management', component: () => import('@/views/user/management/index.vue'), meta: { title: '用户管理', cache: true, // 开启缓存 icon: 'i-heroicons:users', type: 'M' } } ``` #### 动态路由(菜单管理) 对于通过后台菜单管理生成的动态路由,可以在菜单管理界面设置缓存属性: 1. 进入 **系统管理** → **菜单管理** 2. 编辑对应菜单项 3. 在表单中找到 **是否缓存** 开关 4. 开启后保存即可 参考菜单表单实现:[menu-form.vue#L175](https://github.com/mineadmin/mineadmin/blob/master/web/src/modules/base/views/permission/menu/menu-form.vue#L175) ### 缓存机制原理 系统通过以下方式实现页面缓存: 1. **路由守卫检测**: 在 `router.afterEach` 中检测路由的 `meta.cache` 属性 2. **组件名称收集**: 获取页面组件的 `name` 属性并添加到缓存列表 3. **Keep-Alive 包裹**: 在布局组件中使用 `` 包裹路由视图 核心实现代码: ```typescript // src/router/index.ts router.afterEach(async (to) => { const keepAliveStore = useKeepAliveStore() // 检查是否需要缓存且非iframe页面 if (to.meta.cache && to.meta.type !== 'I') { const componentName = to.matched.at(-1)?.components?.default!.name if (componentName) { keepAliveStore.add(componentName) // 添加到缓存列表 } else { console.warn(`组件页面未设置组件名,将不会被缓存`) } } }) ``` ```tsx // src/layouts/index.tsx {({ Component }) => ( {(keepAliveStore.getShowState() && route.meta.type !== 'I') && } )} ``` ### 缓存管理 #### 禁用页面缓存 有多种方式可以禁用页面缓存: 1. **不设置缓存属性**(推荐) ```typescript // 路由配置中不设置 cache 或设置为 false meta: { title: '临时页面', cache: false // 或者不设置此属性 } ``` 2. **不定义组件名称** ```vue ``` #### 清除页面缓存 系统提供了多种清除缓存的方法: ```typescript // 获取 keep-alive 存储实例 const keepAliveStore = useKeepAliveStore() // 1. 移除指定页面缓存 keepAliveStore.remove('UserManagement') // 2. 移除多个页面缓存 keepAliveStore.remove(['UserManagement', 'RoleManagement']) // 3. 清除所有页面缓存 keepAliveStore.clean() // 4. 临时隐藏缓存(用于页面刷新) keepAliveStore.hidden() // 恢复显示缓存 keepAliveStore.display() ``` #### 标签页缓存管理 标签页系统与页面缓存紧密结合,提供了完整的缓存生命周期管理: ```typescript const tabStore = useTabStore() // 刷新当前标签页(会清除并重新加载缓存) await tabStore.refreshTab() // 关闭标签页时自动清除对应的页面缓存 tabStore.closeTab(targetTab) // 关闭其他标签页(保留固定标签页和当前标签页的缓存) await tabStore.closeOtherTab(currentTab) ``` ## 数据缓存 (Web Storage) 除了页面缓存,MineAdmin 还提供了功能强大的数据缓存系统,用于缓存 API 数据、用户偏好设置等信息。 ### useCache Hook 系统提供了 `useCache` Hook 来统一管理浏览器存储: ```typescript import useCache from '@/hooks/useCache' // 使用 localStorage(默认) const localStorage = useCache('localStorage') // 使用 sessionStorage const sessionStorage = useCache('sessionStorage') ``` ### 基本用法 ```typescript const cache = useCache() // 存储数据 cache.set('userInfo', { id: 1, name: 'admin', roles: ['admin'] }) // 存储带过期时间的数据(单位:秒) cache.set('tempData', { value: 'temp' }, { exp: 3600 }) // 1小时后过期 // 获取数据 const userInfo = cache.get('userInfo') const tempData = cache.get('tempData', null) // 提供默认值 // 删除数据 cache.remove('tempData') // 删除所有过期数据 cache.removeAllExpires() // 更新数据的过期时间 cache.touch('userInfo', 7200) // 延长2小时 ``` ### 高级特性 #### 自动前缀 所有缓存键都会自动添加应用前缀,避免与其他应用冲突: ```typescript // 实际存储的键名会是:VITE_APP_STORAGE_PREFIX + 'userInfo' cache.set('userInfo', data) ``` #### 容量管理 当存储容量不足时,系统会自动清理过期数据: ```typescript cache.set('largeData', data, { exp: 3600, force: true // 当容量不足时,强制清理过期数据后再存储 }) ``` ### 在 HTTP 请求中的应用 系统在 HTTP 拦截器中使用缓存存储用户认证信息: ```typescript // src/utils/http.ts const cache = useCache() const userStore = useUserStore() // 存储认证令牌 cache.set('token', data.access_token) cache.set('expire', useDayjs().unix() + data.expire_at, { exp: data.expire_at }) cache.set('refresh_token', data.refresh_token) // 自动刷新令牌时读取缓存 if (!cache.get('refresh_token')) { await logout() } ``` ## 缓存最佳实践 ### 1. 合理使用页面缓存 * **适合缓存的页面**:列表页、表单页、查看详情页 * **不适合缓存的页面**:登录页、错误页、临时弹窗页面 * **注意事项**:确保组件名称唯一,避免缓存冲突 ### 2. 数据缓存策略 ```typescript // 缓存字典数据(长期有效) cache.set('dictData', dictList, { exp: 24 * 3600 }) // 24小时 // 缓存用户偏好设置(持久化) cache.set('userSettings', settings) // 无过期时间 // 缓存临时状态(短期有效) cache.set('searchForm', formData, { exp: 1800 }) // 30分钟 ``` ### 3. 缓存清理策略 ```typescript // 用户登出时清理敏感数据 function logout() { cache.remove('token') cache.remove('refresh_token') cache.remove('userInfo') // 清理页面缓存 keepAliveStore.clean() tabStore.clearTab() } // 定期清理过期数据 setInterval(() => { cache.removeAllExpires() }, 60 * 60 * 1000) // 每小时清理一次 ``` ### 4. 性能优化建议 * 避免缓存过大的数据对象 * 合理设置过期时间,避免内存泄漏 * 对于频繁更新的数据,考虑使用 sessionStorage * 监控缓存使用情况,及时清理无用缓存 ## 常见问题 ### 问题1:页面缓存未生效 **可能原因**: * 组件未定义 `name` 属性 * 路由 `meta.cache` 未设置为 `true` * 页面模板存在多个根节点 **解决方案**: ```vue ``` ### 问题2:缓存数据过期 ```typescript // 检查数据是否存在且未过期 const cachedData = cache.get('userData') if (!cachedData) { // 重新获取数据 const newData = await fetchUserData() cache.set('userData', newData, { exp: 3600 }) } ``` ### 问题3:缓存占用过多空间 ```typescript // 定期清理和监控缓存使用 function monitorCacheUsage() { try { const used = JSON.stringify(localStorage).length const quota = 5 * 1024 * 1024 // 5MB 限制 if (used > quota * 0.8) { console.warn('缓存使用率过高,建议清理') cache.removeAllExpires() } } catch (error) { console.error('缓存监控失败', error) } } ``` ## 源码参考 * [useCache Hook](https://github.com/mineadmin/mineadmin/blob/master/web/src/hooks/useCache.ts) - 数据缓存工具 * [useKeepAliveStore](https://github.com/mineadmin/mineadmin/blob/master/web/src/store/modules/useKeepAliveStore.ts) - 页面缓存状态管理 * [useTabStore](https://github.com/mineadmin/mineadmin/blob/master/web/src/store/modules/useTabStore.ts) - 标签页缓存管理 * [Router 配置](https://github.com/mineadmin/mineadmin/blob/master/web/src/router/index.ts) - 路由缓存逻辑 * [Layout 布局](https://github.com/mineadmin/mineadmin/blob/master/web/src/layouts/index.tsx) - Keep-Alive 实现 --- --- url: /v3/front/base/configure.md --- # 前端配置指南 MineAdmin 前端基于 Vite 构建,提供了灵活的环境变量配置系统,支持开发、测试、生产等多种环境的个性化配置。 ## 环境变量配置 ### 配置文件概述 项目默认提供了以下环境配置文件: * `.env.development` - 开发环境配置 * `.env.production` - 生产环境配置 您可以根据需要创建额外的环境配置文件,如: * `.env.test` - 测试环境 * `.env.staging` - 预发布环境 * `.env.local` - 本地开发专用(会被 git 忽略) ::: tip 提示 环境变量配置遵循 Vite 的约定,详细信息请参考 [Vite - 环境变量和模式](https://cn.vitejs.dev/guide/env-and-mode.html) ::: ### 开发环境配置 (.env.development) 开发环境配置主要用于本地开发调试,包含了调试工具和代理设置。 ::: code-group ```env [.env.development] # ================================ # 基础应用配置 # ================================ # 页面标题 - 显示在浏览器标签页和页面标题中 VITE_APP_TITLE = MineAdmin # 开发服务器端口 VITE_APP_PORT = 2888 # 应用根路径 - 部署到子目录时需要修改 VITE_APP_ROOT_BASE = / # ================================ # API 接口配置 # ================================ # 后端 API 地址 - 开发环境通常指向本地后端服务 VITE_APP_API_BASEURL = http://127.0.0.1:9501 # ================================ # 路由配置 # ================================ # 路由模式:hash | history # hash: 带 # 号的路由模式,兼容性好 # history: HTML5 History API,需要服务器支持 VITE_APP_ROUTE_MODE = hash # ================================ # 存储配置 # ================================ # 本地存储前缀 - 避免多个项目间的存储冲突 VITE_APP_STORAGE_PREFIX = mine_ # ================================ # 代理配置 # ================================ # 是否开启开发代理 - 解决开发环境跨域问题 VITE_OPEN_PROXY = true # 代理前缀 - 用于标识需要代理的请求 VITE_PROXY_PREFIX = /dev # ================================ # 调试工具 # ================================ # 是否开启 vConsole - 移动端调试工具 VITE_OPEN_vCONSOLE = false # 是否开启 Vue DevTools - Vue 开发者工具 VITE_OPEN_DEVTOOLS = false ``` ::: ### 生产环境配置 (.env.production) 生产环境配置注重性能和安全性,移除了调试功能并优化了构建选项。 ::: code-group ```env [.env.production] # ================================ # 基础应用配置 # ================================ # 页面标题 VITE_APP_TITLE = MineAdmin # 应用根路径 - 根据实际部署路径调整 VITE_APP_ROOT_BASE = / # ================================ # API 接口配置 # ================================ # 生产环境 API 地址 - 通常使用相对路径或完整域名 VITE_APP_API_BASEURL = / # ================================ # 路由配置 # ================================ # 生产环境路由模式 VITE_APP_ROUTE_MODE = hash # ================================ # 存储配置 # ================================ # 存储前缀 VITE_APP_STORAGE_PREFIX = mine_ # ================================ # 代理配置(生产环境一般不需要) # ================================ VITE_OPEN_PROXY = false VITE_PROXY_PREFIX = /prod # ================================ # 构建配置 # ================================ # 是否在打包时启用 Mock - 生产环境建议关闭 VITE_BUILD_MOCK = false # 是否生成 source map - 影响构建大小和调试能力 VITE_BUILD_SOURCEMAP = false # 构建压缩方式 - 支持 gzip, brotli VITE_BUILD_COMPRESS = gzip,brotli # 构建后是否生成压缩包 - 支持 zip, tar VITE_BUILD_ARCHIVE = ``` ::: ## 配置项详细说明 ### 基础配置项 | 配置项 | 类型 | 默认值 | 说明 | |--------|------|--------|------| | `VITE_APP_TITLE` | string | MineAdmin | 应用标题,显示在浏览器标签页 | | `VITE_APP_PORT` | number | 2888 | 开发服务器端口(仅开发环境) | | `VITE_APP_ROOT_BASE` | string | / | 应用部署的基础路径 | ### API 接口配置 | 配置项 | 类型 | 说明 | 示例 | |--------|------|------|------| | `VITE_APP_API_BASEURL` | string | 后端 API 基础地址 | `http://api.example.com` | ::: warning 注意 生产环境的 API 地址配置需要特别注意: * 如果前后端部署在同一域名下,使用相对路径 `/` * 如果跨域部署,需要配置完整的 API 地址 * 确保 API 服务器已正确配置 CORS ::: ### 路由配置 | 配置项 | 可选值 | 说明 | |--------|--------|------| | `VITE_APP_ROUTE_MODE` | `hash` | `history` | 路由模式选择 | **路由模式对比:** | 模式 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | `hash` | 兼容性好,无需服务器配置 | URL 带 # 号,SEO 不友好 | 传统部署环境 | | `history` | URL 简洁,SEO 友好 | 需要服务器支持,配置复杂 | 现代部署环境 | ### 存储配置 | 配置项 | 说明 | 建议值 | |--------|------|--------| | `VITE_APP_STORAGE_PREFIX` | 本地存储前缀 | 项目唯一标识 | ### 代理配置 | 配置项 | 类型 | 说明 | |--------|------|------| | `VITE_OPEN_PROXY` | boolean | 是否启用开发代理 | | `VITE_PROXY_PREFIX` | string | 代理请求前缀标识 | ### 构建配置 | 配置项 | 类型 | 说明 | |--------|------|------| | `VITE_BUILD_MOCK` | boolean | 是否在构建时包含 Mock 功能 | | `VITE_BUILD_SOURCEMAP` | boolean | 是否生成 source map | | `VITE_BUILD_COMPRESS` | string | 压缩算法,多个用逗号分隔 | | `VITE_BUILD_ARCHIVE` | string | 构建后生成的压缩包格式 | ## 部署场景配置 ### 场景 1:子目录部署 如果需要将应用部署到服务器的子目录(如 `https://example.com/admin/`): ```env # 设置基础路径 VITE_APP_ROOT_BASE = /admin/ # API 地址相应调整 VITE_APP_API_BASEURL = /admin/api/ ``` ### 场景 2:CDN 部署 使用 CDN 加速静态资源: ```env # 基础路径设置为 CDN 地址 VITE_APP_ROOT_BASE = https://cdn.example.com/admin/ # API 地址保持原域名 VITE_APP_API_BASEURL = https://api.example.com/ ``` ### 场景 3:Docker 部署 Docker 容器化部署配置: ```env # 使用环境变量占位符 VITE_APP_API_BASEURL = ${API_BASE_URL} VITE_APP_TITLE = ${APP_TITLE:-MineAdmin} ``` ### 场景 4:前后端分离部署 前后端完全分离的部署架构: ```env # 前端独立域名 VITE_APP_ROOT_BASE = / # 后端 API 完整地址 VITE_APP_API_BASEURL = https://api.example.com/v1/ # 使用 history 路由模式(需要 Nginx 配置支持) VITE_APP_ROUTE_MODE = history ``` ## 最佳实践 ### 1. 环境变量命名规范 * 所有环境变量必须以 `VITE_` 开头才能被客户端访问 * 使用全大写字母和下划线命名 * 按功能模块分组命名 ### 2. 安全考虑 ::: danger 安全提醒 * 不要在环境变量中存储敏感信息(如密钥、密码) * 生产环境配置文件不应包含开发调试信息 * 定期检查和清理不再使用的配置项 ::: ### 3. 性能优化 ```env # 生产环境建议配置 VITE_BUILD_SOURCEMAP = false # 减少包体积 VITE_BUILD_COMPRESS = gzip,brotli # 启用压缩 VITE_OPEN_vCONSOLE = false # 关闭调试工具 VITE_OPEN_DEVTOOLS = false # 关闭开发工具 ``` ### 4. 多环境管理 创建 `.env.staging` 用于预发布环境: ```env # 预发布环境配置 VITE_APP_TITLE = MineAdmin (Staging) VITE_APP_API_BASEURL = https://staging-api.example.com/ VITE_BUILD_SOURCEMAP = true # 保留 source map 用于调试 ``` ## 常见问题 ### Q: 修改环境变量后不生效? **A:** 请确保: 1. 重启开发服务器 2. 环境变量名称以 `VITE_` 开头 3. 语法格式正确(无多余空格) ### Q: 生产环境 API 请求失败? **A:** 检查以下配置: 1. `VITE_APP_API_BASEURL` 是否正确 2. 后端服务是否配置了正确的 CORS 3. 网络防火墙是否允许对应端口访问 ### Q: 如何在代码中获取环境变量? **A:** 使用 `import.meta.env` 访问: ```typescript // 获取 API 基础地址 const apiBaseUrl = import.meta.env.VITE_APP_API_BASEURL // 获取应用标题 const appTitle = import.meta.env.VITE_APP_TITLE // 检查是否为开发环境 const isDev = import.meta.env.DEV ``` ### Q: history 路由模式下页面刷新 404? **A:** 需要配置服务器重写规则,以 Nginx 为例: ```nginx location / { try_files $uri $uri/ /index.html; } ``` ::: tip 提示 更多部署相关问题,请参考 [部署指南](../../guide/start/deployment.md) 章节。 ::: --- --- url: /v3/guide/upgrade.md --- # 升级指南 ::: warning 自 3.0 开始,不管是目录结构还是数据表或自带的特性功能都与历史版本差异较大, 因此 2.0 => 3.0 的升级文档将不再另行说明 ::: --- --- url: /v3/guide/releases.md --- # 发行说明 ## 版本控制方案 MineAdmin 自 `>=3.0` 采用 [语义化版本控制](https://semver.org/)来施行版本控制方案 主要版本发放为每年一度,而较小的的补丁则会`每周 or 每月`一度发放 ## 命名参数 [命名参数](https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments) 目前暂未被全面采用。 我们可能会在必要时进行 `类名` `类函数名` `函数名` 进行重命名。以便改进 MineAdmin 代码 目前官方采用的命名规范以以下描述为准 1. 所有类采用`大驼峰`命名方式 2. 所有类方法采用`小驼峰`命名方式 3. 函数采用`蛇形`命名方式 ## 支持计划 对于所有的 MineAdmin 版本,`错误修复` 提供18个月服务支持,`安全修复`提供两年的服务支持 因历史原因 2.0 提供安全维护服务将随着 php8.3 生命周期结束停止维护 | 版本 | PHP(*) | Hyperf(*) | 发行时间 | Bug修复截止时间 | 安全修复截止时间 | |-----|---------|-----------|---------|-----------|----------| | 0.4 | 8.0 | 2.2 | 2021-01 | 2024-01 | 2024-01 | | 1.4 | 8.0 | 2.2 | 2022-07 | 2024-07 | 2024-07 | | 2.0 | 8.1~8.3 | 3.1 | 2023-12 | 2027-12 | 2027-12 | | 3.0 | 8.1~8.3 | >=3.1 | 2024-10 | 2026-04 | 2026-10 | --- --- url: /v3/backend/contracts/routing.md --- # 后台路由契约 后台路由契约定义管理端资源的访问路径、HTTP 方法、权限边界和操作语义。不同框架的路由注册方式可以不同,但对外暴露的后台接口应保持一致。 本文依据 MineAdmin 当前管理端控制器整理,描述前台模板、权限系统、接口文档和审计日志共同依赖的稳定约定。 ## 基本原则 * 同一资源在不同实现中使用一致的路径语义。 * 列表、详情、创建、更新、删除等常见操作保持稳定的 HTTP 方法和参数位置。 * 需要登录的后台接口必须经过统一认证流程。 * 需要授权的后台接口必须绑定稳定的权限标识。 * 路由元数据应能支撑接口文档、前台能力识别和操作日志。 * 批量操作、导入导出、状态切换等扩展操作应有清晰的资源归属。 ## URL 命名空间 后台接口统一使用 `/admin` 作为管理端命名空间,命名空间后的第一段通常表示业务域或资源。 | 类型 | 路径形态 | 示例 | |------|----------|------| | 认证接口 | `/admin/passport/{action}` | `/admin/passport/login`、`/admin/passport/refresh` | | 当前用户能力 | `/admin/permission/{action}` | `/admin/permission/menus`、`/admin/permission/roles` | | 资源管理 | `/admin/{resource}` | `/admin/user`、`/admin/role`、`/admin/department` | | 资源列表 | `/admin/{resource}/list` | `/admin/user/list`、`/admin/attachment/list` | | 资源子能力 | `/admin/{resource}/{id}/{relation}` | `/admin/role/{id}/permissions` | | 插件后台接口 | `/admin/plugin/{plugin}/{action}` | `/admin/plugin/store/index` | 框架实现可以使用注解、路由表、控制器前缀或文件路由注册这些地址,但最终公开 URL 不应因为框架差异而变化。 ## 资源操作 | 操作 | HTTP 方法与路径 | 参数位置 | 说明 | |------|-----------------|----------|------| | 列表 | `GET /admin/{resource}/list` | 查询参数或请求参数 | 支持分页、筛选、排序和数据权限过滤。 | | 创建 | `POST /admin/{resource}` | JSON 请求体 | 请求体承载表单数据。 | | 更新 | `PUT /admin/{resource}/{id}` | 路径参数 + JSON 请求体 | 路径参数定位资源,请求体承载可变更字段。 | | 删除 | `DELETE /admin/{resource}` | 请求参数或请求体 | 当前多数管理资源通过请求数据传入单个或多个 ID。 | | 单条删除 | `DELETE /admin/{resource}/{id}` | 路径参数 | 适用于已经明确用路径参数定位的资源,例如附件。 | | 关系读取 | `GET /admin/{resource}/{id}/{relation}` | 路径参数 | 查询用户角色、角色权限等资源关系。 | | 关系写入 | `PUT /admin/{resource}/{id}/{relation}` | 路径参数 + JSON 请求体 | 批量授予角色、权限或数据范围。 | | 资源动作 | `POST/PUT /admin/{resource}/{action}` | 依动作而定 | 上传、重置密码、状态切换等非标准 CRUD 动作。 | ## 认证路由 认证路由属于后台契约的一部分,但不按普通资源权限处理。 | 路径 | 方法 | 认证要求 | 说明 | |------|------|----------|------| | `/admin/passport/login` | `POST` | 不需要访问令牌 | 使用用户名和密码换取访问令牌与刷新令牌。 | | `/admin/passport/logout` | `POST` | 需要访问令牌 | 退出当前登录状态。 | | `/admin/passport/getInfo` | `GET` | 需要访问令牌 | 返回当前登录用户的基础信息和后台设置。 | | `/admin/passport/refresh` | `POST` | 需要刷新令牌 | 换取新的访问令牌。 | ## 当前用户能力路由 当前用户能力路由用于前台初始化,不等同于普通资源管理接口。 | 路径 | 方法 | 说明 | |------|------|------| | `/admin/permission/menus` | `GET` | 返回当前用户可访问的菜单树。 | | `/admin/permission/roles` | `GET` | 返回当前用户拥有的角色。 | | `/admin/permission/update` | `POST` | 修改当前用户资料或密码。 | 这些接口必须保持响应字段稳定,因为前台模板会用它们完成路由生成、菜单渲染和用户信息展示。 ## 参数约定 * 分页参数固定为 `page` 和 `page_size`,默认值分别为 `1` 和 `10`。 * 列表筛选字段随资源定义,但应通过请求参数统一传入,并由资源仓储或服务解释。 * 创建和更新接口使用 JSON 请求体,字段以对应表单请求或接口元数据为准。 * 文件上传使用 `multipart/form-data`,附件上传字段名固定为 `file`。 * 批量删除接口通常使用 `ids` 或资源 ID 数组;如果接口已经声明路径参数,应以路径参数定位单个资源。 * 用户授权角色使用 `role_codes` 数组,角色授权权限使用 `permissions` 数组,数组元素分别对应角色代码和菜单权限标识。 * 岗位数据权限使用 `policy_type` 描述策略类型,`value` 描述策略值。 ## 权限标识 权限标识应与后台菜单、按钮和接口元数据保持一致。前台模板根据权限标识控制按钮和页面能力,后端实现根据同一标识执行授权校验。 权限标识推荐使用 `业务域:资源:动作` 的形式,例如: | 场景 | 权限标识示例 | |------|--------------| | 用户列表 | `permission:user:index` | | 创建用户 | `permission:user:save` | | 删除用户 | `permission:user:delete` | | 授权用户角色 | `permission:user:setRole` | | 角色授权权限 | `permission:role:setMenu` | | 附件上传 | `dataCenter:attachment:upload` | | 登录日志列表 | `log:userLogin:list` | 后端授权校验应以该标识为准,前台菜单、按钮权限和接口元数据也应复用同一标识,避免同一能力出现多个命名。 ## 路由元数据 每个后台接口应提供稳定的路由元数据,至少覆盖以下语义: | 元数据 | 作用 | |--------|------| | `operationId` | 接口唯一操作名,用于接口文档、客户端生成和变更识别。 | | `summary` | 接口业务名称,用于接口文档和操作日志。 | | `tags` | 接口分组,应与后台功能模块或资源归属一致。 | | `security` | 声明接口需要访问令牌、刷新令牌或无需认证。 | | 请求体 schema | 描述创建、更新、授权、上传等接口的输入字段。 | | 响应 schema | 描述列表、详情、当前用户能力等接口的返回结构。 | 如果某个框架没有原生注解能力,也需要在路由注册、OpenAPI 生成或等效配置中保存这些语义。 ## 审计约定 需要记录操作日志的后台接口,应保留可识别的 HTTP 方法、真实路径和业务名称。操作日志至少需要能够追踪: * 操作用户。 * 请求方法。 * 请求路径。 * 业务名称。 * 客户端 IP。 因此,涉及新增、修改、删除、授权、上传等会改变系统状态的接口,应确保路由元数据中的业务名称明确,不使用空泛描述。 ## 插件路由 插件可以注册自己的后台路由前缀,但仍应遵守管理端路由契约: * 路径归入 `/admin/plugin/{plugin}` 或插件明确声明的后台命名空间。 * 需要登录的插件接口必须使用统一认证流程。 * 会改变系统状态的插件接口应使用合适的 HTTP 方法,并提供可审计的业务名称。 * 插件接口返回结构应保持与后台统一响应契约一致。 ## 框架实现 Hyperf 当前通过注解、控制器和中间件承载路由与 API 文档生成,具体用法见 [Hyperf 路由与 API 文档](/backend/frameworks/hyperf/3.2/base/router)。 --- --- url: /v3/backend.md --- # 后端文档 欢迎来到 MineAdmin 3.x 后端开发文档。这里主要描述 MineAdmin v3 的公共契约:前台模板、后台路由、接口元数据、响应结构和数据模型等稳定约定。具体框架实现已经独立到 [后端框架实现](/backend/frameworks/)。 ## 阅读路径 如果你只关心业务开发,建议先阅读公共契约,再进入当前项目使用的框架实现: 1. [公共契约](/v3/backend/contracts/):了解所有后端实现必须保持一致的接口、模型和响应规范。 2. [Hyperf latest](/backend/frameworks/hyperf/):当前指向 Hyperf 3.2,包含生命周期、中间件、异常处理、日志、事件、上传、多语言、认证授权和数据权限等细节。 3. [Laravel 1.0](/backend/frameworks/laravel/1.0/):规划中实现入口,后续 Laravel 版本将复用同一套公共契约。 ## 架构原则 MineAdmin 的后端实现可以由不同框架承载,但面向前台模板和外部集成时需要保持一致: * 数据模型语义一致:用户、角色、菜单、部门、岗位、附件等核心模型保持同一业务含义。 * 后台路由一致:同类后台资源使用一致的路由语义、权限标识和操作边界。 * 接口元数据一致:OpenAPI/Swagger 元数据需要描述同一套请求、响应和认证要求。 * 响应结构一致:接口统一返回业务状态码、消息和数据载荷,避免前台因框架差异分支处理。 * 前台模板一致:同一套前台模板可以通过同一套接口契约对接不同后端实现。 ## 当前实现状态 | 实现 | 版本 | 语言 | 状态 | 说明 | |------|------|------|------|------| | Hyperf | 3.2 | PHP | latest / 稳定实现 | MineAdmin 3.x 当前默认后端实现,运行在 Swoole/Swow 协程环境中。 | | Hyperf | 3.1 | PHP | 稳定实现 | 与 3.2 初始文档结构一致,后续按版本差异维护。 | | Laravel | 1.0 | PHP | 规划中 | 第一阶段只保留入口,后续按公共契约补齐实现文档。 | ## 核心专题 * [Hyperf 用户认证](/backend/frameworks/hyperf/3.2/security/passport):双 Token、JWT 与登录流程。 * [Hyperf 用户授权(RBAC)](/backend/frameworks/hyperf/3.2/security/access):角色、菜单、权限校验与审计。 * [Hyperf 数据权限](/backend/frameworks/hyperf/3.2/data-permission/overview):部门、岗位、策略和数据过滤规则。 * [插件开发](/v3/plugin/index):通过插件机制扩展系统功能。 ## 参考资料 * [公共契约总览](/v3/backend/contracts/) * [后端框架实现](/backend/frameworks/) * [Hyperf 官方文档](https://hyperf.wiki/3.1) * [Laravel 官方文档](https://laravel.com/docs/11.x/) --- --- url: /backend/frameworks.md --- # 后端框架实现 这里收录 MineAdmin 后端框架实现文档。框架实现不跟随 MineAdmin 主产品的 `v3`、`v4` 大版本节奏,而是按照各框架自己的版本独立维护。 ## 版本关系 | 框架 | 实现版本 | 状态 | 适配契约 | 入口 | |------|----------|------|----------|------| | Hyperf | 3.2 | latest / 稳定实现 | MineAdmin v3 | [Hyperf latest](/backend/frameworks/hyperf/) | | Hyperf | 3.1 | 稳定实现 | MineAdmin v3 | [Hyperf 3.1](/backend/frameworks/hyperf/3.1/) | | Laravel | 1.0 | 规划中 | MineAdmin v3 | [Laravel 1.0](/backend/frameworks/laravel/1.0/) | ## 阅读方式 如果你要了解跨框架必须一致的接口、响应、路由和前台对接约定,请先阅读 [MineAdmin v3 后端公共契约](/v3/backend/contracts/)。 如果你要查看具体框架的生命周期、容器、中间件、异常、日志、事件、队列、上传或数据权限实现,请进入对应框架版本文档。 --- --- url: /v3/backend/contracts/response.md --- # 响应结构契约 响应结构契约定义 MineAdmin API 的统一返回格式。不同框架实现可以使用不同的响应对象或异常处理器,但对外返回的数据结构需要保持一致。 ## 基础结构 标准响应包含三个核心字段: ```json { "code": 200, "message": "操作成功", "data": {} } ``` | 字段 | 说明 | |------|------| | `code` | 业务状态码,用于表达业务处理结果。 | | `message` | 面向用户或开发者的结果说明。 | | `data` | 响应数据,可以是对象、数组、分页结构或空对象。 | HTTP 状态码用于表达传输层结果,`code` 用于表达业务层结果。前台模板应优先根据业务状态码和约定的异常处理流程展示提示。 ## 分页结构 分页接口应在 `data` 中返回列表和分页信息,字段命名应在同一产品版本内保持稳定。不同框架实现可以在内部使用不同分页器,但不能改变前台依赖的最终结构。 ## 错误结构 业务错误、验证错误、未认证、未授权和系统错误都应进入统一响应结构。框架实现需要把自身异常转换为稳定的业务响应,避免直接暴露底层框架异常格式。 ## 多语言消息 业务消息可以由后端实现根据客户端语言生成,但消息键、业务状态码和错误语义应保持一致。框架具体多语言加载方式见对应实现文档。 ## 框架实现 Hyperf 当前通过 `Result`、`ResultCode` 和异常处理器统一响应结构,具体用法见 [Hyperf 错误处理](/backend/frameworks/hyperf/3.2/base/error-handler)。 --- --- url: /v3/front/base/icon.md --- # 图标系统 MineAdmin 采用现代化的图标解决方案,基于 Iconify 图标框架和 UnoCSS 提供强大的图标支持。系统支持在线图标库、离线模式和自定义图标等多种方案。 ## 图标架构概览 ```plantuml @startuml !theme plain package "图标系统架构" { [Iconify框架] as iconify [UnoCSS引擎] as unocss [MaSvgIcon组件] as component [离线图标缓存] as cache [自定义SVG图标] as custom iconify --> unocss : 图标解析 unocss --> component : CSS类生成 iconify --> cache : 本地缓存 custom --> component : 本地图标 component --> [浏览器渲染] } @enduml ``` ## 图标解决方案对比 | 解决方案 | 优势 | 适用场景 | 性能 | 维护成本 | |---------|------|---------|------|---------| | **Iconify在线** | 图标丰富(200k+)、即用即加载 | 快速开发、原型设计 | ⭐⭐⭐ | 低 | | **Iconify离线** | 无网络依赖、加载速度快 | 生产环境、内网部署 | ⭐⭐⭐⭐⭐ | 中 | | **自定义SVG** | 完全可控、品牌定制 | 企业级应用、品牌统一 | ⭐⭐⭐⭐ | 高 | ## Iconify 图标使用 ::: tip Iconify 优势 `Iconify` 是目前最全面的图标框架,包含: * **150+ 图标集合**:FontAwesome、Material Design、Ant Design、Tabler Icons等 * **200,000+ 图标**:涵盖各行各业的设计需求 * **统一API**:一套语法适配所有图标集 * **按需加载**:只加载使用的图标,减少包体积 ::: ### 基础图标使用 ### 图标搜索和选择 推荐使用 [Icônes](https://icones.js.org/) 搜索图标,这是基于 Iconify 的专业图标搜索工具: ![Icônes 界面展示](https://s21.ax1x.com/2024/10/14/pAt0w8S.jpg) **搜索技巧:** 1. **按分类浏览**:选择 Material Design、FontAwesome 等知名图标集 2. **关键词搜索**:支持中英文搜索,如 "用户"、"user"、"profile" 3. **标签筛选**:通过 solid、outline、filled 等标签精确筛选 4. **尺寸预览**:实时预览不同尺寸下的图标效果 ::: info 图标命名规范 复制得到的图标格式为:`i-{集合名}:{图标名}` * 例如:`i-material-symbols:person` * 例如:`i-heroicons:user-solid` ::: ### MaSvgIcon 组件使用 `MaSvgIcon` 是系统内置的图标组件,提供统一的图标渲染接口: ```vue ``` **组件属性说明:** | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `name` | string | - | 图标名称(必填) | | `size` | string|number | '16' | 图标尺寸(px) | | `color` | string | 'currentColor' | 图标颜色 | | `class` | string | - | 自定义CSS类 | ### CSS 类直接使用 对于简单场景,可以直接使用 CSS 类名: ```html ``` ::: warning 使用限制 CSS 类方式有以下限制: * **不支持异步加载**:图标名称必须在构建时确定 * **不支持动态拼接**:`class="i-${iconName}"` 这种写法无效 * **推荐静态使用**:适用于布局固定的场景 ::: ### 在路由菜单中使用 在路由配置中使用图标,支持多种图标来源: ```typescript // 路由配置示例 export const routes = [ { name: 'dashboard', path: '/dashboard', meta: { title: '仪表盘', icon: 'i-material-symbols:dashboard', // Iconify图标 } }, { name: 'users', path: '/users', meta: { title: '用户管理', icon: 'i-heroicons:users', // 另一个图标集 } }, { name: 'settings', path: '/settings', meta: { title: '系统设置', icon: 'custom-gear', // 自定义SVG图标 } } ] ``` ### 离线模式配置 对于生产环境或内网部署,建议使用离线模式以提升性能和稳定性: ```plantuml @startuml !theme plain start :开发阶段; :收集使用的图标; note right 统计项目中实际使用的 所有 Iconify 图标 end note :运行生成命令; note right: pnpm run gen:icons :选择图标集; :选择离线模式; :生成本地图标库; :构建应用; note right 图标直接从本地加载 无需网络请求 end note stop @enduml ``` **离线模式设置步骤:** 1. **收集图标使用情况** ```bash # 扫描项目中使用的图标 grep -r "i-[a-zA-Z-]*:" src/ --include="*.vue" --include="*.ts" ``` 2. **生成离线图标库** ```bash # 运行图标生成命令 pnpm run gen:icons ``` 3. **按提示选择配置** * 选择需要的图标集(如 Material Symbols、Heroicons) * 选择使用模式为 "离线模式" * 确认生成配置 ::: tip 性能优化建议 * **按需选择**:只选择项目实际使用的图标集 * **定期更新**:当添加新图标时记得重新生成 * **版本控制**:将生成的图标文件纳入版本管理 ::: ## 自定义 SVG 图标 对于企业特定的图标需求,可以使用自定义 SVG 图标: ### 图标文件管理 ``` src/assets/icons/ ├── brand/ # 品牌相关图标 │ ├── logo.svg │ └── logo-mini.svg ├── business/ # 业务专用图标 │ ├── order.svg │ └── product.svg └── common/ # 通用图标 ├── export.svg └── import.svg ``` ### 使用自定义图标 ```vue ``` ### SVG 图标规范 为确保图标在系统中正常显示,请遵循以下规范: ```xml ``` **规范要点:** * **统一尺寸**:建议使用 24x24 的 viewBox * **可变颜色**:使用 `currentColor` 支持动态颜色 * **简化路径**:移除不必要的属性和注释 * **语义化命名**:文件名要清晰表达图标含义 ## 图标在组件中的应用 ### 表格操作按钮 ```vue ``` ### 表单组件图标 ```vue ``` ### 状态指示器 ```vue ``` ## 实践指南 ### 图标选择原则 1. **一致性原则** ```vue ``` 2. **语义化原则** ```vue 保存 保存 ``` ### 性能优化策略 ```typescript // 图标预加载配置 const criticalIcons = [ 'i-heroicons:home', 'i-heroicons:user', 'i-heroicons:cog-6-tooth', 'i-heroicons:bell' ] // 在应用启动时预加载关键图标 criticalIcons.forEach(icon => { // 触发图标加载 document.createElement('i').className = icon }) ``` ### 无障碍访问支持 ```vue ``` ## 常见问题排查 ### 图标不显示 **问题现象:** * 图标位置显示空白 * 控制台出现 404 错误 **排查步骤:** 1. **检查图标名称** ```vue ``` 2. **验证网络连接** ```javascript // 在浏览器控制台检查 fetch('https://api.iconify.design/heroicons.json') .then(r => r.json()) .then(data => console.log('图标集数据:', data)) ``` 3. **检查离线配置** ```bash # 确认离线图标是否包含所需图标 ls dist/assets/icons/ # 检查生成的图标文件 ``` ### 图标加载缓慢 **优化方案:** ```typescript // 1. 启用图标预加载 const iconPreloader = { preload: ['i-heroicons:user', 'i-heroicons:home'], init() { this.preload.forEach(icon => { const link = document.createElement('link') link.rel = 'preload' link.href = `https://api.iconify.design/${icon.replace('i-', '').replace(':', '/')}.svg` link.as = 'image' document.head.appendChild(link) }) } } // 2. 使用离线模式 // 运行 pnpm run gen:icons 生成本地图标库 ``` ### 图标样式问题 ```vue ``` ## 最佳实践总结 ### 开发阶段 * ✅ 使用 [Icônes](https://icones.js.org/) 搜索和预览图标 * ✅ 选择一致的图标集合(推荐 Heroicons 或 Material Symbols) * ✅ 为图标添加语义化的名称和注释 * ✅ 建立项目图标使用规范文档 ### 生产部署 * ✅ 生成离线图标库提升加载性能 * ✅ 启用图标预加载优化首屏体验 * ✅ 配置 CDN 加速图标资源加载 * ✅ 监控图标加载性能和错误率 ### 维护阶段 * ✅ 定期清理未使用的图标引用 * ✅ 跟踪图标集版本更新 * ✅ 建立图标变更的代码审查机制 * ✅ 维护自定义图标的设计规范 --- --- url: /v3/front/base/concept.md --- # 基础概念 整个项目进行了重构,现在我们将会介绍一些基础概念,以便于你更好的理解整个文档,请务必仔细先阅读这一部分。 ::: tip 以下所讲全部针对源码根目录下的 `./web` 里的结构 ::: ## 项目整体架构 本项目采用现代化的前端开发架构,基于 Vue 3 + TypeScript + Vite 构建,实现了模块化、插件化的开发模式。 ```plantuml @startmindmap * 项目根目录 ./web ** src 源码目录 *** modules 模块系统 **** base 基础模块 ***** api 接口 ***** views 视图 ***** locales 国际化 **** custom 自定义模块... *** plugins 插件系统 **** 独立应用 **** 功能插件 *** components 全局组件 *** utils 工具函数 ** types 全局类型 ** vite.config.ts 配置 @endmindmap ``` ## 全局类型系统 由于新版采用 `TypeScript` 所写,全局的类型定义都在 `./types` 目录下存放着,可在里面找到相关的数据类型结构。 ### 类型文件组织结构 ``` ./types/ ├── api.d.ts # API 相关类型定义 ├── components.d.ts # 组件类型定义 ├── global.d.ts # 全局类型定义 ├── modules.d.ts # 模块类型定义 └── utils.d.ts # 工具函数类型定义 ``` ### 使用示例 在项目中可以通过别名 `#` 快速引入类型: ```typescript // 引入 API 类型 import type { ApiResponse, UserInfo } from '#/api' // 引入全局类型 import type { MenuConfig, RouteConfig } from '#/global' // 在组件中使用 interface ComponentProps { userInfo: UserInfo menuConfig: MenuConfig[] } ``` ### 类型定义最佳实践 * **命名规范**:使用 PascalCase 命名接口和类型 * **文件组织**:按功能模块划分类型文件 * **类型导出**:使用 `export type` 导出类型定义 * **泛型支持**:合理使用泛型提高类型复用性 ## 模块化架构 新版本进行模块化划分,目录为 `./src/modules`。每个模块管理着所属业务的 `api`、`types`、`locales` 以及 `视图文件`,实现业务的完全隔离和独立管理。 ### 模块结构设计 ```plantuml @startmindmap * modules 模块根目录 ** base 基础模块 *** api/ 接口层 **** user.ts **** menu.ts *** views/ 视图层 **** dashboard/ **** login/ *** locales/ 国际化 **** zh_CN.yaml **** en.yaml *** components/ 模块组件 ** user 用户模块 ** order 订单模块 ** ... 其他业务模块 @endmindmap ``` ### 标准模块目录结构 ``` ./src/modules/[模块名]/ ├── api/ # API 接口定义 │ ├── user.ts # 用户相关接口 │ ├── menu.ts # 菜单相关接口 │ └── index.ts # 接口统一导出 ├── components/ # 模块专用组件 │ ├── UserForm.vue # 用户表单组件 │ └── MenuTree.vue # 菜单树组件 ├── locales/ # 模块国际化文件 │ ├── zh_CN.yaml # 中文语言包 │ ├── en.yaml # 英文语言包 │ └── index.ts # 语言包导出 ├── views/ # 视图页面 │ ├── user/ # 用户管理页面 │ │ ├── index.vue # 用户列表页 │ │ └── detail.vue # 用户详情页 │ └── dashboard/ # 仪表板页面 │ └── index.vue └── index.ts # 模块统一导出 ``` ### 模块开发流程 1. **创建模块目录**:在 `./src/modules/` 下创建新的模块文件夹 2. **定义模块结构**:按照标准结构创建相应目录和文件 3. **配置路由**:在模块中定义路由配置 4. **开发业务逻辑**:编写 API、组件和视图 5. **添加国际化**:配置多语言支持 6. **模块导出**:通过 index.ts 统一导出模块内容 ### 模块间通信 ```plantuml @startuml participant "模块 A" as A participant "全局 Store" as S participant "模块 B" as B participant "Event Bus" as E A -> S: 更新全局状态 S -> B: 状态变更通知 A -> E: 发送事件 E -> B: 事件监听响应 A -> B: 直接调用公共 API @enduml ``` ### 模块使用示例 ```typescript // 在其他模块中使用 base 模块的 API import { userApi, menuApi } from '~/base/api' import type { UserInfo } from '~/base/types' // 在组件中使用模块功能 export default defineComponent({ async setup() { // 调用用户 API const userList = await userApi.getUsers() // 调用菜单 API const menuTree = await menuApi.getMenuTree() return { userList, menuTree } } }) ``` ## 别名系统 在 `vite.config.ts` 文件中定义了路径别名系统,简化文件引入路径,提高开发效率和代码可维护性。 ### 别名配置 ```typescript // vite.config.ts export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, 'src'), '#': path.resolve(__dirname, 'types'), '$': path.resolve(__dirname, 'src/plugins'), '~': path.resolve(__dirname, 'src/modules'), }, }, }) ``` ### 别名映射表 | 别名 | 目录路径 | 用途描述 | 使用场景 | |------|----------|----------|----------| | `@` | `./src` | 源码根目录 | 引入组件、工具函数、样式等 | | `#` | `./types` | 全局类型定义 | 引入 TypeScript 类型定义 | | `$` | `./src/plugins` | 插件目录 | 引入插件内的文件和组件 | | `~` | `./src/modules` | 模块目录 | 引入模块内的 API、组件、视图 | ### 别名使用示例 #### 1. 基础路径别名 (@) ```typescript // ❌ 使用相对路径(不推荐) import Utils from '../../../utils/common' import Button from '../../../components/Button.vue' // ✅ 使用别名(推荐) import Utils from '@/utils/common' import Button from '@/components/Button.vue' ``` #### 2. 类型定义别名 (#) ```typescript // 引入全局类型 import type { ApiResponse, UserInfo, MenuConfig } from '#/global' // 引入 API 类型 import type { LoginParams } from '#/api' // 在接口中使用 interface ComponentProps { userInfo: UserInfo menuList: MenuConfig[] } ``` #### 3. 插件别名 ($) ```typescript // 引入图表插件 import ChartPlugin from '$/charts' import { useChart } from '$/charts/hooks' // 引入编辑器插件 import EditorPlugin from '$/editor' import EditorComponent from '$/editor/components/RichEditor.vue' ``` #### 4. 模块别名 (~) ```typescript // 引入 base 模块的 API import { userApi, menuApi } from '~/base/api' // 引入用户模块的组件 import UserForm from '~/user/components/UserForm.vue' import UserList from '~/user/views/UserList.vue' // 引入模块的类型 import type { UserModuleState } from '~/user/types' ``` ### 别名系统架构图 ```plantuml @startmindmap * 项目根目录 ** @: ./src *** components/ *** utils/ *** $: plugins/ **** charts/ **** editor/ **** map/ *** ~: modules/ **** base/ **** user/ **** order/ ** #: ./types *** global.d.ts *** api.d.ts *** components.d.ts @endmindmap ``` ### 别名配置最佳实践 #### 1. IDE 支持配置 为了获得更好的 IDE 智能提示和路径跳转支持,需要配置 `tsconfig.json`: ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "#/*": ["types/*"], "$/*": ["src/plugins/*"], "~/*": ["src/modules/*"] } } } ``` #### 2. 使用规范 * **一致性**:团队内部统一使用别名,避免混用相对路径 * **可读性**:别名应该语义明确,易于理解 * **层级控制**:避免过深的路径层级,合理使用别名简化路径 * **类型安全**:配合 TypeScript 确保路径引用的类型安全 #### 3. 常见使用模式 ```typescript // 组件内综合使用示例 ``` ### 别名系统优势 1. **简化路径**:避免复杂的相对路径引用 2. **提高可维护性**:文件移动时无需修改大量引用路径 3. **增强可读性**:通过别名快速识别文件所属模块 4. **统一规范**:团队开发中保持一致的引用风格 5. **IDE 友好**:配合 TypeScript 和 IDE 提供更好的开发体验 ## 总结 通过以上基础概念的介绍,我们了解了项目的核心架构设计: ### 架构特点 * **模块化设计**:业务功能按模块划分,实现高内聚低耦合 * **插件化架构**:支持功能的热插拔和扩展 * **类型安全**:基于 TypeScript 提供完整的类型支持 * **路径优化**:通过别名系统简化文件引用 ### 开发流程 ```plantuml @startuml (*) --> "理解项目架构" "理解项目架构" --> "配置开发环境" "配置开发环境" --> "创建/选择模块" "创建/选择模块" --> "开发业务功能" "开发业务功能" --> "配置类型定义" "配置类型定义" --> "集成插件系统" "集成插件系统" --> "测试与部署" "测试与部署" --> (*) @enduml ``` ### 下一步 在掌握了这些基础概念后,建议按以下顺序深入学习: 1. **[开始使用](/v3/front/base/start)** - 环境搭建和项目启动 2. **[配置说明](/v3/front/base/configure)** - 详细配置选项 3. **[路由菜单](/v3/front/base/route-menu)** - 路由和菜单配置 4. **[模块开发](/v3/front/advanced/module)** - 深入模块化开发 5. **[插件开发](/v3/front/high/plugins)** - 插件系统详解 通过系统性的学习和实践,你将能够高效地在此架构基础上进行前端开发工作。 --- --- url: /v3/guide/toc-demo.md --- # 增强型目录导航演示 这个页面展示了我们新实现的增强型页内导航(TOC)功能。请注意右侧的目录导航,它具有以下特性: ## 主要功能特性 ### 视觉层次优化 我们的新目录导航具有清晰的视觉层次,不同级别的标题通过缩进、字体大小和颜色进行区分。 #### 标题级别区分 * H1 标题作为主要章节,具有突出的视觉效果 * H2-H6 标题根据层级递减显示 * 每个级别都有适当的缩进和间距 #### 颜色和样式 * 激活项使用渐变色高亮显示 * 悬停效果提供即时的视觉反馈 * 深色模式下自动调整颜色方案 ### 交互体验提升 增强的交互功能让导航更加流畅和直观。 #### 平滑滚动 点击目录项时,页面会平滑滚动到对应位置,而不是突然跳转。 #### 滚动同步 当您滚动页面时,目录会自动高亮当前可见的章节。 #### 键盘导航 支持使用键盘快捷键进行导航: * `↑/↓` 或 `j/k` - 上下导航 * `Enter` 或 `Space` - 激活当前项 * `Home/End` - 跳到第一项/最后一项 * `Esc` - 关闭移动端浮层 ### 现代化设计元素 我们采用了现代化的设计语言,包括: #### 渐变和阴影 * 激活指示器使用优雅的渐变效果 * 微妙的阴影增加深度感 * 圆角设计符合现代审美 #### 动画过渡 * 所有状态变化都有平滑的过渡动画 * 激活项的脉冲动画提供视觉指引 * 移动端的弹出动画流畅自然 ## 功能增强 ### 阅读时间估算 目录顶部显示预估的阅读时间,帮助读者合理安排时间。计算考虑了: * 文本长度 * 代码块数量 * 图片数量 ### 滚动进度指示 页面右侧有一个垂直的进度条,显示您的阅读进度。 ### 长目录折叠 对于包含大量章节的长文档,支持折叠/展开功能: * Ctrl/Cmd + 点击 H1 标题可以折叠其下的子章节 * 自动记住折叠状态 ### 搜索高亮支持 当使用页面搜索功能时,匹配的目录项会被高亮显示。 ## 国际化支持 ### 多语言适配 目录导航完美支持多种语言: #### 中文支持 * 优化的中文字体渲染 * 适当的字符间距 * 本地化的界面文本 #### 日文支持 * 日文字体优化 * 假名和汉字的混合排版 * 日式界面习惯 #### 英文支持 * 西文字体优化 * 自然的单词间距 * 英文界面文本 ### CJK 字符优化 针对中日韩文字的特殊优化: * 更好的字重控制 * 优化的行高设置 * 适当的段落间距 ## 移动端体验 ### 响应式设计 目录导航在不同设备上都有良好的表现: #### 桌面端 * 固定在右侧 * 始终可见 * 充分利用屏幕空间 #### 平板端 * 自适应宽度 * 字体大小调整 * 触摸友好的交互 #### 移动端 * 浮动按钮触发 * 底部弹出式面板 * 手势支持 ### 触摸优化 移动设备上的特殊优化: * 更大的点击区域 * 触摸手势支持 * 防误触设计 ## 技术实现细节 ### 性能优化 我们注重性能,确保流畅的用户体验: #### 防抖和节流 * 滚动事件使用节流处理 * 窗口调整使用防抖 * 减少不必要的重绘 #### GPU 加速 * 动画使用 transform 属性 * 启用硬件加速 * 优化的重排重绘 #### 懒加载 * 按需加载功能模块 * 延迟初始化非关键功能 * 优化首屏加载时间 ### 可访问性 我们重视所有用户的使用体验: #### 屏幕阅读器支持 * 语义化的 HTML 结构 * ARIA 属性标注 * 键盘导航支持 #### 高对比度模式 * 自动适应系统设置 * 增强的边框和轮廓 * 清晰的焦点指示 #### 减少动画 * 尊重用户的动画偏好 * 提供无动画的替代方案 * 保持功能完整性 ## 使用示例 ### 基础配置 目录导航会自动生成,无需额外配置。但您可以通过选项自定义行为: ```typescript enhanceTOC({ enableReadingTime: true, // 显示阅读时间 enableProgress: true, // 显示进度条 enableCollapse: true, // 启用折叠功能 enableSmoothScroll: true, // 平滑滚动 enableKeyboardNav: true, // 键盘导航 enableMobileFloat: true, // 移动端浮动 enableSearchHighlight: true // 搜索高亮 }) ``` ### 样式自定义 您可以通过 CSS 变量自定义外观: ```css :root { --toc-gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); --toc-indent-base: 1rem; --toc-font-h1: 0.95rem; /* 更多变量... */ } ``` ## 总结 这个增强型的页内导航系统大大提升了文档的可读性和导航体验。它不仅在视觉上更加现代和吸引人,在功能上也提供了丰富的交互特性。无论是桌面端还是移动端,无论是哪种语言,用户都能获得一致且优秀的使用体验。 通过这些改进,我们希望让用户在浏览长文档时能够: * 快速了解文档结构 * 轻松定位感兴趣的内容 * 追踪阅读进度 * 享受流畅的导航体验 感谢您体验我们的增强型目录导航系统! --- --- url: /backend/frameworks/hyperf/3.1/base/lang.md --- # 多语言处理 ::: tip MineAdmin 的多语言处理依赖 [hyperf/translation](https://github.com/hyperf/translation)。 因此关于如何加载多语言本文不再另行讲述。 ::: ## 客户端语言识别 在 MineAdmin 中。识别客户端的语言交由了 `Mine\Support\Middleware\TranslationMiddleware` 中间件识别 并在 `config/autoload/middlewares.php` 中进行了中间件注册 ::: code-group ```php{34-41} [TranslationMiddleware] declare(strict_types=1); /** * This file is part of MineAdmin. * * @link https://www.mineadmin.com * @document https://doc.mineadmin.com * @contact root@imoi.cn * @license https://github.com/mineadmin/MineAdmin/blob/master/LICENSE */ namespace Mine\Support\Middleware; use Hyperf\Contract\TranslatorInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class TranslationMiddleware implements MiddlewareInterface { public function __construct( private readonly TranslatorInterface $translator ) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $this->translator->setLocale($this->getLocale($request)); return $handler->handle($request); } // 获取语言标识 protected function getLocale(ServerRequestInterface $request): string { $locale = null; // 判断请求头是否有 Accept-Language 语言标识。如果有则设置。如果没有则设置为简体中文 if ($request->hasHeader('Accept-Language')) { $locale = $request->getHeaderLine('Accept-Language'); } return $locale ?: 'zh_CN'; } } ``` ```php{25-26} [middlewares.php] [ // 请求ID中间件 RequestIdMiddleware::class, // 多语言识别中间件 TranslationMiddleware::class, // 跨域中间件,正式环境建议关闭。使用 Nginx 等代理服务器处理跨域问题。 CorsMiddleware::class, // 验证器中间件,处理 formRequest 验证器 ValidationMiddleware::class, ], ]; ``` ::: ## 使用 以经典的业务开发场景-用户中心用户登录来说。假设在登录时。需要返回 `登录成功` `登录失败` `密码错误` `用户已被锁定` 并且需要做 `简体中文` `繁体中文` `英语` 三个语言翻译。以下方示例为准。创建三个翻译文件 | 文件名称 | 文件所在目录 | 解释 | |-----------------|---------------|----------| | user-center.php | storage/en | 英文翻译文件 | | user-center.php | storage/zh\_CN | 简体中文翻译文件 | | user-center.php | storage/ZH\_TW | 繁体中文翻译文件 | 同时在用户处理类中直接通过 `throw new BusinessException(ResultCode::Fail,'翻译标识')` 返回业务端错误信息 ::: code-group ```php{1} [英文翻译文件] // storage/en/user-center.php return [ 'success' => 'Login Success.', 'fail' => 'Login Fail.', ’passport_eror' => 'Incorrect password.', 'user_lock' => 'The account has been locked.' ]; ``` ```php{1} [简体中文翻译文化] // storage/zh_CN/user-center.php return [ 'success' => '登录成功', 'fail' => '登录失败', ’passport_eror' => '密码错误', 'user_lock' => '账号已被锁定' ]; ``` ```php{1} [繁体中文翻译文件] // storage/zh_TW/user-center.php return [ 'success' => ' 登錄成功 ', 'fail' => ' 登錄失敗 ', 'passport_eror' => ' 密碼錯誤 ', 'user_lock' => ' 賬號已被鎖定 ' ]; ``` ```php{5,8,11} [业务处理类] class UserController extends AbstractController { public function Login(string $username,string $password){ $entity = UserModel::query->where('username',$username)->first(); if(!$entity){ throw new BusinessException(ResultCode::Fail,trans('user-center.fail'); } if(!password_verify($password,$entity->password)){ throw new BusinessException(ResultCode::Fail,trans('user-center.passport_error'); } if($user->status !== StatusEnum::Normal){ throw new BusinessException(ResultCode::Fail,trans('user-center.user_lock'); } return $this->success(trans('user-center.success')); } } ``` ::: --- --- url: /backend/frameworks/hyperf/3.2/base/lang.md --- # 多语言处理 ::: tip MineAdmin 的多语言处理依赖 [hyperf/translation](https://github.com/hyperf/translation)。 因此关于如何加载多语言本文不再另行讲述。 ::: ## 客户端语言识别 在 MineAdmin 中。识别客户端的语言交由了 `Mine\Support\Middleware\TranslationMiddleware` 中间件识别 并在 `config/autoload/middlewares.php` 中进行了中间件注册 ::: code-group ```php{34-41} [TranslationMiddleware] declare(strict_types=1); /** * This file is part of MineAdmin. * * @link https://www.mineadmin.com * @document https://doc.mineadmin.com * @contact root@imoi.cn * @license https://github.com/mineadmin/MineAdmin/blob/master/LICENSE */ namespace Mine\Support\Middleware; use Hyperf\Contract\TranslatorInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class TranslationMiddleware implements MiddlewareInterface { public function __construct( private readonly TranslatorInterface $translator ) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $this->translator->setLocale($this->getLocale($request)); return $handler->handle($request); } // 获取语言标识 protected function getLocale(ServerRequestInterface $request): string { $locale = null; // 判断请求头是否有 Accept-Language 语言标识。如果有则设置。如果没有则设置为简体中文 if ($request->hasHeader('Accept-Language')) { $locale = $request->getHeaderLine('Accept-Language'); } return $locale ?: 'zh_CN'; } } ``` ```php{25-26} [middlewares.php] [ // 请求ID中间件 RequestIdMiddleware::class, // 多语言识别中间件 TranslationMiddleware::class, // 跨域中间件,正式环境建议关闭。使用 Nginx 等代理服务器处理跨域问题。 CorsMiddleware::class, // 验证器中间件,处理 formRequest 验证器 ValidationMiddleware::class, ], ]; ``` ::: ## 使用 以经典的业务开发场景-用户中心用户登录来说。假设在登录时。需要返回 `登录成功` `登录失败` `密码错误` `用户已被锁定` 并且需要做 `简体中文` `繁体中文` `英语` 三个语言翻译。以下方示例为准。创建三个翻译文件 | 文件名称 | 文件所在目录 | 解释 | |-----------------|---------------|----------| | user-center.php | storage/en | 英文翻译文件 | | user-center.php | storage/zh\_CN | 简体中文翻译文件 | | user-center.php | storage/ZH\_TW | 繁体中文翻译文件 | 同时在用户处理类中直接通过 `throw new BusinessException(ResultCode::Fail,'翻译标识')` 返回业务端错误信息 ::: code-group ```php{1} [英文翻译文件] // storage/en/user-center.php return [ 'success' => 'Login Success.', 'fail' => 'Login Fail.', ’passport_eror' => 'Incorrect password.', 'user_lock' => 'The account has been locked.' ]; ``` ```php{1} [简体中文翻译文化] // storage/zh_CN/user-center.php return [ 'success' => '登录成功', 'fail' => '登录失败', ’passport_eror' => '密码错误', 'user_lock' => '账号已被锁定' ]; ``` ```php{1} [繁体中文翻译文件] // storage/zh_TW/user-center.php return [ 'success' => ' 登錄成功 ', 'fail' => ' 登錄失敗 ', 'passport_eror' => ' 密碼錯誤 ', 'user_lock' => ' 賬號已被鎖定 ' ]; ``` ```php{5,8,11} [业务处理类] class UserController extends AbstractController { public function Login(string $username,string $password){ $entity = UserModel::query->where('username',$username)->first(); if(!$entity){ throw new BusinessException(ResultCode::Fail,trans('user-center.fail'); } if(!password_verify($password,$entity->password)){ throw new BusinessException(ResultCode::Fail,trans('user-center.passport_error'); } if($user->status !== StatusEnum::Normal){ throw new BusinessException(ResultCode::Fail,trans('user-center.user_lock'); } return $this->success(trans('user-center.success')); } } ``` ::: --- --- url: /v3/backend/base/lang.md --- # 多语言处理 本文已迁移到 [Hyperf 多语言](/backend/frameworks/hyperf/3.2/base/lang)。 这里的“多语言处理”指业务消息的语言识别和加载;后端多框架实现请阅读 [框架实现](/backend/frameworks/hyperf/)。 --- --- url: /v3/front/advanced/tools.md --- # 工具栏扩展 :::tip 提示 右上角一排图标按钮,就是工具栏,系统开放了接口可以扩展工具栏。工具栏系统基于组件化架构,支持动态添加、移除和管理各种工具。 ::: ![工具栏](https://s21.ax1x.com/2024/10/24/pAwKsvq.jpg) ## 系统架构 工具栏系统的核心实现位于以下文件中: * **主要实现**: [`web/src/utils/toolbars.ts`](https://github.com/mineadmin/mineadmin/blob/master/web/src/utils/toolbars.ts) (本地: `/web/src/utils/toolbars.ts`) * **类型定义**: [`web/types/global.d.ts#L319-327`](https://github.com/mineadmin/mineadmin/blob/master/web/types/global.d.ts#L319-327) (本地: `/web/types/global.d.ts:319`) * **全局注册**: [`web/src/bootstrap.ts#L85`](https://github.com/mineadmin/mineadmin/blob/master/web/src/bootstrap.ts#L85) (本地: `/web/src/bootstrap.ts:85`) * **插件示例**: [`web/src/plugins/mine-admin/demo/index.ts`](https://github.com/mineadmin/mineadmin/blob/master/web/src/plugins/mine-admin/demo/index.ts) (本地: `/web/src/plugins/mine-admin/demo/index.ts`) ## 默认工具栏 系统内置了以下默认工具栏: | 工具名称 | 功能描述 | 图标 | 组件位置 | |---------|---------|------|----------| | search | 全局搜索 | `heroicons:magnifying-glass-20-solid` | `@/layouts/components/bars/toolbar/components/search.tsx` | | notification | 消息通知 | `heroicons:bell` | `@/layouts/components/bars/toolbar/components/notification.tsx` | | translate | 语言切换 | `heroicons:language-20-solid` | `@/layouts/components/bars/toolbar/components/translate.tsx` | | fullscreen | 全屏切换 | `mingcute:fullscreen-line` | `@/layouts/components/bars/toolbar/components/fullscreen.tsx` | | switchMode | 主题切换 | `lets-icons:color-mode-light` | `@/layouts/components/bars/toolbar/components/switch-mode.tsx` | | settings | 系统设置 | `heroicons:cog-solid` | `@/layouts/components/bars/toolbar/components/settings.tsx` | ## 获取工具栏实例 ::: code-group ```vue [useGlobal() 方式] ``` ```ts [通过 Vue 实例获取] import { getCurrentInstance } from 'vue' // 通过当前实例获取 const { appContext } = getCurrentInstance() const toolbar = appContext.config.globalProperties.$toolbars ``` ```ts [插件内获取方法] import type { App } from 'vue' import type { MineToolbarExpose } from '#/global' /** * 系统插件 `install` 方法,外部会传入 Vue 实例,然后获取 toolbar * 参考: web/src/plugins/mine-admin/demo/index.ts **/ function install(app: App) { const toolbar = app.config.globalProperties.$toolbars as MineToolbarExpose // 在这里可以添加自定义工具栏 } ``` ::: ## API 接口 ### MineToolbarExpose 类型 完整的工具栏 API 接口定义如下(来源:[`web/types/global.d.ts#L329-336`](https://github.com/mineadmin/mineadmin/blob/master/web/types/global.d.ts#L329-336)): ```ts interface MineToolbarExpose { state: Ref // 工具栏状态 defaultToolbars: Ref // 默认工具栏列表 toolbars: Ref // 当前工具栏列表 getShowToolbar: () => MineToolbar[] // 获取显示的工具栏 add: (toolbar: MineToolbar) => void // 添加工具栏 remove: (name: string) => void // 移除工具栏 render: () => Promise // 渲染工具栏 } ``` ### API 方法详解 | API | 类型 | 说明 | 返回值 | |-----|------|------|--------| | `state` | `Ref` | 工具栏整体显示状态 | `boolean` | | `defaultToolbars` | `Ref` | 系统默认工具栏(只读) | `MineToolbar[]` | | `toolbars` | `Ref` | 当前注册的所有工具栏 | `MineToolbar[]` | | `getShowToolbar()` | `Function` | 获取当前启用并显示的工具栏 | `MineToolbar[]` | | `add(toolbar)` | `Function` | 向工具栏注册新工具 | `void` | | `remove(name)` | `Function` | 移除指定名称的工具栏 | `void` | | `render()` | `Function` | 渲染工具栏组件(内部使用) | `Promise` | ## MineToolbar 类型定义 工具栏项的完整类型定义(来源:[`web/types/global.d.ts#L319-327`](https://github.com/mineadmin/mineadmin/blob/master/web/types/global.d.ts#L319-327)): ```ts interface MineToolbar { name: string // 工具唯一标识符 icon: string // 图标名称(支持多种图标库) title: string | (() => string) // 工具标题,支持函数动态返回 show: boolean // 是否显示该工具 className?: string | (() => string) // 自定义CSS类名 component?: () => any // 自定义组件(与handle互斥) handle?: (toolbar: MineToolbar) => any // 点击处理函数(与component互斥) } ``` ### 属性说明 * **name**: 工具的唯一标识,用于识别和管理工具 * **icon**: 图标名称,支持 heroicons、mingcute 等图标库 * **title**: 工具提示文本,支持国际化函数 * **show**: 控制工具是否在工具栏中显示 * **className**: 可选的CSS类名,用于自定义样式 * **component**: 自定义Vue组件,用于复杂的工具实现 * **handle**: 简单的点击处理函数,用于快速功能实现 :::warning 注意 `handle` 和 `component` 属性是互斥的。如果同时定义,`handle` 优先级更高,`component` 将被忽略。 ::: ## 扩展工具栏 ### 简单工具扩展 添加一个带有简单点击事件的工具: ```ts const toolbar = useGlobal().$toolbars // 添加一个简单的提示工具 toolbar.add({ name: 'simple-alert', title: '简单提示', show: true, icon: 'heroicons:information-circle', handle: (toolbar) => { console.log('点击了工具:', toolbar.name) alert('这是一个简单的工具扩展!') } }) ``` ### 组件化工具扩展 添加一个使用自定义组件的复杂工具: ```ts const toolbar = useGlobal().$toolbars // 添加一个组件化工具 toolbar.add({ name: 'custom-component', title: '自定义组件', show: true, icon: 'heroicons:puzzle-piece', // 注意:使用 component 时不要定义 handle component: () => import('@/components/custom-toolbar-item.vue') }) ``` ### 动态工具栏 根据条件动态显示工具: ```ts const toolbar = useGlobal().$toolbars const userStore = useUserStore() // 添加管理员专用工具 toolbar.add({ name: 'admin-panel', title: () => userStore.hasPermission('admin') ? '管理面板' : '无权限', show: userStore.hasPermission('admin'), icon: 'heroicons:cog-8-tooth', className: () => userStore.hasPermission('admin') ? 'admin-tool' : 'disabled-tool', handle: () => { if (userStore.hasPermission('admin')) { // 打开管理面板 router.push('/admin/panel') } else { message.warning('您没有管理员权限') } } }) ``` ### 插件中的工具栏扩展 在插件开发中扩展工具栏(参考:[`web/src/plugins/mine-admin/demo/index.ts#L19-26`](https://github.com/mineadmin/mineadmin/blob/master/web/src/plugins/mine-admin/demo/index.ts#L19-26)): ```ts import type { Plugin, MineToolbarExpose } from '#/global' import Message from 'vue-m-message' const pluginConfig: Plugin.PluginConfig = { install(app) { const $toolbars = app.config.globalProperties.$toolbars as MineToolbarExpose // 插件扩展的工具栏 $toolbars.add({ name: 'plugin-demo', title: '插件演示', show: true, icon: 'heroicons:archive-box', handle: () => Message.info('我是在插件中扩展的工具栏!') }) } } export default pluginConfig ``` ## 移除工具栏 ### 移除单个工具 ```ts const toolbar = useGlobal().$toolbars // 移除指定名称的工具 toolbar.remove('test') ``` ### 批量移除工具 ```ts const toolbar = useGlobal().$toolbars const toolsToRemove = ['tool1', 'tool2', 'tool3'] // 批量移除 toolsToRemove.forEach(name => { toolbar.remove(name) }) ``` ## 最佳实践 ### 命名规范 * 使用有意义的名称:`user-profile` 而不是 `tool1` * 使用短横线分隔:`admin-panel` 而不是 `adminPanel` * 避免与系统默认工具重名 ### 图标选择 系统支持多种图标库,推荐使用: * **Heroicons**: `heroicons:user-circle` * **Mingcute**: `mingcute:settings-line` * **Tabler**: `tabler:dashboard` ### 性能考虑 * 使用 `component` 时,利用动态导入进行代码分割 * 避免在 `handle` 函数中执行重操作 * 合理设置 `show` 属性,减少不必要的渲染 ### 用户体验 * 提供清晰的 `title` 描述工具功能 * 使用一致的图标风格 * 考虑不同权限用户的使用场景 ## 常见问题 ### Q: 工具栏不显示? **A**: 检查以下几点: 1. `show` 属性是否设置为 `true` 2. 工具名称是否与现有工具重复 3. 是否正确获取了 `$toolbars` 实例 ### Q: `handle` 和 `component` 同时定义了会怎样? **A**: `handle` 优先级更高,`component` 会被忽略。建议只定义其中一个。 ### Q: 如何调试工具栏问题? **A**: 在浏览器开发者工具中: ```js // 查看当前所有工具栏 console.log(window.__vue_app__.config.globalProperties.$toolbars.toolbars.value) // 查看显示的工具栏 console.log(window.__vue_app__.config.globalProperties.$toolbars.getShowToolbar()) ``` ### Q: 工具栏权限控制? **A**: 利用 `show` 属性和权限系统: ```ts const userStore = useUserStore() toolbar.add({ name: 'admin-only', title: '管理功能', show: userStore.hasRole('admin'), // 基于权限显示 icon: 'heroicons:shield-check', handle: () => { /* 管理功能 */ } }) ``` ## 源码参考 * **核心实现**: [`web/src/utils/toolbars.ts`](https://github.com/mineadmin/mineadmin/blob/master/web/src/utils/toolbars.ts) * **类型定义**: [`web/types/global.d.ts`](https://github.com/mineadmin/mineadmin/blob/master/web/types/global.d.ts#L319-336) * **默认组件**: [`web/src/layouts/components/bars/toolbar/components/`](https://github.com/mineadmin/mineadmin/tree/master/web/src/layouts/components/bars/toolbar/components) * **插件示例**: [`web/src/plugins/mine-admin/demo/index.ts`](https://github.com/mineadmin/mineadmin/blob/master/web/src/plugins/mine-admin/demo/index.ts) * **全局注册**: [`web/src/bootstrap.ts`](https://github.com/mineadmin/mineadmin/blob/master/web/src/bootstrap.ts#L85) --- --- url: /v3/front/advanced/layout.md --- # 布局系统 MineAdmin 3.0 的布局系统是一个灵活且强大的前端布局解决方案,支持多种布局模式和动态切换。相比 2.0 版本,新的布局系统采用了统一的架构设计,所有布局逻辑都集中在 `src/layouts/index.tsx` 文件中,提供了更好的维护性和扩展性。 ## 布局架构概览 ```plantuml @startuml package "布局系统" { [布局容器 (index.tsx)] as Layout [头部组件 (Header)] as Header [侧边栏容器] as Sidebar [主内容区] as Content [底部组件 (Footer)] as Footer Layout --> Header Layout --> Sidebar Layout --> Content Layout --> Footer package "侧边栏组件" { [主侧边栏] as MainSidebar [子侧边栏] as SubSidebar Sidebar --> MainSidebar Sidebar --> SubSidebar } package "状态管理" { [设置存储 (useSettingStore)] as SettingStore [布局状态] as LayoutState Layout --> SettingStore SettingStore --> LayoutState } } @enduml ``` ## 布局模式 MineAdmin 支持三种主要的布局模式: ### 1. 经典布局 (Classic Layout) * **特点**: 传统的左侧边栏 + 主内容区布局 * **适用场景**: 标准的后台管理界面 * **组件结构**: 固定左侧菜单,右侧内容区域 ### 2. 混合布局 (Mixed Layout) * **特点**: 顶部菜单 + 左侧子菜单的组合布局 * **适用场景**: 需要多级菜单导航的复杂应用 * **组件结构**: 顶部主菜单,左侧当前分类的子菜单 ### 3. 分栏布局 (Columns Layout) * **特点**: 多栏式菜单布局 * **适用场景**: 菜单分类较多的大型应用 * **组件结构**: 左侧主菜单栏,中间子菜单栏,右侧内容区 ## 布局相关API ### useSettingStore API 参考 | 方法名 | 返回类型 | 说明 | 使用示例 | |--------|----------|------|----------| | `isMixedLayout()` | `boolean` | 判断当前是否为混合布局模式 | `store.isMixedLayout()` | | `isColumnsLayout()` | `boolean` | 判断当前是否为分栏布局模式 | `store.isColumnsLayout()` | | `isClassicLayout()` | `boolean` | 判断当前是否为经典布局模式 | `store.isClassicLayout()` | | `getFixedAsideState()` | `boolean` | 获取子侧边栏是否为固定状态 | `store.getFixedAsideState()` | | `getMenuCollapseState()` | `boolean` | 获取菜单是否为折叠状态 | `store.getMenuCollapseState()` | | `getMobileState()` | `boolean` | 判断当前是否为移动端状态 | `store.getMobileState()` | ::: tip API 源码位置 * **GitHub**: [useSettingStore.ts](https://github.com/mineadmin/MineAdmin/blob/master/web/src/store/modules/useSettingStore.ts) * **本地路径**: `mineadmin/web/src/store/modules/useSettingStore.ts` ::: ### 使用示例 ```typescript // 在Vue组件中使用布局API import { useSettingStore } from '@/stores/modules/settingStore' export default defineComponent({ setup() { const settingStore = useSettingStore() // 检查当前布局模式 const isClassic = computed(() => settingStore.isClassicLayout()) const isMixed = computed(() => settingStore.isMixedLayout()) const isColumns = computed(() => settingStore.isColumnsLayout()) // 获取菜单状态 const isMenuCollapsed = computed(() => settingStore.getMenuCollapseState()) const isAsideFixed = computed(() => settingStore.getFixedAsideState()) // 响应式判断设备类型 const isMobile = computed(() => settingStore.getMobileState()) return { isClassic, isMixed, isColumns, isMenuCollapsed, isAsideFixed, isMobile } } }) ``` ## 布局切换功能 ### 动态布局切换 ```typescript // 布局模式切换示例 import { useSettingStore } from '@/stores/modules/settingStore' const settingStore = useSettingStore() // 切换到经典布局 const switchToClassic = () => { settingStore.updateSettings({ layout: 'classic' }) } // 切换到混合布局 const switchToMixed = () => { settingStore.updateSettings({ layout: 'mixed' }) } // 切换到分栏布局 const switchToColumns = () => { settingStore.updateSettings({ layout: 'columns' }) } // 切换菜单折叠状态 const toggleMenuCollapse = () => { settingStore.toggleMenuCollapse() } ``` ## 全局样式配置 ### CSS 变量定义 ::: tip 配置文件位置 * **GitHub**: * **本地路径**: `mineadmin/web/src/assets/styles/global.scss` ::: ```scss /* 布局尺寸变量 */ :root { /* ========== 头部区域 ========== */ --mine-g-header-height: 55px; --mine-g-toolbar-height: 55px; /* ========== 底部区域 ========== */ --mine-g-footer-height: 50px; /* ========== 侧边栏区域 ========== */ --mine-g-main-aside-width: 80px; /* 主侧边栏宽度 */ --mine-g-sub-aside-width: 200px; /* 子侧边栏展开宽度 */ --mine-g-sub-aside-collapse-width: 65px; /* 子侧边栏折叠宽度 */ --mine-g-menu-retract-width: 15px; /* 菜单缩进宽度 */ /* ========== 标签栏 ========== */ --mine-g-tabbar-height: 40px; /* ========== 主题色彩 ========== */ --mine-g-box-shadow-color: rgb(0 0 0 / 18%); --el-color-primary: --ui-primery; /* ========== 响应式断点 ========== */ --mine-g-mobile-breakpoint: 768px; --mine-g-tablet-breakpoint: 1024px; } ``` ### 响应式布局配置 ```scss /* 响应式布局样式 */ @media screen and (max-width: 768px) { :root { --mine-g-main-aside-width: 0px; --mine-g-sub-aside-width: 100vw; --mine-g-header-height: 50px; } } @media screen and (min-width: 768px) and (max-width: 1024px) { :root { --mine-g-main-aside-width: 60px; --mine-g-sub-aside-width: 180px; } } ``` ## 高级配置 ### 自定义布局样式 ```scss /* 自定义布局配置示例 */ .mine-layout { /* 自定义头部样式 */ &__header { background: var(--el-bg-color); border-bottom: 1px solid var(--el-border-color-light); height: var(--mine-g-header-height); } /* 自定义侧边栏样式 */ &__aside { width: var(--mine-g-main-aside-width); transition: width 0.3s ease; &--collapsed { width: var(--mine-g-sub-aside-collapse-width); } } /* 自定义内容区域样式 */ &__main { margin-left: var(--mine-g-main-aside-width); transition: margin-left 0.3s ease; min-height: calc(100vh - var(--mine-g-header-height)); } } ``` ### 布局状态持久化 ```typescript // 布局状态持久化配置 import { defineStore } from 'pinia' export const useLayoutStore = defineStore('layout', { state: () => ({ mode: 'classic' as 'classic' | 'mixed' | 'columns', isAsideCollapsed: false, isAsideFixed: true, isMobile: false }), persist: { key: 'mine-admin-layout', storage: localStorage, paths: ['mode', 'isAsideCollapsed', 'isAsideFixed'] } }) ``` ## 性能优化 ### 布局组件懒加载 ```typescript // 布局组件异步加载 import { defineAsyncComponent } from 'vue' export const LayoutComponents = { Header: defineAsyncComponent(() => import('@/layouts/components/Header.vue')), Aside: defineAsyncComponent(() => import('@/layouts/components/Aside.vue')), Main: defineAsyncComponent(() => import('@/layouts/components/Main.vue')), Footer: defineAsyncComponent(() => import('@/layouts/components/Footer.vue')) } ``` ### 布局切换动画优化 ```scss /* 布局切换动画优化 */ .layout-transition { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); will-change: transform, width, margin; } /* 减少不必要的重绘 */ .layout-aside { contain: layout style paint; transform: translateZ(0); /* 启用硬件加速 */ } ``` ## 常见问题解决 ### 1. 移动端布局适配问题 ```typescript // 移动端适配解决方案 import { useBreakpoints } from '@vueuse/core' const breakpoints = useBreakpoints({ mobile: 0, tablet: 768, desktop: 1024 }) const isMobile = breakpoints.smaller('tablet') const isTablet = breakpoints.between('tablet', 'desktop') const isDesktop = breakpoints.greater('desktop') ``` ### 2. 布局闪烁问题 ```scss /* 防止布局闪烁 */ .mine-layout { opacity: 0; transition: opacity 0.2s ease; &.loaded { opacity: 1; } } ``` ### 3. 侧边栏滚动问题 ```scss /* 侧边栏滚动优化 */ .layout-aside { overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--el-border-color) transparent; &::-webkit-scrollbar { width: 6px; } &::-webkit-scrollbar-thumb { background: var(--el-border-color); border-radius: 3px; } } ``` ## 相关文档 * [常用 Store](/v3/front/high/store) - 状态管理相关文档 ::: tip 源码参考 完整的布局系统源码可以在以下位置找到: * **GitHub**: [web/src/layouts](https://github.com/mineadmin/mineadmin/tree/master/web/src/layouts) * **本地路径**: `mineadmin/web/src/layouts/` ::: --- --- url: /v3/faq.md --- # 常见问题 *** ## 安装成功后报错 `DNS Lookup resolve failed` 检查 `.env` 文件中的 `mysql` `redis` 是否正确,能否正常连接 *** ## 购买的插件无法使用 如果是付费插件请在QQ群或微信群联系管理员,提供订单号,管理员会拉你进对应的插件售后群 *** ## 如何从 Swoole 切换到 Swow ::: warning Swow 安装请参考 [Swow 官方文档](https://docs.toast.run/swow-blog/chs/init.html#%E6%94%AF%E6%8C%81%E7%9A%84%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F) ::: 1. copy 项目目录下的 `.github/ci/server.php` 覆盖 `config/autoload/server.php` 2. copy 项目目录下的 `.github/ci/hyperf.php` 覆盖 `bin/hyperf.php` 重新启动即可 *** ## 安装了插件后,提交到git后,线上部署拉取代码(或者其他人拉取代码),前端访问插件的后端接口报not fund 1. plugin/mine-admin下面的插件中install.lock 必须提交,否则插件的路由无法识别 2. gitignore中有\*.lock,去掉这行 *** ## 上传图片或文件,访问Not Found 问题 1. 生产环境下,建议使用nginx代理。 使用Nginx 代理可以借鉴以下配置 (注意 env 配置 和上传目录权限)。请注意,以下路径仅为示例,需根据实际部署环境调整。 假设资源url 为 https://example.com/uploads/\*\*/\*\*\*\*.png ```nginx # 代理 uploads 中的图片资源 location /uploads/ { alias /www/wwwroot/MineAdmin/storage/uploads/; expires 30d; add_header Cache-Control "public"; add_header Access-Control-Allow-Origin *; # 只允许图片文件 location ~* \.(jpg|jpeg|png|gif|webp|svg|ico|bmp)$ { expires 1y; add_header Cache-Control "public, immutable"; } # 防止访问其他文件类型 location ~* \.(php|html|htm|js|css)$ { deny all; } } ``` ::: warning 如果确认所有配置均正确,但仍无法访问并且出现 403 Forbidden,请检查 `uploads` 目录的权限是否设置为 755,并确保所属用户为 `www`。 ::: 2. 开发环境下,在/config/autoload/server.php,配置如下: ```php 'settings' => [ // 开启外部可以访问 Constant::OPTION_ENABLE_STATIC_HANDLER => env('APP_DEBUG', false), Constant::OPTION_DOCUMENT_ROOT => BASE_PATH . '/storage', //... ], ``` .env文件,APP\_DEBUG改为true,配置后重启服务。 *** ## Windows下使用Docker启动为什么这么慢 ### 原因分析 在Windows系统下使用Docker时,启动速度慢主要是由于Docker的文件系统特性导致的。Docker在Windows上运行时,底层使用的是虚拟化技术(如WSL2或Hyper-V),当使用bind mount(绑定挂载)方式将Windows宿主机的目录挂载到容器内时,会产生跨文件系统的访问开销。 特别是当挂载包含大量小文件的目录(如`vendor`目录包含成千上万个依赖包文件,`runtime`目录包含日志和缓存文件)时,每次文件读写都需要经过: 1. 容器内的文件系统 2. Docker虚拟化层 3. Windows主机文件系统 这种跨文件系统的频繁I/O操作会显著降低性能,导致应用启动缓慢。 ### 解决方案 通过使用Docker的命名卷(named volumes)来管理不需要频繁修改的目录,让这些目录完全在Docker内部的文件系统中管理,避免跨文件系统访问。同时只挂载必要的源代码目录,实现性能和开发便利性的平衡。 在`docker-compose.yml`中配置如下: ```yaml services: hyperf: volumes: # 使用命名卷存储vendor和runtime,避免跨文件系统访问 - vendor_data:/www/vendor - runtime_data:/www/runtime # 只挂载必要的源代码目录 - ./app:/www/app - ./config:/www/config - ./bin:/www/bin - ./plugin:/www/plugin - ./databases:/www/databases - ./storage:/www/storage - ./web:/www/web - ./composer.json:/www/composer.json - ./composer.lock:/www/composer.lock - ./.env:/www/.env # 定义命名卷 volumes: vendor_data: runtime_data: ``` **配置说明:** * `vendor_data` 和 `runtime_data` 是Docker命名卷,数据存储在Docker管理的空间中,I/O性能接近原生 * 源代码目录(如`app`、`config`等)仍然挂载到宿主机,方便实时编辑和调试 * `composer.json`、`composer.lock`和`.env`单独挂载,确保依赖配置和环境变量可以实时同步 采用这种配置后,应用启动速度可以显著提升。 *** ## 定时任务-Command类型,只有第一次成功,后续都失败问题。 调用目标,需要加上--disable-event-dispatcher: ture配置。 即crontab表中的value值为: ```json {"command":"mine:xxx","--disable-event-dispatcher":true} ``` 详细文档:https://hyperf.wiki/3.1/#/zh-cn/crontab --- --- url: /v3/plugin/develop/question.md --- # 常见问题 开发常见的问题将在此处列出 *** 目前待收集反馈的问题,如有问题,请在群里联系 `MineAdmin的团队成员`,我会将您拉入开发者交流群。 ## 禁止应用类型 以下为 MineAdmin 禁止上架的应用类型列表 *** ## 禁止列表 * 夺宝 * 返利 * 众筹 * 借贷 * 拍卖 * 数字币 * 区块链 * 境外支付 * 个人支付 * 抽奖 * 点卡 * 论坛 * 采集 --- --- url: /v3/plugin/develop/publish.md --- # 应用发布 打包应用并发布到应用市场, 供其他用户下载使用。 ## 应用打包 目前提供一种打包方式,将插件应用整个目录作为一个 git 仓库项目。可以托管到 `github`、`gitee` 等代码托管平台。 也可以托管到 MineAdmin 自建的 GIT 服务器上。 http://git.mineadmin.com ### 打包步骤 1. 将应用目录作为一个 git 仓库项目,提交到代码托管平台。 ```shell cd 你的插件应用目录 git init git add . git commit -m "first commit" git remote add origin 你的代码仓库地址 git push -u origin master ``` 2. 进入 [MineAdmin 插件创建页](https://www.mineadmin.com/member/createApp) 输入代码仓库地址并提交 3) 等待 MineAdmin 审核通过后,应用会显示在应用市场中。 ::: warning 打包注意事项 * 一定要将 `mine.json` 必填信息填写完整,否则应用无法正常发布。 * 应用上传后,会经过我们审核,审核通过后,才会显示到应用市场,请知悉。 * 请确保你的插件目录中不包含 `install.lock` 文件,否则会导致应用无法正常安装。 ::: ## 应用版本控制 为了保持 `MineAdmin` 生态系统的健康、可靠和安全,每次你对自己拥有的应用进行重大更新时,我们建议遵循: semantic versioning spec 的基础上发布新版本。 ### 建议 我们建议你的应用版本从1.0.0开始并递增,如下: | 代码状态 | 阶段说明 | 规则 | 示例版本号 | |:---------------|:--------------|:-----------|:-------| | 代码首次发布 | 新版本上线 | 从 1.0.0 开始 | 1.0.0 | | 代码向后兼容的错误修复 | Bug 修复,发布补丁版本 | 建议增加第三位数字 | 1.0.1 | | 代码向后兼容的新功能 | 新增小功能,发布次要版本 | 建议增加第二位数字 | 1.1.0 | | 代码破坏并不向后兼容性的变更 | 破坏性更新,发布主要版本 | 建议增加第一位数字 | 2.0.0 | --- --- url: /v3/front/base/start.md --- # 开始 ::: tip 提示 以下内容全以源码已经下载好,并在命令行下进入到了 `./web` 目录为前提。 ::: ## 开发环境 需要在本地依次安装好 [Node.js](https://nodejs.org/zh-cn), [pnpm](https://pnpm.io/)。也可以使用 `yarn` 等其他包管理工具,推荐使用 `pnpm`,文档内容以 `pnpm` 为准。 * Node.js >= 20.0.0,推荐 20.x.x 的 LTS 版本 * PNPM >= 9.0.0 ## 安装依赖及运行 运行成功后,会自动打开页面,默认地址为 http://localhost:2888 ```bash # 安装依赖 pnpm i 或 pnpm install # 运行 pnpm dev ``` ::: warning 安装依赖报错 如果无法正常安装依赖,可能是因为 npm 默认源无法访问, 可以尝试执行 `pnpm config set registry https://registry.npmmirror.com/` 切换为国内 `npmmirror` 镜像源(也可以使用 [nrm](https://github.com/Pana/nrm) 一键切换源), 然后删除根目录下 `/node_modules` 文件夹并重新安装依赖。 ::: --- --- url: /v3/plugin/guide.md --- # 快速入门指南 本指南将帮助您快速创建第一个 MineAdmin 插件,从环境准备到插件发布的完整流程。 ## 前置要求 开始之前,请确保您已经: 1. **安装 MineAdmin**:确保 MineAdmin 系统正常运行 2. **熟悉技术栈**: * PHP 8.1+ 和 Hyperf 框架 * Vue 3 + TypeScript (如需前端开发) * Composer 包管理器 ## 环境配置 ### 1. 获取 AccessToken 访问插件市场和开发者功能需要 AccessToken: 1. 登录 [MineAdmin 官网](https://www.mineadmin.com/login) 2. 进入 [个人中心设置](https://www.mineadmin.com/member/setting) 3. 查看并复制 AccessToken ### 2. 配置环境变量 在项目根目录的 `.env` 文件中添加: ```ini # MineAdmin AccessToken MINE_ACCESS_TOKEN=您的AccessToken ``` ### 3. 初始化插件系统 如果是首次使用插件系统,需要初始化: ```bash # 初始化插件扩展系统 (MineAdmin 3.0+ 版本已默认初始化) php bin/hyperf.php mine-extension:initial ``` ## 创建第一个插件 ### 1. 使用命令行创建插件 ```bash # 创建一个混合型插件 php bin/hyperf.php mine-extension:create mycompany/hello-world \ --name "Hello World" \ --type mix \ --author "Your Name" \ --description "我的第一个MineAdmin插件" ``` **参数说明**: * `mycompany/hello-world`:插件路径 (命名空间/插件名) * `--name`:插件显示名称 * `--type`:插件类型 (mix/backend/frontend) * `--author`:作者名称 * `--description`:插件描述 ### 2. 生成的目录结构 命令执行后会在 `plugin/mycompany/hello-world/` 目录下生成: ``` plugin/mycompany/hello-world/ ├── mine.json # 插件配置文件 ├── src/ # 后端源码目录 │ ├── ConfigProvider.php # 配置提供者 │ ├── InstallScript.php # 安装脚本 │ └── UninstallScript.php # 卸载脚本 ├── web/ # 前端源码目录 └── Database/ # 数据库相关 ├── Migrations/ # 数据库迁移 └── Seeders/ # 数据填充 ``` ## 开发您的插件 ### 1. 配置插件信息 编辑 `mine.json` 文件,完善插件信息: ```json { "name": "mycompany/hello-world", "description": "我的第一个MineAdmin插件", "version": "1.0.0", "type": "mixed", "author": [ { "name": "Your Name", "role": "developer" } ], "composer": { "psr-4": { "Plugin\\Mycompany\\HelloWorld\\": "src" }, "config": "Plugin\\Mycompany\\HelloWorld\\ConfigProvider" } } ``` ### 2. 实现配置提供者 编辑 `src/ConfigProvider.php`: ```php [ // 依赖注入配置 ], 'annotations' => [ 'scan' => [ 'paths' => [ __DIR__, ], ], ], 'publish' => [ // 配置文件发布设置 ], ]; } } ``` ### 3. 添加业务逻辑 创建控制器 `src/Controller/HelloController.php`: ```php 200, 'message' => 'Hello from MineAdmin Plugin!', 'data' => [ 'plugin' => 'hello-world', 'timestamp' => time() ] ]; } } ``` ### 4. 前端开发 (可选) 在 `web/` 目录下添加前端组件: ```vue ``` ## 安装和测试插件 ### 1. 安装插件 ```bash # 安装插件到系统 php bin/hyperf.php mine-extension:install mycompany/hello-world --yes ``` ### 2. 测试功能 启动开发服务器并测试 API: ```bash # 启动服务 php bin/hyperf.php start # 测试 API (新终端) curl http://localhost:9501/hello-world/greeting ``` ### 3. 检查安装状态 ```bash # 查看本地已安装插件 php bin/hyperf.php mine-extension:local-list ``` ## 插件管理命令 ### 常用命令总览 ```bash # 查看远程插件列表 php bin/hyperf.php mine-extension:list # 下载远程插件 php bin/hyperf.php mine-extension:download --name plugin-name # 安装本地插件 php bin/hyperf.php mine-extension:install plugin/path --yes # 卸载插件 php bin/hyperf.php mine-extension:uninstall plugin/path --yes # 查看本地插件 php bin/hyperf.php mine-extension:local-list ``` ## 开发调试技巧 ### 1. 日志调试 在插件中使用 Hyperf 日志系统: ```php use Hyperf\Logger\LoggerFactory; $logger = $container->get(LoggerFactory::class)->get('plugin'); $logger->info('Hello World Plugin Debug', ['data' => $someData]); ``` ### 2. 配置热重载 开发期间修改配置后需要重启服务: ```bash # 重启 Hyperf 服务 php bin/hyperf.php start ``` ### 3. 前端热更新 如果使用 MineAdmin 前端开发环境: ```bash # 在前端项目目录 npm run dev ``` ## 下一步 现在您已经创建了第一个插件!接下来可以: 1. [深入了解插件结构](./structure.md) 2. [学习完整开发流程](./develop.md) 3. [了解生命周期管理](./lifecycle.md) 4. [查看更多示例](./examples.md) ## 常见问题 ### Q: 插件安装失败怎么办? A: 检查 `mine.json` 配置是否正确,确保 PSR-4 自动加载路径正确。 ### Q: 如何调试插件? A: 使用 Hyperf 的日志系统和调试工具,查看 `runtime/logs/` 目录下的日志文件。 ### Q: 前端组件不显示? A: 确保前端文件放在 `web/` 目录下,安装插件时会自动复制到前端项目。 --- --- url: /backend/frameworks/hyperf/3.1/data-permission/performance.md --- # 性能优化 ## 数据库索引优化 为了确保数据权限系统的高性能,需要创建适当的数据库索引: ```sql -- 核心表索引优化 CREATE INDEX idx_user_dept_id ON user(dept_id); CREATE INDEX idx_user_created_by ON user(created_by); CREATE INDEX idx_dept_parent_id ON department(parent_id); -- 数据权限策略相关索引 CREATE INDEX idx_policy_user_id ON data_permission_policy(user_id); CREATE INDEX idx_policy_position_id ON data_permission_policy(position_id); CREATE INDEX idx_policy_type ON data_permission_policy(policy_type); -- 组合索引优化复合查询 CREATE INDEX idx_user_dept_created ON user(dept_id, created_by); CREATE INDEX idx_user_dept_status ON user(dept_id, status); -- 关联表索引 CREATE INDEX idx_user_dept_mapping ON user_dept(user_id, dept_id); CREATE INDEX idx_user_position_mapping ON user_position(user_id, position_id); CREATE INDEX idx_dept_leader_mapping ON dept_leader(dept_id, user_id); ``` ## 现有系统优化建议 基于 MineAdmin 当前的数据权限实现,以下是针对性的优化建议: ### 1. Factory 类优化 ```php // /mineadmin/app/Library/DataPermission/Factory.php // 当前的 Factory 类可以通过以下方式优化: class Factory { // 添加查询结果缓存 private static array $queryCache = []; public function build(Builder $builder, User $user): void { // 超级管理员跳过检查 if ($user->isSuperAdmin()) { return; } // 缓存用户策略避免重复查询 $cacheKey = "user_policy_{$user->id}"; $policy = self::$queryCache[$cacheKey] ?? ($user->getPolicy()); if ($policy) { self::$queryCache[$cacheKey] = $policy; // 应用权限过滤逻辑... } } } ``` ### 2. 部门树查询优化 ```php // /mineadmin/app/Model/Permission/Department.php // 优化现有的 getFlatChildren 方法: public function getFlatChildren(): Collection { // 使用递归CTE优化部门树查询 $sql = " WITH RECURSIVE dept_tree AS ( SELECT id, parent_id, name, 0 as level FROM department WHERE id = ? UNION ALL SELECT d.id, d.parent_id, d.name, dt.level + 1 FROM department d INNER JOIN dept_tree dt ON d.parent_id = dt.id WHERE dt.level < 10 -- 防止无限递归 ) SELECT * FROM dept_tree ORDER BY level, id "; return collect(DB::select($sql, [$this->id])); } ``` ### 3. DataScope 注解性能优化 ```php // /mineadmin/app/Library/DataPermission/Aspects/DataScopeAspect.php // 建议在切面处理中添加性能监控: class DataScopeAspect { public function process(ProceedingJoinPoint $proceedingJoinPoint) { $start = microtime(true); try { $result = $this->handleDataScope($proceedingJoinPoint); // 记录执行时间 $duration = microtime(true) - $start; if ($duration > 0.1) { Log::warning('数据权限处理耗时较长', [ 'method' => $proceedingJoinPoint->className . '::' . $proceedingJoinPoint->methodName, 'duration' => $duration ]); } return $result; } catch (\Throwable $e) { Log::error('数据权限处理异常', [ 'error' => $e->getMessage(), 'method' => $proceedingJoinPoint->className . '::' . $proceedingJoinPoint->methodName ]); throw $e; } } } ``` ## 查询优化策略 ### 1. 预编译语句使用 ```php // 优化重复的权限查询 class OptimizedPolicyResolver { private static array $preparedStatements = []; public static function getUserPolicy(int $userId): ?Policy { $stmt = self::$preparedStatements['user_policy'] ?? DB::getPdo()->prepare( "SELECT * FROM data_permission_policy WHERE user_id = ? LIMIT 1" ); $stmt->execute([$userId]); $result = $stmt->fetch(); return $result ? new Policy($result) : null; } } ``` ### 2. 批量查询优化 ```php // 针对现有系统的批量操作优化 class BatchDataPermissionHelper { public static function loadUsersWithPolicies(array $userIds): Collection { // 批量预加载用户及其权限策略 return User::with(['policy', 'position.policy']) ->whereIn('id', $userIds) ->get(); } public static function loadDepartmentTrees(array $deptIds): array { // 批量加载部门树 $departments = Department::with('children') ->whereIn('id', $deptIds) ->get(); $trees = []; foreach ($departments as $dept) { $trees[$dept->id] = $dept->getFlatChildren(); } return $trees; } } ``` ## 缓存策略 ### Redis 缓存配置 ```php // config/autoload/cache.php return [ 'default' => [ 'driver' => 'redis', 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 'prefix' => 'mineadmin:cache:', ], // 数据权限专用缓存 'data_permission' => [ 'driver' => 'redis', 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 'prefix' => 'mineadmin:data_perm:', 'pool' => 'default', ] ]; ``` ### 策略缓存实现 ```php use Hyperf\Cache\Annotation\Cacheable; class CachedPolicyService { #[Cacheable(prefix: "user_policy", ttl: 300)] public function getUserPolicy(int $userId): ?Policy { return Policy::where('user_id', $userId)->first(); } #[Cacheable(prefix: "dept_tree", ttl: 600)] public function getDepartmentTree(int $deptId): array { $dept = Department::find($deptId); return $dept ? $dept->getFlatChildren()->toArray() : []; } } ``` ## 监控和调试 ### 查询性能监控 ```php // 在 Factory 类中添加性能监控 class Factory { public function build(Builder $builder, User $user): void { if (config('app.debug', false)) { $start = microtime(true); // 原有的权限处理逻辑 $this->applyDataPermission($builder, $user); $duration = microtime(true) - $start; if ($duration > 0.05) { // 超过50ms记录 Log::debug('数据权限查询耗时', [ 'user_id' => $user->id, 'duration' => $duration, 'sql' => $builder->toSql() ]); } } else { $this->applyDataPermission($builder, $user); } } } ``` ### 慢查询分析 ```php // 在服务提供者中注册查询监听器 class AppServiceProvider { public function boot() { if (config('app.debug')) { DB::listen(function ($query) { if ($query->time > 100) { // 超过100ms Log::warning('慢查询检测', [ 'sql' => $query->sql, 'bindings' => $query->bindings, 'time' => $query->time . 'ms' ]); } }); } } } ``` ## 配置优化建议 ### Hyperf 框架配置 ```php // config/autoload/databases.php return [ 'default' => [ 'driver' => 'mysql', 'pool' => [ 'min_connections' => 5, // 根据实际并发调整 'max_connections' => 50, // 避免连接过多 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, // 禁用心跳减少开销 'max_idle_time' => 60, // 连接最大空闲时间 ], 'options' => [ PDO::ATTR_CASE => PDO::CASE_NATURAL, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, PDO::ATTR_STRINGIFY_FETCHES => false, PDO::ATTR_EMULATE_PREPARES => false, ], ] ]; ``` ### 协程上下文优化 ```php // 在数据权限上下文中添加协程安全机制 use Hyperf\Utils\Context; class DataPermissionContext { public static function setUserPolicy(Policy $policy): void { Context::set('data_permission.user_policy', $policy); } public static function getUserPolicy(): ?Policy { return Context::get('data_permission.user_policy'); } public static function clearContext(): void { Context::destroy('data_permission.user_policy'); } } ``` ## 总结 当前 MineAdmin 数据权限系统的优化重点应该放在: 1. **数据库索引优化** - 为核心查询字段添加合适的索引 2. **查询缓存** - 对用户策略和部门树进行缓存 3. **批量操作** - 优化多用户权限检查的批量处理 4. **性能监控** - 添加查询性能追踪和告警 5. **协程安全** - 确保在 Hyperf 协程环境下的上下文安全 这些优化措施基于现有的代码结构,可以逐步实施以提升系统性能。 --- --- url: /backend/frameworks/hyperf/3.2/data-permission/performance.md --- # 性能优化 ## 数据库索引优化 为了确保数据权限系统的高性能,需要创建适当的数据库索引: ```sql -- 核心表索引优化 CREATE INDEX idx_user_dept_id ON user(dept_id); CREATE INDEX idx_user_created_by ON user(created_by); CREATE INDEX idx_dept_parent_id ON department(parent_id); -- 数据权限策略相关索引 CREATE INDEX idx_policy_user_id ON data_permission_policy(user_id); CREATE INDEX idx_policy_position_id ON data_permission_policy(position_id); CREATE INDEX idx_policy_type ON data_permission_policy(policy_type); -- 组合索引优化复合查询 CREATE INDEX idx_user_dept_created ON user(dept_id, created_by); CREATE INDEX idx_user_dept_status ON user(dept_id, status); -- 关联表索引 CREATE INDEX idx_user_dept_mapping ON user_dept(user_id, dept_id); CREATE INDEX idx_user_position_mapping ON user_position(user_id, position_id); CREATE INDEX idx_dept_leader_mapping ON dept_leader(dept_id, user_id); ``` ## 现有系统优化建议 基于 MineAdmin 当前的数据权限实现,以下是针对性的优化建议: ### 1. Factory 类优化 ```php // /mineadmin/app/Library/DataPermission/Factory.php // 当前的 Factory 类可以通过以下方式优化: class Factory { // 添加查询结果缓存 private static array $queryCache = []; public function build(Builder $builder, User $user): void { // 超级管理员跳过检查 if ($user->isSuperAdmin()) { return; } // 缓存用户策略避免重复查询 $cacheKey = "user_policy_{$user->id}"; $policy = self::$queryCache[$cacheKey] ?? ($user->getPolicy()); if ($policy) { self::$queryCache[$cacheKey] = $policy; // 应用权限过滤逻辑... } } } ``` ### 2. 部门树查询优化 ```php // /mineadmin/app/Model/Permission/Department.php // 优化现有的 getFlatChildren 方法: public function getFlatChildren(): Collection { // 使用递归CTE优化部门树查询 $sql = " WITH RECURSIVE dept_tree AS ( SELECT id, parent_id, name, 0 as level FROM department WHERE id = ? UNION ALL SELECT d.id, d.parent_id, d.name, dt.level + 1 FROM department d INNER JOIN dept_tree dt ON d.parent_id = dt.id WHERE dt.level < 10 -- 防止无限递归 ) SELECT * FROM dept_tree ORDER BY level, id "; return collect(DB::select($sql, [$this->id])); } ``` ### 3. DataScope 注解性能优化 ```php // /mineadmin/app/Library/DataPermission/Aspects/DataScopeAspect.php // 建议在切面处理中添加性能监控: class DataScopeAspect { public function process(ProceedingJoinPoint $proceedingJoinPoint) { $start = microtime(true); try { $result = $this->handleDataScope($proceedingJoinPoint); // 记录执行时间 $duration = microtime(true) - $start; if ($duration > 0.1) { Log::warning('数据权限处理耗时较长', [ 'method' => $proceedingJoinPoint->className . '::' . $proceedingJoinPoint->methodName, 'duration' => $duration ]); } return $result; } catch (\Throwable $e) { Log::error('数据权限处理异常', [ 'error' => $e->getMessage(), 'method' => $proceedingJoinPoint->className . '::' . $proceedingJoinPoint->methodName ]); throw $e; } } } ``` ## 查询优化策略 ### 1. 预编译语句使用 ```php // 优化重复的权限查询 class OptimizedPolicyResolver { private static array $preparedStatements = []; public static function getUserPolicy(int $userId): ?Policy { $stmt = self::$preparedStatements['user_policy'] ?? DB::getPdo()->prepare( "SELECT * FROM data_permission_policy WHERE user_id = ? LIMIT 1" ); $stmt->execute([$userId]); $result = $stmt->fetch(); return $result ? new Policy($result) : null; } } ``` ### 2. 批量查询优化 ```php // 针对现有系统的批量操作优化 class BatchDataPermissionHelper { public static function loadUsersWithPolicies(array $userIds): Collection { // 批量预加载用户及其权限策略 return User::with(['policy', 'position.policy']) ->whereIn('id', $userIds) ->get(); } public static function loadDepartmentTrees(array $deptIds): array { // 批量加载部门树 $departments = Department::with('children') ->whereIn('id', $deptIds) ->get(); $trees = []; foreach ($departments as $dept) { $trees[$dept->id] = $dept->getFlatChildren(); } return $trees; } } ``` ## 缓存策略 ### Redis 缓存配置 ```php // config/autoload/cache.php return [ 'default' => [ 'driver' => 'redis', 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 'prefix' => 'mineadmin:cache:', ], // 数据权限专用缓存 'data_permission' => [ 'driver' => 'redis', 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 'prefix' => 'mineadmin:data_perm:', 'pool' => 'default', ] ]; ``` ### 策略缓存实现 ```php use Hyperf\Cache\Annotation\Cacheable; class CachedPolicyService { #[Cacheable(prefix: "user_policy", ttl: 300)] public function getUserPolicy(int $userId): ?Policy { return Policy::where('user_id', $userId)->first(); } #[Cacheable(prefix: "dept_tree", ttl: 600)] public function getDepartmentTree(int $deptId): array { $dept = Department::find($deptId); return $dept ? $dept->getFlatChildren()->toArray() : []; } } ``` ## 监控和调试 ### 查询性能监控 ```php // 在 Factory 类中添加性能监控 class Factory { public function build(Builder $builder, User $user): void { if (config('app.debug', false)) { $start = microtime(true); // 原有的权限处理逻辑 $this->applyDataPermission($builder, $user); $duration = microtime(true) - $start; if ($duration > 0.05) { // 超过50ms记录 Log::debug('数据权限查询耗时', [ 'user_id' => $user->id, 'duration' => $duration, 'sql' => $builder->toSql() ]); } } else { $this->applyDataPermission($builder, $user); } } } ``` ### 慢查询分析 ```php // 在服务提供者中注册查询监听器 class AppServiceProvider { public function boot() { if (config('app.debug')) { DB::listen(function ($query) { if ($query->time > 100) { // 超过100ms Log::warning('慢查询检测', [ 'sql' => $query->sql, 'bindings' => $query->bindings, 'time' => $query->time . 'ms' ]); } }); } } } ``` ## 配置优化建议 ### Hyperf 框架配置 ```php // config/autoload/databases.php return [ 'default' => [ 'driver' => 'mysql', 'pool' => [ 'min_connections' => 5, // 根据实际并发调整 'max_connections' => 50, // 避免连接过多 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, // 禁用心跳减少开销 'max_idle_time' => 60, // 连接最大空闲时间 ], 'options' => [ PDO::ATTR_CASE => PDO::CASE_NATURAL, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, PDO::ATTR_STRINGIFY_FETCHES => false, PDO::ATTR_EMULATE_PREPARES => false, ], ] ]; ``` ### 协程上下文优化 ```php // 在数据权限上下文中添加协程安全机制 use Hyperf\Utils\Context; class DataPermissionContext { public static function setUserPolicy(Policy $policy): void { Context::set('data_permission.user_policy', $policy); } public static function getUserPolicy(): ?Policy { return Context::get('data_permission.user_policy'); } public static function clearContext(): void { Context::destroy('data_permission.user_policy'); } } ``` ## 总结 当前 MineAdmin 数据权限系统的优化重点应该放在: 1. **数据库索引优化** - 为核心查询字段添加合适的索引 2. **查询缓存** - 对用户策略和部门树进行缓存 3. **批量操作** - 优化多用户权限检查的批量处理 4. **性能监控** - 添加查询性能追踪和告警 5. **协程安全** - 确保在 Hyperf 协程环境下的上下文安全 这些优化措施基于现有的代码结构,可以逐步实施以提升系统性能。 --- --- url: /v3/backend/contracts/api-metadata.md --- # 接口元数据契约 接口元数据契约用于保证不同后端实现生成一致的 API 文档和接口描述。MineAdmin 推荐使用 OpenAPI/Swagger 作为公共表达形式,框架实现负责把自身注解、属性或配置转换为同一套元数据。 接口元数据不只服务于 Swagger UI。它同时影响前台类型生成、表单约束、权限按钮、调试工具和操作日志。因此,元数据应被视为“接口行为的一部分”,而不是代码写完后的附属说明。 ## 必填信息 每个后台接口至少应描述以下信息: | 信息 | 说明 | |------|------| | 路径与方法 | 接口路径、HTTP 方法和资源语义。 | | 分组标签 | 所属业务模块,便于 Swagger UI 和前台工具识别。 | | 认证要求 | 是否需要登录、Token 类型和安全方案。 | | 权限标识 | 后台菜单、按钮或接口权限编码。 | | 请求参数 | 路径参数、查询参数、请求体和文件字段。 | | 响应结构 | 成功、失败、分页和业务异常的响应模型。 | ## 元数据来源 不同框架可以使用注解、属性、路由配置或独立描述文件生成 OpenAPI,但最终语义需要对齐。以当前 Hyperf 实现为例,元数据主要来自以下位置: | 来源 | 契约语义 | |------|----------| | HTTP 方法注解 | `path`、`operationId`、`summary`、`tags`、`security`。 | | `Permission` 属性 | 后台权限标识,应与菜单和按钮权限一致。 | | 表单请求对象 | 请求体字段、必填字段、验证语义和字段说明。 | | Schema 类 | 响应字段、字段类型、嵌套对象和数组元素类型。 | | `ResultResponse` | 普通接口统一响应结构。 | | `PageResponse` | 列表接口分页响应结构。 | | 全局 OpenAPI 配置 | 文档版本、服务地址、认证方案和外部文档入口。 | 如果某个实现无法把这些信息直接放进 OpenAPI 标准字段,应使用稳定的扩展字段承载 MineAdmin 语义,例如 `x-permission`、`x-request-source` 或等效字段。扩展字段名称一旦公开,应按接口契约维护。 ## 路由操作元数据 每个接口操作应保持以下字段稳定: | 字段 | 要求 | |------|------| | `operationId` | 在同一服务内唯一,作为客户端生成、接口变更识别和调试定位的稳定键。 | | `summary` | 使用清晰的业务动作名称。当前实现也会用它记录操作日志,因此不要写成“接口一”“处理数据”等空泛描述。 | | `tags` | 对应后台功能模块或资源归属,同一模块的接口应归入同一标签。 | | `path` | 与后台路由契约一致,路径参数名称需要和方法参数、文档参数保持一致。 | | `method` | 与真实 HTTP 方法一致,不能为了文档分组而改变语义。 | | `security` | 准确声明接口是否需要访问令牌、刷新令牌或无需认证。 | 认证、权限和审计依赖的接口不应省略操作元数据。对于会改变系统状态的接口,`summary` 应能直接表达被记录的业务动作。 ## 认证与权限元数据 MineAdmin 后台接口默认使用统一令牌体系。OpenAPI 中应声明全局安全方案,并在接口级别明确引用: | 方案 | 说明 | |------|------| | `Bearer` | HTTP Bearer Token,承载 JWT 访问令牌或刷新令牌。 | | `ApiKey` | Header 中的 `token`,用于兼容需要额外令牌的场景。 | 接口是否需要认证应以实际中间件和路由语义为准。登录接口不声明认证要求;退出、当前用户信息、普通后台资源和上传接口应声明认证要求;刷新令牌接口应明确它使用刷新令牌语义。 权限标识来自后端授权规则,也被前台按钮和菜单使用。接口元数据应保留权限编码,例如 `permission:user:index`、`permission:user:save`、`dataCenter:attachment:upload`。当 OpenAPI 标准字段无法表达权限时,需要通过扩展字段或生成器配置保留该值。 ## 请求元数据 请求描述应覆盖参数位置、字段类型、必填状态和验证语义: | 类型 | 要求 | |------|------| | 路径参数 | 在 `path` 中出现的参数必须声明为必填,并标注类型。 | | 查询参数 | 列表、搜索、排序和分页参数应声明为查询参数或等效请求参数。 | | JSON 请求体 | 创建、更新、授权等接口应通过 request body 描述字段。 | | 文件上传 | 使用 `multipart/form-data`,附件上传字段名固定为 `file`。 | | 批量参数 | ID 数组、权限数组、角色代码数组等应标注数组元素类型。 | 表单请求对象中的验证规则和文档必填字段应保持一致。例如规则中 `required`、`required_with`、`sometimes` 对应的必填/条件必填语义,需要在请求 schema 或字段说明里体现。字段白名单也需要一致,避免文档展示了后端不会接收的字段。 ## 响应元数据 后台接口响应应统一描述为 `Result` 包装结构: ```json { "code": 200, "message": "成功", "data": {} } ``` `code` 使用业务状态码,`message` 为业务消息,`data` 承载真实数据。接口元数据应描述 `data` 的具体结构,而不是只声明为泛型对象。 常见响应类型如下: | 类型 | `data` 结构 | |------|-------------| | 普通成功 | 对象、数组或空数组,按业务 schema 描述。 | | 分页列表 | 至少包含 `list` 和 `total`;`list` 元素类型必须指向对应 Schema。 | | 登录成功 | 包含 `access_token`、`refresh_token` 和 `expire_at`。 | | 验证失败 | 使用统一错误响应,业务状态码为 `422`。 | | 未认证/未授权 | 使用统一错误响应,业务状态码分别为 `401`、`403`。 | | 禁用状态 | 使用统一错误响应,业务状态码为 `423`。 | `ResultCode` 的枚举值应进入接口文档,至少覆盖成功、失败、未认证、未授权、未找到、方法不允许、不可接受、验证失败和禁用等场景。不同框架的异常处理可以不同,但对外元数据中的业务状态码语义需要保持一致。 ## Schema 约定 Schema 用于描述稳定的对外字段,不应直接暴露内部模型实现细节。 * Schema 名称应稳定,推荐使用资源名加 `Schema` 的形式。 * 字段名以最终 JSON 字段为准,例如 `storage_mode`、`origin_name`,不要让 PHP、JavaScript 或 ORM 的内部命名泄漏到契约中。 * 字段类型应使用 OpenAPI 可识别类型;数组字段需要声明元素类型。 * 嵌套关系应引用对应 Schema,避免把复杂对象退化成无结构的 `array`。 * 时间、枚举、状态值和文件字段应补充说明或示例。 * 请求 Schema 和响应 Schema 可以复用基础字段,但必须允许按场景过滤字段,避免创建接口、更新接口和返回对象混在一起。 ## 分页与通用能力 列表接口应使用统一分页元数据: | 项 | 约定 | |----|------| | 请求页码 | `page`,默认 `1`。 | | 每页数量 | `page_size`,默认 `10`。 | | 列表数据 | `data.list`。 | | 总数 | `data.total`。 | 导入、导出、上传、批量删除、批量授权等通用能力应使用固定的命名和结构。比如附件上传的 request body 必须能看出 `file` 是文件字段,批量授权用户角色的 `role_codes` 必须标注为字符串数组。 ## 元数据一致性 * 同一接口在不同框架实现中应生成一致的路径、方法、标签和响应模型。 * 字段名称、类型、必填状态和示例值应与真实接口保持一致。 * 权限标识应和菜单/按钮配置共用同一套编码。 * 分页、导入导出、文件上传等通用能力应使用统一描述方式。 * 操作名称、权限编码和响应 Schema 的变更应被视为兼容性变更。 * 对已公开字段进行重命名或删除时,应先通过废弃说明、版本说明或新增字段过渡。 ## 示例 以下示例展示接口元数据应表达的核心语义,不限定具体框架写法: ```yaml paths: /admin/user/list: get: operationId: userList summary: 用户列表 tags: - 用户管理 security: - Bearer: [] ApiKey: [] x-permission: permission:user:index parameters: - name: page in: query schema: type: integer default: 1 - name: page_size in: query schema: type: integer default: 10 responses: "200": description: 成功 ``` ## 与前台模板的关系 前台模板可以基于接口元数据生成请求类型、表单约束或调试文档。因此接口元数据不是单纯的说明文档,而是前后端协作的稳定契约。 前台依赖接口元数据时,通常会读取以下信息: * 根据 `operationId` 和路径生成请求方法。 * 根据 request body 和参数 schema 推导表单字段与类型。 * 根据响应 schema 推导列表、详情和选择器的数据类型。 * 根据权限标识控制按钮、菜单和页面能力。 * 根据认证声明决定是否附加访问令牌或刷新令牌。 ## 框架实现 Hyperf 当前通过 MineAdmin Swagger 注解封装生成接口元数据,具体用法见 [Hyperf 路由与 API 文档](/backend/frameworks/hyperf/3.2/base/router)。 --- --- url: /v3/plugin/backend/unit-test.md --- # 插件单元测试 一个标准应用目录结构说明 *** ## 本质上插件也是一个 composer 包,只不过 mine 在此基础上集成了更多功能. 针对需要进行单元测试的开发者,请直接在 插件目录下创建 composer.json 文件。 但需要注意以下事项: 1. composer.json 以及 mine.json 的 psr4 命名空间是否保证一致 2. 友情提示: [禁止安装非官方以外一切插件](#){style="color: red;"},以免您的应用程序遭到恶意破坏 --- --- url: /v3/plugin/command.md --- # 插件命令 ## 插件扩展初始化命令 *** ::: danger MineAdmin 2.0 版本已经默认存在插件扩展以及初始化动作,无需重复执行初始化 ::: > 此命令会发布 app-store 的配置文件、语言文件 ```shell php bin/hyperf.php mine-extension:initial ``` *** ## 查询远程插件列表(从 MineAdmin 官方扩展源) ```shell php bin/hyperf.php mine-extension:list ``` ### 参数 | 参数 | 类型 | 默认值 | 备注 | |---------|---------|------| ---| | --type | string | all | 筛选扩展类型 | | --name | string | 无 | 筛选扩展名称 | *** ## 查询本地所有插件(包括未安装的) ```shell php bin/hyperf.php mine-extension:local-list ``` ## 下载远程插件到本地 ```shell php bin/hyperf.php mine-extension:download ``` ### 参数 | 参数 | 类型 | 默认值 | 备注 | |---------|---------|-----| ---| | --name | string | 无 | 必选 | ## 安装指定插件 ```shell php bin/hyperf.php mine-extension:install {path} --yes ``` ### 参数 | 参数 | 类型 | 默认值 | 备注 | |---------|---------|-----|-----------| | path | string | 无 | 必填,插件所处目录 | | --yes | bool | false | 是否关闭安装询问 | ## 卸载指定插件 ```shell php bin/hyperf.php mine-extension:uninstall {path} --yes ``` ### 参数 | 参数 | 类型 | 默认值 | 备注 | |---------|---------|-----|-----------| | path | string | 无 | 必填,插件所处目录 | | --yes | bool | false | 是否关闭安装询问 | ## 创建一个插件 ```shell php bin/hyperf.php mine-extension:create ``` ### 参数 | 参数 | 类型 | 默认值 | 备注 | |---------------|---------|---------|------------------------------------| | path | string | 无 | 必填;创建路径,格式为 用户名/插件目录名(例如:zds/app-store) | | --type | string | mixed | 插件类型 可选: mixed,frontend,backend | | --author | string| 无,可选 | 作者名称,此值会填充到 minejson.author 中 | | --description | string| 无,可选 | 插件介绍,此值会填充到 minejson.description 中 | --- --- url: /v3/plugin/develop.md --- # 插件开发指南 本指南基于实际的 MineAdmin 官方插件代码,详细介绍插件的完整开发流程。 ## 开发流程概览 ```plantuml @startuml !define PROCESS rectangle PROCESS "1. 创建插件结构" as create PROCESS "2. 配置 mine.json" as config PROCESS "3. 编写 ConfigProvider" as provider PROCESS "4. 后端开发" as backend PROCESS "5. 前端开发" as frontend PROCESS "6. 安装/卸载脚本" as script PROCESS "7. 测试调试" as test PROCESS "8. 打包发布" as publish create --> config config --> provider provider --> backend provider --> frontend backend --> script frontend --> script script --> test test --> publish note right of create : mine-extension:create note right of config : 插件元数据配置 note right of provider : 注册服务和路由 note right of backend : Controller + Service note right of frontend : Vue3 + TypeScript note right of script : InstallScript/UninstallScript note right of test : 本地安装测试 note right of publish : 发布到应用市场 @enduml ``` ## 插件结构规范 基于 `app-store` 和 `code-generator` 插件的实际代码,MineAdmin 插件有两种典型结构: ### 简单插件结构(适合纯后端或简单功能) ``` plugin/mine-admin/plugin-name/ ├── mine.json # 插件配置文件 ├── install.lock # 安装标记(自动生成) └── src/ ├── ConfigProvider.php # 配置提供者 ├── Controller/ # 控制器 │ └── IndexController.php └── Service/ # 服务层 └── Service.php ``` ### 完整插件结构(适合复杂业务) ``` plugin/mine-admin/plugin-name/ ├── mine.json # 插件配置文件 ├── install.lock # 安装标记(自动生成) ├── README.md # 插件说明 ├── src/ # 后端代码 │ ├── ConfigProvider.php # 配置提供者 │ ├── InstallScript.php # 安装脚本 │ ├── UninstallScript.php # 卸载脚本 │ ├── Http/ │ │ ├── Controller/ # 控制器 │ │ ├── Request/ # 请求验证 │ │ └── Vo/ # 值对象 │ ├── Model/ # 数据模型 │ ├── Repository/ # 仓储层 │ └── Service/ # 服务层 ├── web/ # 前端代码 │ ├── index.ts # 插件入口 │ ├── api/ # API接口 │ ├── views/ # Vue组件 │ └── locales/ # 语言包 ├── Database/ # 数据库 │ ├── Migrations/ # 迁移文件 │ └── Seeder/ # 种子数据 ├── languages/ # 后端语言包 │ └── zh_CN/ └── publish/ # 发布资源 └── template/ # 模板文件 ``` ## 后端开发 ### 1. ConfigProvider 配置提供者 基于 app-store 插件的实际实现: ```php [ 'scan' => [ 'paths' => [ __DIR__, ], ], ], // 依赖注入(可选) 'dependencies' => [ // Interface::class => Implementation::class ], // 命令行(可选) 'commands' => [ // Command::class ], // 中间件(可选) 'middlewares' => [ 'http' => [ // Middleware::class ], ], // 事件监听器(可选) 'listeners' => [ // Listener::class ], ]; } } ``` ### 2. 控制器开发 参考 app-store 的 IndexController 实现: ```php success( $this->service->getAppList($this->request->all()) ); } /** * 下载插件 */ #[PostMapping("download")] #[Permission("plugin:store:download")] public function download(): ResponseInterface { $params = $this->request->all(); $this->service->download($params); return $this->success(); } /** * 安装插件 */ #[PostMapping("install")] #[Permission("plugin:store:install")] public function install(): ResponseInterface { $params = $this->request->all(); $this->service->install($params); return $this->success(); } /** * 卸载插件 */ #[PostMapping("unInstall")] #[Permission("plugin:store:uninstall")] public function unInstall(): ResponseInterface { $params = $this->request->all(); $this->service->unInstall($params); return $this->success(); } /** * 本地插件安装列表 */ #[GetMapping("getInstallList")] #[RemoteState] public function getInstallList(): ResponseInterface { return $this->success( $this->service->getLocalAppInstallList() ); } /** * 本地上传安装 */ #[PostMapping("uploadInstall")] #[Permission("plugin:store:uploadInstall")] public function uploadInstall(): ResponseInterface { return $this->success( $this->service->uploadLocalApp($this->request->all()) ); } } ``` **关键注解说明**: * `#[Controller]`: 定义控制器路由前缀 * `#[Auth]`: 需要登录验证 * `#[Permission]`: 权限验证 * `#[GetMapping]`/`#[PostMapping]`: 定义路由方法 * `#[Inject]`: 依赖注入 * `#[RemoteState]`: 远程状态管理 ### 3. 服务层开发 基于 app-store 的 Service 实现模式: ```php service->getAppList($params); } /** * 下载应用 */ public function download(array $params): void { $app = $this->service->getAppInfo($params['identifier']); if (empty($app['download_url'])) { throw new MineException('该应用无法下载', 500); } if (Plugin::hasLocalInstalled($params['identifier'])) { throw new MineException('应用已经存在本地,如需重新下载,请先删除本地应用', 500); } $this->service->download($params); } /** * 安装应用 */ public function install(array $params): void { $pluginName = $params['name']; if (!Plugin::hasLocal($pluginName)) { throw new MineException('插件不存在', 500); } if (Plugin::hasLocalInstalled($pluginName)) { throw new MineException('插件已经安装', 500); } Plugin::forceRefreshJsonPath($pluginName); Plugin::install($pluginName); } /** * 卸载应用 */ public function unInstall(array $params): void { $pluginName = $params['name']; if (!Plugin::hasLocalInstalled($pluginName)) { throw new MineException('插件未安装', 500); } Plugin::uninstall($pluginName); } /** * 获取本地已安装插件列表 */ public function getLocalAppInstallList(): array { $list = []; $plugins = Plugin::getLocalPlugins(); foreach ($plugins as $name => $info) { $app = ['identifier' => $name]; $app['name'] = $info['name'] ?? '未知'; $app['status'] = $info['status'] ?? false; $app['version'] = $info['version'] ?? '0.0.0'; $app['description'] = $info['description'] ?? '暂无描述'; $app['created_at'] = $info['created_at'] ?? ''; $list[] = $app; } return $list; } /** * 本地上传安装 */ public function uploadLocalApp(array $params): void { if (empty($params['path'])) { throw new MineException('请上传插件包', 500); } // 解压并验证插件包 $zipFile = new \ZipArchive(); $result = $zipFile->open($params['path']); if ($result !== true) { throw new MineException('插件包解压失败', 500); } // 获取插件信息并安装 $mineJson = $zipFile->getFromName('mine.json'); if (!$mineJson) { throw new MineException('插件包格式错误,缺少mine.json', 500); } $config = json_decode($mineJson, true); $pluginName = $config['name'] ?? null; if (!$pluginName) { throw new MineException('插件包配置错误', 500); } // 解压到插件目录 $targetPath = Plugin::getPluginPath($pluginName); $zipFile->extractTo($targetPath); $zipFile->close(); // 刷新缓存并安装 Plugin::forceRefreshJsonPath($pluginName); Plugin::install($pluginName); } } ``` ### 4. 模型层(如需数据库) 参考 code-generator 插件的模型实现: ```php 'boolean', 'is_list' => 'boolean', 'is_query' => 'boolean', 'is_required' => 'boolean', 'is_sort' => 'boolean', 'is_edit' => 'boolean', 'is_readonly' => 'boolean', ]; } ``` ## 前端开发 ### 1. 插件入口文件 (index.ts) 基于 app-store 的前端实现: ```typescript import type { App } from 'vue' import type { Plugin } from '#/global' const pluginConfig: Plugin.PluginConfig = { install(app: App) { // Vue插件安装钩子 console.log('app-store plugin install') }, config: { enable: true, info: { name: 'app-store', version: '1.0.0', author: 'MineAdmin Team', description: 'MineAdmin应用市场可视化插件' } }, views: [ { name: 'plugin:store', path: '/plugin/store', meta: { title: 'app_store.app_store', i18n: true, icon: 'material-symbols:app-shortcut', type: 'M', hidden: false, componentPath: '/plugin/mine-admin/app-store/views/index.vue', componentName: 'plugin:mine-admin:app-store:index', }, component: () => import('./views/index.vue'), } ], } export default pluginConfig ``` ### 2. API 接口封装 ```typescript // api/app-store.ts import { request } from '@/utils/request' // 获取远程插件列表 export const getAppList = (params: any) => { return request.get('/admin/plugin/store/index', { params }) } // 下载插件 export const downloadApp = (data: any) => { return request.post('/admin/plugin/store/download', data) } // 安装插件 export const installApp = (data: any) => { return request.post('/admin/plugin/store/install', data) } // 卸载插件 export const uninstallApp = (data: any) => { return request.post('/admin/plugin/store/unInstall', data) } // 获取本地已安装插件 export const getInstalledList = () => { return request.get('/admin/plugin/store/getInstallList') } // 上传本地插件安装 export const uploadInstall = (data: any) => { return request.post('/admin/plugin/store/uploadInstall', data) } ``` ### 3. Vue 组件开发 ```vue ``` ### 4. 国际化支持 ```typescript // locales/zh_CN.ts export default { app_store: { app_store: '应用市场', app_list: '应用列表', installed: '已安装', install: '安装', uninstall: '卸载', download: '下载', upload: '上传', local_upload: '本地上传', upload_tips: '请选择插件包文件(.zip格式)', } } ``` ## 安装与卸载脚本 ### InstallScript.php 基于 code-generator 插件的实际实现: ```php output = new ConsoleOutput(); try { $this->info('========================================'); $this->info('MineAdmin 代码生成器插件'); $this->info('========================================'); $this->info('开始安装插件...'); // 1. 复制模板文件 $this->copyTemplates(); // 2. 复制语言包 $this->copyLanguages(); // 3. 发布依赖资源 $this->publishVendor(); // 4. 执行数据库迁移 $this->runMigrations(); $this->info('插件安装成功!'); $this->info('========================================'); } catch (\Throwable $e) { $this->error('插件安装失败:' . $e->getMessage()); throw $e; } } /** * 复制模板文件 */ protected function copyTemplates(): void { $source = dirname(__DIR__) . '/publish/template'; $target = BASE_PATH . '/runtime/generate/template'; if (!is_dir($target)) { mkdir($target, 0755, true); } Filesystem::copy($source, $target, false); $this->info('模板文件复制成功'); } /** * 复制语言包 */ protected function copyLanguages(): void { $source = dirname(__DIR__) . '/languages'; $target = BASE_PATH . '/storage/languages'; Filesystem::copy($source, $target, false); $this->info('语言包复制成功'); } /** * 发布依赖包资源 */ protected function publishVendor(): void { $app = ApplicationContext::getContainer()->get(ApplicationInterface::class); $app->setAutoExit(false); $input = new ArrayInput([ 'command' => 'vendor:publish', 'package' => 'hyperf/translation', ]); $app->run($input, new NullOutput()); $this->info('依赖资源发布成功'); } /** * 执行数据库迁移 */ protected function runMigrations(): void { $migrationPath = dirname(__DIR__) . '/Database/Migrations'; if (!is_dir($migrationPath)) { return; } $app = ApplicationContext::getContainer()->get(ApplicationInterface::class); $app->setAutoExit(false); $input = new ArrayInput([ 'command' => 'migrate', '--path' => $migrationPath, '--force' => true, ]); $app->run($input, new NullOutput()); $this->info('数据库迁移执行成功'); } } ``` ### UninstallScript.php ```php output = new ConsoleOutput(); $this->info('========================================'); $this->info('即将卸载代码生成器插件'); $this->info('========================================'); try { // 清理模板文件 $this->cleanTemplates(); // 清理语言包 $this->cleanLanguages(); // 清理数据库(可选,根据需求决定是否清理) if ($this->confirm('是否清理数据库表?')) { $this->cleanDatabase(); } $this->info('插件卸载成功!'); } catch (\Throwable $e) { $this->error('插件卸载失败:' . $e->getMessage()); throw $e; } } protected function cleanTemplates(): void { $templatePath = BASE_PATH . '/runtime/generate/template'; if (is_dir($templatePath)) { // 递归删除目录 $this->removeDirectory($templatePath); $this->info('模板文件清理成功'); } } protected function cleanLanguages(): void { // 清理语言包文件 $langFile = BASE_PATH . '/storage/languages/zh_CN/code-generator.php'; if (file_exists($langFile)) { unlink($langFile); $this->info('语言包清理成功'); } } protected function cleanDatabase(): void { // 执行数据库清理 // 注意:这里需要谨慎处理,避免误删用户数据 $this->info('数据库表清理成功'); } private function removeDirectory(string $dir): void { if (!is_dir($dir)) { return; } $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; is_dir($path) ? $this->removeDirectory($path) : unlink($path); } rmdir($dir); } } ``` ## 数据库迁移 基于 code-generator 的迁移文件: ```php engine = 'InnoDB'; $table->comment('生成业务表'); $table->bigIncrements('id')->comment('主键'); $table->string('table_name', 200)->comment('表名称'); $table->string('table_comment', 500)->comment('表注释'); $table->string('module_name', 100)->comment('模块名'); $table->string('namespace', 255)->comment('命名空间'); $table->string('menu_name', 100)->comment('菜单名称'); $table->bigInteger('belong_menu_id')->nullable()->comment('所属菜单'); $table->string('package_name', 100)->nullable()->comment('包名'); $table->addColumn('string', 'type', ['length' => 100])->comment('生成类型'); $table->addColumn('string', 'generate_mode', ['length' => 30])->default('1')->comment('生成方式'); $table->addColumn('string', 'generate_menus', ['length' => 255])->nullable()->comment('生成菜单列表'); $table->addColumn('string', 'build_menu', ['length' => 10])->default('1')->comment('构建菜单'); $table->addColumn('string', 'component_type', ['length' => 30])->default('1')->comment('组件类型'); $table->json('options')->nullable()->comment('其他配置'); $table->bigInteger('created_by')->comment('创建者'); $table->bigInteger('updated_by')->comment('更新者'); $table->datetimes(); $table->unique('table_name'); $table->index('table_name'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('setting_generate_tables'); } }; ``` ## 测试与调试 ### 1. 本地安装测试 ```bash # 创建插件 php bin/hyperf.php mine-extension:create mine-admin/my-plugin # 安装插件 php bin/hyperf.php mine-extension:install mine-admin/my-plugin # 查看已安装插件 php bin/hyperf.php mine-extension:local-list # 卸载插件 php bin/hyperf.php mine-extension:uninstall mine-admin/my-plugin ``` ### 2. 调试技巧 ```php // 在服务层添加日志 use Hyperf\Context\ApplicationContext; use Psr\Log\LoggerInterface; $logger = ApplicationContext::getContainer()->get(LoggerInterface::class); $logger->info('调试信息', ['data' => $data]); // 使用 dd() 函数调试 dd($variable); // 使用异常抛出调试 throw new \Exception('调试信息: ' . json_encode($data)); ``` ### 3. 前端调试 ```typescript // 在浏览器控制台查看 console.log('调试信息', data) // 使用 Vue DevTools 调试组件状态 // 查看网络请求 // 使用浏览器的 Network 面板查看 API 请求和响应 ``` ## 开发最佳实践 ⭐ ### 1. 代码规范 * **命名规范**: * 插件名:`vendor/plugin-name` 格式 * 命名空间:`Plugin\Vendor\PluginName` * 类名:PascalCase * 方法名:camelCase * **PSR规范**: * 遵循 PSR-4 自动加载规范 * 遵循 PSR-12 编码规范 ### 2. 目录组织原则 * 后端代码统一放在 `src/` 目录 * 前端代码统一放在 `web/` 目录 * 数据库相关放在 `Database/` 目录 * 静态资源放在 `publish/` 目录 * 语言包放在 `languages/` 和 `web/locales/` 目录 ### 3. 配置管理 (重要) * **不要依赖 ConfigProvider 的 publish 功能** * **在 InstallScript 中处理所有文件复制和配置发布** * **在 InstallScript 中执行数据库迁移** * **在 InstallScript 中进行环境检测** ### 4. 安全考虑 ```php // 参数验证 use Hyperf\Validation\Request\FormRequest; class StoreRequest extends FormRequest { public function rules(): array { return [ 'name' => 'required|string|max:100', 'email' => 'required|email', ]; } } // 权限控制 #[Permission("plugin:module:action")] public function action() {} // SQL注入防护 - 使用参数绑定 $model->where('name', '=', $name)->get(); // XSS防护 - 前端处理 {{ data | escape }} ``` ### 5. 性能优化 ```php // 使用依赖注入减少耦合 #[Inject] protected Service $service; // 使用缓存 use Hyperf\Cache\Annotation\Cacheable; #[Cacheable(prefix: "plugin", ttl: 3600)] public function getData() {} // 路由懒加载 component: () => import('./views/index.vue') // 数据库查询优化 $query->select(['id', 'name'])->with('relation')->limit(20); ``` ### 6. 错误处理 ```php use Mine\Exception\MineException; // 业务异常 if (!$condition) { throw new MineException('错误信息', 500); } // try-catch 处理 try { // 业务逻辑 } catch (\Throwable $e) { $this->logger->error('操作失败', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw new MineException('操作失败: ' . $e->getMessage()); } ``` ## 常见问题解决 ### Q: 插件安装后无法访问? A: 1. 检查 ConfigProvider 的 annotations 配置是否正确 2. 确认控制器的 #\[Controller] 注解路由前缀 3. 检查权限注解 #\[Permission] 是否已在系统中配置 ### Q: 配置文件没有发布? A: ConfigProvider 的 publish 功能在插件中不可靠,请在 InstallScript 中手动处理配置发布。 ### Q: 数据库迁移失败? A: 1. 检查数据库连接配置 2. 确认迁移文件路径正确 3. 查看迁移命令的错误输出 ### Q: 前端组件不显示? A: 1. 检查 web/index.ts 的路由配置 2. 确认组件路径正确 3. 查看浏览器控制台错误信息 ### Q: 依赖包冲突? A: 1. 在 mine.json 中正确配置 composer 依赖版本约束 2. 使用 `composer update` 更新依赖 3. 检查与主项目的依赖兼容性 ## 相关文档 * [插件结构详解](./structure.md) * [生命周期管理](./lifecycle.md) * [API 参考文档](./api.md) * [示例代码](./examples.md) * [mine.json 配置](./mineJson.md) * [ConfigProvider 说明](./configProvider.md) --- --- url: /v3/plugin/lifecycle.md --- # 插件生命周期管理 详细介绍 MineAdmin 插件的生命周期管理,包括安装、启用、禁用、更新和卸载的完整流程。 ## 生命周期概览 MineAdmin 插件的生命周期包括以下几个阶段: ```plantuml @startuml !define RECTANGLE class state "未安装" as uninstalled state "已下载" as downloaded state "已安装" as installed state "已启用" as enabled state "已禁用" as disabled state "需要更新" as needUpdate state "已卸载" as uninstalled2 [*] --> uninstalled uninstalled --> downloaded : mine-extension:download downloaded --> installed : mine-extension:install installed --> enabled : 自动启用 enabled --> disabled : 禁用插件 disabled --> enabled : 启用插件 enabled --> needUpdate : 检测到新版本 needUpdate --> enabled : mine-extension:update enabled --> uninstalled2 : mine-extension:uninstall disabled --> uninstalled2 : mine-extension:uninstall uninstalled2 --> [*] note right of installed : 执行 InstallScript note right of uninstalled2 : 执行 UninstallScript @enduml ``` ## 插件发现与加载 ### 1. 插件发现机制 **核心实现**: `Plugin::init()` 方法在 `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) 中调用 ```plantuml @startuml participant "Application" as app participant "Plugin::init()" as plugin participant "ConfigProvider" as config participant "Hyperf Container" as container app -> plugin : 应用启动时调用 plugin -> plugin : 扫描 plugin/ 目录 plugin -> plugin : 读取 mine.json 配置 plugin -> config : 加载 ConfigProvider config -> container : 注册服务到容器 container -> app : 服务可用 note right of plugin : 只加载已安装的插件\n(存在 install.lock 文件) @enduml ``` ### 2. 加载过程详解 1. **扫描插件目录**: 遍历 `plugin/` 目录下的所有子目录 2. **检查安装状态**: 验证是否存在 `install.lock` 文件 3. **读取配置**: 解析 `mine.json` 配置文件 4. **加载 ConfigProvider**: 注册插件服务到 Hyperf 容器 5. **注册路由**: 自动注册控制器路由 6. **加载中间件**: 注册插件中间件 7. **注册事件监听器**: 加载事件监听器 ## 下载阶段 ### 命令使用 ```bash # 下载指定插件 php bin/hyperf.php mine-extension:download --name plugin-name # 查看可下载的插件列表 php bin/hyperf.php mine-extension:list ``` ### 下载过程 1. **验证 AccessToken**: 检查 `MINE_ACCESS_TOKEN` 环境变量 2. **请求远程仓库**: 从 MineAdmin 官方仓库获取插件信息 3. **下载插件包**: 下载压缩包到本地临时目录 4. **解压文件**: 解压到 `plugin/vendor/plugin-name/` 目录 5. **验证完整性**: 检查 `mine.json` 文件是否存在且格式正确 ### 实现原理 **核心服务**: App-Store 组件 ([GitHub](https://github.com/mineadmin/appstore)) 提供下载功能 ```php // 伪代码示例 class DownloadService { public function download(string $pluginName): bool { // 1. 验证访问令牌 $this->validateAccessToken(); // 2. 获取插件信息 $pluginInfo = $this->getPluginInfo($pluginName); // 3. 下载插件包 $packagePath = $this->downloadPackage($pluginInfo['download_url']); // 4. 解压到目标目录 $this->extractPackage($packagePath, $this->getPluginPath($pluginName)); return true; } } ``` ## 安装阶段 ### 命令使用 ```bash # 安装插件 php bin/hyperf.php mine-extension:install vendor/plugin-name --yes # 强制重新安装 php bin/hyperf.php mine-extension:install vendor/plugin-name --force ``` ### 安装流程详解 > ⚠️ **重要提示**: 配置文件发布、环境检测和数据库迁移应在 `InstallScript` 中处理,而不是依赖 ConfigProvider 的 publish 功能。 ```plantuml @startuml start :检查插件目录; if (目录存在?) then (是) :读取 mine.json 配置; if (配置有效?) then (是) :检查依赖关系; if (依赖满足?) then (是) :安装 Composer 依赖; :复制前端文件; #pink:执行 InstallScript; note right InstallScript 中处理: - 环境检测 - 配置文件发布 - 数据库迁移 - 初始数据填充 end note if (InstallScript 成功?) then (是) :创建 install.lock; :注册到插件列表; :触发安装事件; :清理缓存; stop else (失败) :回滚操作; :清理临时文件; stop endif else (不满足) :提示安装依赖插件; stop endif else (无效) :报告配置错误; stop endif else (否) :报告插件不存在; stop endif @enduml ``` ### 1. 前置检查 ```php // 安装前检查逻辑 class InstallChecker { public function check(string $pluginPath): array { $errors = []; // 检查插件目录 if (!is_dir($pluginPath)) { $errors[] = '插件目录不存在'; } // 检查 mine.json $configPath = $pluginPath . '/mine.json'; if (!file_exists($configPath)) { $errors[] = 'mine.json 配置文件不存在'; } // 检查依赖关系 $config = json_decode(file_get_contents($configPath), true); foreach ($config['require'] ?? [] as $dependency => $version) { if (!$this->isDependencyMet($dependency, $version)) { $errors[] = "依赖 {$dependency} 版本 {$version} 不满足"; } } return $errors; } } ``` ### 2. Composer 依赖安装 安装过程会处理插件的 Composer 依赖: ```json // mine.json 中的 composer 配置 { "composer": { "require": { "hyperf/async-queue": "^3.0", "symfony/console": "^6.0" }, "psr-4": { "Plugin\\Vendor\\PluginName\\": "src" } } } ``` 系统会自动执行: ```bash composer require hyperf/async-queue:^3.0 symfony/console:^6.0 ``` ### 3. InstallScript 处理 ⭐ > **最佳实践**: 数据库迁移、配置发布和环境检测应在 `InstallScript` 中处理: ```php // 在 InstallScript 中处理所有安装逻辑 class InstallScript { public function handle(): bool { // 1. 环境检测 if (!$this->checkEnvironment()) { echo "环境不满足要求\n"; return false; } // 2. 发布配置文件(不使用 ConfigProvider 的 publish) $this->publishConfig(); // 3. 执行数据库迁移 if (!$this->runMigrations()) { echo "数据库迁移失败\n"; return false; } // 4. 初始化数据 $this->seedData(); return true; } private function publishConfig(): void { $source = __DIR__ . '/../publish/config/plugin.php'; $target = BASE_PATH . '/config/autoload/plugin.php'; if (!file_exists($target)) { copy($source, $target); echo "配置文件已发布\n"; } } private function runMigrations(): bool { $migrationPath = __DIR__ . '/../Database/Migrations'; if (is_dir($migrationPath)) { // 使用 Hyperf 的迁移命令 $container = \Hyperf\Context\ApplicationContext::getContainer(); $application = $container->get(\Hyperf\Contract\ApplicationInterface::class); $input = new \Symfony\Component\Console\Input\ArrayInput([ 'command' => 'migrate', '--path' => $migrationPath, ]); $output = new \Symfony\Component\Console\Output\BufferedOutput(); $exitCode = $application->run($input, $output); return $exitCode === 0; } return true; } } ``` ### 4. 前端文件复制 将 `web/` 目录下的文件复制到前端项目: ``` plugin/vendor/plugin-name/web/ → 前端项目对应目录 ├── views/example.vue → src/views/plugin/vendor/plugin-name/example.vue ├── components/ExampleComp.vue → src/components/plugin/vendor/plugin-name/ExampleComp.vue └── api/example.js → src/api/plugin/vendor/plugin-name/example.js ``` ### 5. 配置文件发布 ⚠️ > **注意**: ConfigProvider 中的 `publish` 功能在插件系统中不可靠,应在 InstallScript 中手动处理: ```php // 不推荐:ConfigProvider 中的 publish 可能不生效 'publish' => [ // 这种方式在插件中可能不会执行 ] // 推荐:在 InstallScript 中手动发布 protected function publishConfig(): void { $configs = [ [ 'source' => __DIR__ . '/../publish/config/plugin.php', 'target' => BASE_PATH . '/config/autoload/plugin.php', ], [ 'source' => __DIR__ . '/../publish/config/routes.php', 'target' => BASE_PATH . '/config/routes/plugin.php', ], ]; foreach ($configs as $config) { if (!file_exists($config['target'])) { copy($config['source'], $config['target']); echo "配置文件已发布: {$config['target']}\n"; } } } ``` ### 6. 创建安装锁文件 安装成功后创建 `install.lock` 文件标记安装状态: ``` plugin/vendor/plugin-name/install.lock ``` 文件内容包含安装信息: ```json { "installed_at": "2024-01-01 12:00:00", "version": "1.0.0", "installer": "admin", "checksum": "abc123..." } ``` ## 启用/禁用管理 ### 插件状态控制 MineAdmin 支持在不卸载插件的情况下临时禁用插件: ```bash # 禁用插件 php bin/hyperf.php mine-extension:disable vendor/plugin-name # 启用插件 php bin/hyperf.php mine-extension:enable vendor/plugin-name # 查看插件状态 php bin/hyperf.php mine-extension:status vendor/plugin-name ``` ### 状态管理机制 状态信息存储在 `install.lock` 文件中: ```json { "installed_at": "2024-01-01 12:00:00", "version": "1.0.0", "status": "enabled", // enabled | disabled "disabled_at": null, "disabled_reason": null } ``` ## 更新阶段 ### 更新检查 ```bash # 检查插件更新 php bin/hyperf.php mine-extension:check-updates # 更新指定插件 php bin/hyperf.php mine-extension:update vendor/plugin-name # 更新所有插件 php bin/hyperf.php mine-extension:update-all ``` ### 更新流程 ```plantuml @startuml start :检查远程版本; if (有新版本?) then (是) :备份当前插件; :下载新版本; :验证完整性; :执行更新前脚本; :替换插件文件; :执行数据库迁移; :更新配置文件; :执行更新后脚本; if (更新成功?) then (是) :更新版本信息; :清理备份; :触发更新事件; stop else (失败) :恢复备份; :报告错误; stop endif else (否) :无需更新; stop endif @enduml ``` ### 版本兼容性处理 更新时会检查版本兼容性: ```php class UpdateManager { public function checkCompatibility(string $currentVersion, string $newVersion): bool { // 检查主版本兼容性 $current = $this->parseVersion($currentVersion); $new = $this->parseVersion($newVersion); // 主版本不同时可能存在破坏性更新 if ($current['major'] !== $new['major']) { return $this->checkBreakingChanges($currentVersion, $newVersion); } return true; } } ``` ## 卸载阶段 ### 命令使用 ```bash # 卸载插件 php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --yes # 强制卸载 (忽略错误) php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --force ``` ### 卸载流程 ```plantuml @startuml start :检查插件状态; if (插件已安装?) then (是) :检查依赖关系; if (有其他插件依赖?) then (是) :提示依赖冲突; if (强制卸载?) then (是) :继续卸载; else (否) :取消卸载; stop endif endif :执行 UninstallScript; :删除数据库表; :清理配置文件; :删除前端文件; :清理缓存; :移除 Composer 依赖; :删除插件目录; :清理注册信息; :触发卸载事件; stop else (否) :插件未安装; stop endif @enduml ``` ### 卸载脚本执行 ```php // UninstallScript 示例 class UninstallScript { public function handle(): bool { try { // 1. 清理数据库 $this->cleanDatabase(); // 2. 清理配置文件 $this->cleanConfigFiles(); // 3. 清理缓存数据 $this->cleanCache(); // 4. 清理日志文件 $this->cleanLogs(); // 5. 执行自定义清理逻辑 $this->customCleanup(); return true; } catch (\Exception $e) { logger()->error('插件卸载失败: ' . $e->getMessage()); return false; } } private function cleanDatabase(): void { // 删除插件相关表 DB::statement('DROP TABLE IF EXISTS plugin_example'); // 清理配置数据 DB::table('system_config')->where('key', 'like', 'plugin.example.%')->delete(); } } ``` ## 错误处理与回滚 ### 安装错误回滚 如果安装过程中出现错误,系统会自动回滚: ```php class InstallRollback { public function rollback(string $pluginPath, array $operations): void { foreach (array_reverse($operations) as $operation) { try { switch ($operation['type']) { case 'database': $this->rollbackDatabase($operation['data']); break; case 'files': $this->rollbackFiles($operation['data']); break; case 'config': $this->rollbackConfig($operation['data']); break; } } catch (\Exception $e) { logger()->error('回滚操作失败: ' . $e->getMessage()); } } } } ``` ### 依赖冲突处理 当插件之间存在依赖冲突时的处理策略: ```php class DependencyResolver { public function resolveConflicts(array $conflicts): array { $solutions = []; foreach ($conflicts as $conflict) { $solution = match($conflict['type']) { 'version_conflict' => $this->resolveVersionConflict($conflict), 'circular_dependency' => $this->resolveCircularDependency($conflict), 'missing_dependency' => $this->resolveMissingDependency($conflict), default => null }; if ($solution) { $solutions[] = $solution; } } return $solutions; } } ``` ## 事件系统 插件生命周期的各个阶段都会触发相应事件: ### 事件列表 ```php // 插件生命周期事件 class PluginEvents { const BEFORE_INSTALL = 'plugin.before_install'; const AFTER_INSTALL = 'plugin.after_install'; const BEFORE_UNINSTALL = 'plugin.before_uninstall'; const AFTER_UNINSTALL = 'plugin.after_uninstall'; const BEFORE_UPDATE = 'plugin.before_update'; const AFTER_UPDATE = 'plugin.after_update'; const ENABLED = 'plugin.enabled'; const DISABLED = 'plugin.disabled'; } ``` ### 事件监听器示例 ```php use Hyperf\Event\Annotation\Listener; use Hyperf\Event\Contract\ListenerInterface; #[Listener] class PluginInstallListener implements ListenerInterface { public function listen(): array { return [ PluginEvents::AFTER_INSTALL, ]; } public function process(object $event): void { // 插件安装后的处理逻辑 logger()->info('插件安装完成', [ 'plugin' => $event->getPluginName(), 'version' => $event->getVersion() ]); // 清理缓存 $this->clearCache($event->getPluginName()); // 发送通知 $this->sendNotification($event); } } ``` ## 状态查询 ### 查看插件状态 ```bash # 查看所有本地插件状态 php bin/hyperf.php mine-extension:local-list # 查看远程可用插件 php bin/hyperf.php mine-extension:list # 查看特定插件详情 php bin/hyperf.php mine-extension:info vendor/plugin-name ``` ### 状态信息结构 ```json { "name": "vendor/plugin-name", "version": "1.0.0", "status": "enabled", "installed_at": "2024-01-01 12:00:00", "last_updated": "2024-01-15 10:30:00", "dependencies": [ "vendor/dependency-plugin" ], "dependents": [ "vendor/dependent-plugin" ], "file_integrity": "valid", "database_status": "migrated" } ``` ## 最佳实践 ### 1. 安装脚本设计 * 实现幂等性:多次执行结果一致 * 提供详细的错误信息 * 支持事务回滚 * 记录操作日志 ### 2. 卸载脚本设计 * 完全清理插件数据 * 保留用户重要数据的备份选项 * 处理依赖关系 * 优雅降级 ### 3. 版本管理 * 遵循语义化版本规范 * 提供升级路径说明 * 标注破坏性更新 * 维护更新日志 ## 相关文档 * [插件开发指南](./develop.md) - 开发流程 * [插件结构说明](./structure.md) - 目录结构 * [API 参考](./api.md) - 接口文档 * [示例代码](./examples.md) - 实践案例 --- --- url: /v3/plugin/structure.md --- # 插件目录结构 详细介绍 MineAdmin 插件的标准目录结构、文件规范和组织方式。 ## 标准目录结构 一个完整的 MineAdmin 插件目录结构如下: ``` plugin/vendor/plugin-name/ # 插件根目录 ├── mine.json # 插件核心配置文件 ⭐ ├── README.md # 插件说明文档 ├── LICENSE # 许可证文件 ├── composer.json # Composer 依赖配置 (可选) ├── src/ # 后端源码目录 ⭐ │ ├── ConfigProvider.php # 配置提供者 ⭐ │ ├── InstallScript.php # 安装脚本 ⭐ │ ├── UninstallScript.php # 卸载脚本 ⭐ │ ├── Controller/ # 控制器目录 │ │ ├── AdminController.php # 管理员控制器 │ │ └── ApiController.php # API 控制器 │ ├── Service/ # 服务层目录 │ │ └── ExampleService.php # 业务服务类 │ ├── Repository/ # 仓库层目录 │ │ └── ExampleRepository.php # 数据仓库类 │ ├── Model/ # 模型目录 │ │ └── Example.php # 数据模型 │ ├── Request/ # 请求验证目录 │ │ ├── CreateRequest.php # 创建请求验证 │ │ └── UpdateRequest.php # 更新请求验证 │ ├── Resource/ # 资源转换目录 │ │ └── ExampleResource.php # 资源转换类 │ ├── Middleware/ # 中间件目录 │ │ └── ExampleMiddleware.php # 自定义中间件 │ ├── Command/ # 命令行目录 │ │ └── ExampleCommand.php # 自定义命令 │ ├── Listener/ # 事件监听器目录 │ │ └── ExampleListener.php # 事件监听器 │ └── Exception/ # 异常处理目录 │ └── ExampleException.php # 自定义异常 ├── web/ # 前端源码目录 ⭐ │ ├── views/ # 页面组件目录 │ │ ├── index.vue # 主页面 │ │ ├── list.vue # 列表页面 │ │ └── form.vue # 表单页面 │ ├── components/ # 公共组件目录 │ │ └── ExampleComponent.vue # 通用组件 │ ├── api/ # API 接口目录 │ │ └── example.js # 接口定义 │ ├── router/ # 路由配置目录 │ │ └── index.js # 路由配置 │ ├── store/ # 状态管理目录 │ │ └── example.js # 状态管理 │ └── assets/ # 静态资源目录 │ ├── images/ # 图片资源 │ └── styles/ # 样式文件 ├── Database/ # 数据库相关目录 ⭐ │ ├── Migrations/ # 数据库迁移文件 │ │ └── 2024_01_01_000000_create_example_table.php │ └── Seeders/ # 数据填充文件 │ └── ExampleSeeder.php # 数据填充类 ├── config/ # 配置文件目录 │ └── example.php # 插件配置文件 ├── publish/ # 发布文件目录 │ ├── config/ # 配置文件模板 │ │ └── example.php # 配置文件模板 │ └── assets/ # 静态资源模板 ├── tests/ # 测试文件目录 │ ├── Unit/ # 单元测试 │ ├── Feature/ # 功能测试 │ └── TestCase.php # 测试基类 ├── docs/ # 文档目录 │ ├── installation.md # 安装文档 │ ├── usage.md # 使用文档 │ └── api.md # API 文档 └── .gitignore # Git 忽略文件 ``` ## 核心文件详解 ### 1. mine.json (插件配置文件) **文件路径**: `mine.json` ([配置详解](./mineJson.md)) 插件的核心配置文件,定义插件的基本信息、依赖关系和加载配置: ```json { "name": "vendor/plugin-name", "description": "插件描述", "version": "1.0.0", "type": "mixed", "author": [ { "name": "Author Name", "email": "author@example.com", "role": "developer" } ], "keywords": ["mineadmin", "plugin"], "homepage": "https://github.com/vendor/plugin-name", "license": "MIT", "require": { "php": ">=8.1", "hyperf/framework": "^3.0" }, "package": { "dependencies": { "vue": "^3.0", "element-plus": "^2.0" } }, "composer": { "require": { "hyperf/async-queue": "^3.0" }, "psr-4": { "Plugin\\Vendor\\PluginName\\": "src" }, "config": "Plugin\\Vendor\\PluginName\\ConfigProvider" } } ``` ### 2. ConfigProvider.php (配置提供者) **文件路径**: `src/ConfigProvider.php` **实现原理**: 基于 Hyperf ConfigProvider 机制 ([GitHub](https://github.com/hyperf/hyperf/blob/master/src/config-provider/src/ConfigProvider.php)) > ⚠️ **注意**: ConfigProvider 中的 `publish` 功能在插件系统中存在问题,建议在 InstallScript 中处理配置文件发布。 ```php [], 'annotations' => [ 'scan' => [ 'paths' => [__DIR__], ], ], 'commands' => [], 'listeners' => [], // publish 功能在插件中不推荐使用 // 请在 InstallScript 中处理配置文件发布 ]; } } ``` ### 3. InstallScript.php (安装脚本) ⭐ **文件路径**: `src/InstallScript.php` **调用时机**: 执行 `mine-extension:install` 命令时 **重要性**: 推荐在此处理配置发布、环境检测和数据库迁移 ```php checkEnvironment()) { echo "环境检测失败\n"; return false; } // 2. 发布配置文件 $this->publishConfig(); // 3. 执行数据库迁移 $this->runMigrations(); // 4. 初始化数据 $this->seedData(); echo "插件安装成功\n"; return true; } protected function checkEnvironment(): bool { // 检查 PHP 版本 if (version_compare(PHP_VERSION, '8.1.0', '<')) { echo "PHP 版本需要 >= 8.1\n"; return false; } // 检查必要的扩展 $requiredExtensions = ['redis', 'pdo', 'json']; foreach ($requiredExtensions as $ext) { if (!extension_loaded($ext)) { echo "缺少 PHP 扩展: {$ext}\n"; return false; } } return true; } protected function publishConfig(): void { $source = __DIR__ . '/../publish/config/plugin.php'; $target = BASE_PATH . '/config/autoload/plugin.php'; if (!file_exists($target)) { copy($source, $target); echo "配置文件已发布: {$target}\n"; } } protected function runMigrations(): void { $migrationPath = __DIR__ . '/../Database/Migrations'; if (is_dir($migrationPath)) { // 执行迁移命令 $container = \Hyperf\Context\ApplicationContext::getContainer(); $application = $container->get(ApplicationInterface::class); $application->setAutoExit(false); $input = new \Symfony\Component\Console\Input\ArrayInput([ 'command' => 'migrate', '--path' => $migrationPath, ]); $output = new \Symfony\Component\Console\Output\BufferedOutput(); $application->run($input, $output); echo "数据库迁移完成\n"; } } protected function seedData(): void { // 初始化默认数据 // 例如创建默认配置、菜单等 } } ``` ### 4. UninstallScript.php (卸载脚本) ⭐ **文件路径**: `src/UninstallScript.php` **调用时机**: 执行 `mine-extension:uninstall` 命令时 **重要性**: 清理配置文件、数据表和相关资源 ```php backupData(); // 2. 删除数据库表 $this->dropTables(); // 3. 清理配置文件 $this->removeConfig(); // 4. 清理缓存 $this->clearCache(); echo "插件卸载完成\n"; return true; } protected function backupData(): void { // 备份重要数据到指定目录 $backupPath = BASE_PATH . '/runtime/backup/plugin_' . date('YmdHis') . '.sql'; // 实现备份逻辑 } protected function dropTables(): void { // 删除插件创建的数据表 $tables = ['plugin_example_table', 'plugin_settings']; foreach ($tables as $table) { if (Db::schema()->hasTable($table)) { Db::schema()->drop($table); echo "已删除数据表: {$table}\n"; } } } protected function removeConfig(): void { $configFile = BASE_PATH . '/config/autoload/plugin.php'; if (file_exists($configFile)) { unlink($configFile); echo "配置文件已删除: {$configFile}\n"; } } protected function clearCache(): void { // 清理插件相关缓存 $redis = \Hyperf\Context\ApplicationContext::getContainer() ->get(\Hyperf\Redis\Redis::class); $redis->del('plugin:cache:*'); echo "缓存已清理\n"; } } ``` ## 目录结构图解 ```plantuml @startuml !define FOLDER rectangle !define FILE rectangle FOLDER "Plugin Root" as root { FILE "mine.json" as config FOLDER "src/" as src { FILE "ConfigProvider.php" as provider FILE "InstallScript.php" as install FILE "UninstallScript.php" as uninstall FOLDER "Controller/" as controller FOLDER "Service/" as service FOLDER "Model/" as model } FOLDER "web/" as web { FOLDER "views/" as views FOLDER "components/" as components FOLDER "api/" as api } FOLDER "Database/" as database { FOLDER "Migrations/" as migrations FOLDER "Seeders/" as seeders } } config --> provider : 配置加载 provider --> install : 安装时调用 provider --> uninstall : 卸载时调用 web --> views : 前端页面 database --> migrations : 数据库结构 database --> seeders : 初始数据 @enduml ``` ## 不同类型插件的结构差异 ### Mixed (混合型插件) 包含完整的 `src/` 和 `web/` 目录,提供前后端完整功能。 ### Backend (后端插件) 只包含 `src/` 目录,专注于提供 API 服务和业务逻辑: ``` plugin/vendor/backend-plugin/ ├── mine.json ├── src/ │ ├── ConfigProvider.php │ ├── Controller/ │ ├── Service/ │ └── Model/ └── Database/ ``` ### Frontend (前端插件) 只包含 `web/` 目录,专注于前端界面和交互: ``` plugin/vendor/frontend-plugin/ ├── mine.json ├── web/ │ ├── views/ │ ├── components/ │ └── assets/ └── src/ └── ConfigProvider.php # 最小配置 ``` ## 命名规范 ### 1. 目录命名 * 使用小写字母和连字符:`user-management` * 避免使用下划线和空格 ### 2. 文件命名 * PHP 类文件使用 PascalCase:`UserController.php` * Vue 组件使用 PascalCase:`UserList.vue` * 配置文件使用小写:`user.php` ### 3. 命名空间规范 遵循 PSR-4 自动加载标准: ```php // 插件路径: plugin/mineadmin/user-manager/ // 命名空间: Plugin\MineAdmin\UserManager\ namespace Plugin\MineAdmin\UserManager\Controller; ``` ## 文件权限和安全 ### 1. 文件权限设置 ```bash # 设置合适的文件权限 find plugin/ -type f -name "*.php" -exec chmod 644 {} \; find plugin/ -type d -exec chmod 755 {} \; ``` ### 2. 安全注意事项 * 敏感配置使用环境变量 * 避免在代码中硬编码密钥 * 验证和过滤用户输入 * 使用 HTTPS 传输敏感数据 ## 最佳实践 ### 1. 文件组织 * 按功能模块组织代码 * 保持目录结构清晰 * 使用有意义的文件名 ### 2. 代码规范 * 遵循 PSR-12 编码标准 * 添加适当的注释 * 使用类型声明 ### 3. 版本控制 * 使用 `.gitignore` 排除不必要的文件 * 创建清晰的提交信息 * 使用语义化版本号 ## 示例项目结构 查看官方插件的实际结构: **App-Store 插件**: MineAdmin 官方应用市场插件,展示了标准的混合型插件结构 ## 常见问题 ### Q: 插件目录应该放在哪里? A: 插件应该放在项目根目录的 `plugin/` 目录下,按 `vendor/plugin-name` 格式组织。 ### Q: 如何处理插件之间的依赖? A: 在 `mine.json` 的 `require` 字段中声明依赖的其他插件。 ### Q: 前端文件安装后放在哪里? A: `web/` 目录下的文件会在安装时复制到前端项目的对应位置。 ### Q: 数据库迁移文件如何执行? A: 在 `InstallScript.php` 中调用迁移执行逻辑,或使用 Hyperf 的迁移命令。 --- --- url: /v3/plugin/examples.md --- # 插件示例代码 本文档提供完整的 MineAdmin 插件开发示例,包括不同类型插件的实际代码案例和最佳实践。 ## 官方插件示例 ### App-Store 插件 (混合型) **仓库地址**: [mineadmin/appstore](https://github.com/mineadmin/appstore) App-Store 是 MineAdmin 唯一的官方默认插件,提供应用市场管理功能,展示了混合型插件的完整实现。 #### 核心文件结构 ``` plugin/mine-admin/app-store/ ├── mine.json # 插件配置 ├── src/ # 后端代码 │ ├── ConfigProvider.php # 配置提供者 │ ├── Controller/ # 控制器 │ ├── Service/ # 服务层 │ └── Command/ # 命令行 ├── web/ # 前端代码 │ ├── views/ # 页面组件 │ └── api/ # API 接口 └── Database/ # 数据库 ``` #### mine.json 配置示例 ```json { "name": "mine-admin/app-store", "description": "MineAdmin应用市场可视化插件", "version": "1.0.0", "type": "mixed", "author": [ { "name": "MineAdmin Team", "role": "developer" } ], "keywords": ["mineadmin", "app-store", "plugin-management"], "homepage": "https://github.com/mineadmin/appstore", "license": "MIT", "composer": { "require": { "hyperf/async-queue": "^3.0" }, "psr-4": { "Plugin\\MineAdmin\\AppStore\\": "src" }, "config": "Plugin\\MineAdmin\\AppStore\\ConfigProvider" } } ``` #### ConfigProvider 实现 ```php [ // 依赖注入配置 ], 'annotations' => [ 'scan' => [ 'paths' => [ __DIR__, ], ], ], 'commands' => [ Command\AppStoreCommand::class, ], 'listeners' => [ Listener\PluginEventListener::class, ], 'publish' => [ [ 'id' => 'appstore-config', 'description' => 'App Store configuration file', 'source' => __DIR__ . '/../publish/appstore.php', 'destination' => BASE_PATH . '/config/autoload/appstore.php', ], ], ]; } } ``` ## 完整插件开发示例 ### 1. 用户管理插件 (混合型) 以下是一个完整的用户管理插件示例,展示如何开发一个包含前后端的混合型插件。 #### mine.json 配置 ```json { "name": "mycompany/user-manager", "description": "用户管理插件", "version": "1.0.0", "type": "mixed", "author": [ { "name": "Your Name", "email": "email@example.com" } ], "composer": { "require": { "hyperf/database": "^3.0", "hyperf/validation": "^3.0" }, "psr-4": { "Plugin\\MyCompany\\UserManager\\": "src" }, "config": "Plugin\\MyCompany\\UserManager\\ConfigProvider" }, "package": { "dependencies": { "element-plus": "^2.4.0" } } } ``` #### 核心控制器实现 ```php request->all(); $result = $this->service->getPageList($params); return $this->success($result); } #[PostMapping('/user')] public function create(): array { $data = $this->request->all(); $user = $this->service->create($data); return $this->success($user, '用户创建成功'); } #[PutMapping('/user/{id}')] public function update(int $id): array { $data = $this->request->all(); $this->service->update($id, $data); return $this->success(null, '更新成功'); } #[DeleteMapping('/user/{id}')] public function delete(int $id): array { $this->service->delete($id); return $this->success(null, '删除成功'); } } ``` #### 服务层实现 ```php where(function($q) use ($params) { $q->where('username', 'like', "%{$params['keyword']}%") ->orWhere('email', 'like', "%{$params['keyword']}%"); }); } $paginator = $query->paginate( $params['pageSize'] ?? 15, ['*'], 'page', $params['page'] ?? 1 ); return [ 'items' => $paginator->items(), 'pageInfo' => [ 'total' => $paginator->total(), 'currentPage' => $paginator->currentPage(), 'totalPage' => $paginator->lastPage() ] ]; } public function create(array $data): User { $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); return User::create($data); } public function update(int $id, array $data): bool { if (isset($data['password'])) { $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); } return User::query()->where('id', $id)->update($data) > 0; } public function delete(int $id): bool { return User::destroy($id) > 0; } } ``` ### 2. 后端型插件示例 - API 服务插件 以下是一个纯后端 API 服务插件的示例。 #### 插件配置 (mine.json) ```json { "name": "mycompany/api-service", "description": "API 服务插件", "version": "1.0.0", "type": "backend", "author": [ { "name": "Your Name", "email": "email@example.com" } ], "keywords": ["api", "service"], "license": "MIT", "composer": { "require": { "guzzlehttp/guzzle": "^7.0" }, "psr-4": { "Plugin\\MyCompany\\ApiService\\": "src" }, "config": "Plugin\\MyCompany\\ApiService\\ConfigProvider" } } ``` #### ConfigProvider 实现 ```php [ Contract\ApiClientInterface::class => Service\ApiClient::class, ], 'annotations' => [ 'scan' => [ 'paths' => [ __DIR__, ], ], ], 'commands' => [ Command\ApiSyncCommand::class, ], 'publish' => [ [ 'id' => 'api-service-config', 'description' => 'API 服务配置文件', 'source' => __DIR__ . '/../publish/api_service.php', 'destination' => BASE_PATH . '/config/autoload/api_service.php', ], ], ]; } } ``` ### 3. 前端型插件示例 - 数据可视化插件 以下是一个纯前端的数据可视化插件示例。 #### mine.json 配置 ```json { "name": "mycompany/data-visualization", "description": "数据可视化插件", "version": "1.0.0", "type": "frontend", "author": [ { "name": "Your Name", "email": "email@example.com" } ], "package": { "dependencies": { "echarts": "^5.4.0", "vue-echarts": "^6.5.0" } } } ``` ## 完整插件开发最佳实践 ### 1. 目录结构规范 ``` plugin/vendor-name/plugin-name/ ├── mine.json # 插件配置文件 ├── src/ # PHP 后端代码 │ ├── ConfigProvider.php # 配置提供者 │ ├── Controller/ # 控制器 │ ├── Service/ # 服务层 │ ├── Model/ # 模型 │ ├── Command/ # 命令行 │ ├── Listener/ # 事件监听器 │ └── Middleware/ # 中间件 ├── web/ # 前端代码 │ ├── views/ # Vue 页面组件 │ ├── api/ # API 接口封装 │ ├── components/ # 公共组件 │ └── locales/ # 国际化 ├── Database/ # 数据库 │ ├── Migrations/ # 迁移文件 │ └── Seeders/ # 数据填充 └── publish/ # 发布文件 └── config.php # 配置文件 ``` ### 2. 命名规范 * **插件名称**: 使用 `vendor/plugin-name` 格式 * **命名空间**: `Plugin\VendorName\PluginName` * **类名**: 使用大驼峰命名法 * **方法名**: 使用小驼峰命名法 ### 3. 核心组件示例 #### 控制器示例 ```php request->all(); $result = $this->service->getList($params); return $this->success($result); } #[PostMapping('/create')] public function create(): array { $data = $this->request->all(); $result = $this->service->create($data); return $this->success($result, '创建成功'); } $user = $this->userService->find($id); if (!$user) { return $this->error('用户不存在', 404); } return $this->success($user); } /** * 更新用户 */ #[PutMapping('/users/{id:\d+}')] public function update(int $id): array { $data = $this->request->all(); $user = $this->userService->update($id, $data); return $this->success($user, '用户更新成功'); } /** * 删除用户 */ #[DeleteMapping('/users/{id:\d+}')] public function destroy(int $id): array { $this->userService->delete($id); return $this->success([], '用户删除成功'); } /** * 批量导入用户 */ #[PostMapping('/users/import')] public function import(): array { $file = $this->request->file('file'); if (!$file || !$file->isValid()) { return $this->error('请上传有效的文件'); } $result = $this->userService->importFromFile($file); return $this->success($result, '导入完成'); } /** * 导出用户数据 */ #[GetMapping('/users/export')] public function export(): array { $params = $this->request->all(); $filePath = $this->userService->exportToFile($params); return $this->success(['file_path' => $filePath], '导出成功'); } } ``` #### 4. 服务层 (src/Service/UserService.php) ```php repository->getList($params); } /** * 创建用户 */ public function create(array $data): array { // 密码加密 if (isset($data['password'])) { $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); } // 生成用户头像 if (!isset($data['avatar'])) { $data['avatar'] = $this->generateAvatar($data['username']); } $user = $this->repository->create($data); // 触发用户创建事件 event(new UserCreatedEvent($user)); return $user->toArray(); } /** * 更新用户 */ public function update(int $id, array $data): array { // 密码更新处理 if (isset($data['password']) && !empty($data['password'])) { $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); } else { unset($data['password']); } $user = $this->repository->update($id, $data); // 触发用户更新事件 event(new UserUpdatedEvent($user)); return $user->toArray(); } /** * 从文件导入用户 */ public function importFromFile($file): array { $filePath = $file->getPath() . '/' . $file->getFilename(); // 读取 Excel 文件 $data = $this->parseExcelFile($filePath); $successCount = 0; $errorCount = 0; $errors = []; foreach ($data as $index => $row) { try { $this->create([ 'username' => $row['username'], 'email' => $row['email'], 'phone' => $row['phone'] ?? null, 'password' => $row['password'] ?? '123456', ]); $successCount++; } catch (\Exception $e) { $errorCount++; $errors[] = "第{$index}行: " . $e->getMessage(); } } return [ 'success_count' => $successCount, 'error_count' => $errorCount, 'errors' => $errors ]; } /** * 导出用户到文件 */ public function exportToFile(array $params = []): string { $users = $this->repository->getAllForExport($params); // 生成 Excel 文件 $filePath = $this->generateExcelFile($users); return $filePath; } /** * 生成用户头像 */ private function generateAvatar(string $username): string { // 使用第三方库生成头像 $avatar = new \Intervention\Image\ImageManager(); // ... 头像生成逻辑 return '/uploads/avatars/' . $username . '.png'; } /** * 解析 Excel 文件 */ private function parseExcelFile(string $filePath): array { // Excel 解析逻辑 return []; } /** * 生成 Excel 文件 */ private function generateExcelFile(array $users): string { // Excel 生成逻辑 return '/tmp/users_export_' . date('YmdHis') . '.xlsx'; } protected function getRepository(): string { return UserRepository::class; } } ``` #### 5. 数据仓库 (src/Repository/UserRepository.php) ```php getModel()::query(); // 关键词搜索 if (!empty($params['keyword'])) { $query->where(function ($q) use ($params) { $q->where('username', 'like', "%{$params['keyword']}%") ->orWhere('email', 'like', "%{$params['keyword']}%") ->orWhere('phone', 'like', "%{$params['keyword']}%"); }); } // 状态筛选 if (isset($params['status'])) { $query->where('status', $params['status']); } // 角色筛选 if (!empty($params['role_id'])) { $query->whereHas('roles', function ($q) use ($params) { $q->where('id', $params['role_id']); }); } // 时间范围筛选 if (!empty($params['created_at'])) { $dates = explode(' - ', $params['created_at']); if (count($dates) === 2) { $query->whereBetween('created_at', [ $dates[0] . ' 00:00:00', $dates[1] . ' 23:59:59' ]); } } // 排序 $query->orderBy($params['sort'] ?? 'id', $params['order'] ?? 'desc'); return $query->paginate($params['pageSize'] ?? 15)->toArray(); } /** * 获取导出数据 */ public function getAllForExport(array $params = []): array { $query = $this->getModel()::query(); // 应用相同的筛选条件 // ... 筛选逻辑 return $query->select([ 'id', 'username', 'email', 'phone', 'status', 'created_at', 'updated_at' ])->get()->toArray(); } } ``` #### 6. 模型 (src/Model/User.php) ```php 'integer', 'last_login_at' => 'datetime:Y-m-d H:i:s', 'created_at' => 'datetime:Y-m-d H:i:s', 'updated_at' => 'datetime:Y-m-d H:i:s', ]; /** * 状态常量 */ const STATUS_DISABLED = 0; const STATUS_ENABLED = 1; /** * 关联角色 */ public function roles() { return $this->belongsToMany(Role::class, 'user_roles'); } /** * 获取状态文本 */ public function getStatusTextAttribute(): string { return match($this->status) { self::STATUS_ENABLED => '启用', self::STATUS_DISABLED => '禁用', default => '未知' }; } /** * 获取头像 URL */ public function getAvatarUrlAttribute(): string { if (empty($this->avatar)) { return '/default-avatar.png'; } return str_starts_with($this->avatar, 'http') ? $this->avatar : config('app.url') . $this->avatar; } } ``` #### 7. 前端页面 (web/views/UserList.vue) ```vue ``` #### 8. API 接口 (web/api/user.js) ```javascript // web/api/user.js import { request } from '@/utils/request' const API_BASE = '/user-manager' export default { // 获取用户列表 getList(params) { return request({ url: `${API_BASE}/users`, method: 'get', params }) }, // 创建用户 create(data) { return request({ url: `${API_BASE}/users`, method: 'post', data }) }, // 获取用户详情 get(id) { return request({ url: `${API_BASE}/users/${id}`, method: 'get' }) }, // 更新用户 update(id, data) { return request({ url: `${API_BASE}/users/${id}`, method: 'put', data }) }, // 删除用户 delete(id) { return request({ url: `${API_BASE}/users/${id}`, method: 'delete' }) }, // 批量导入 import(file) { const formData = new FormData() formData.append('file', file) return request({ url: `${API_BASE}/users/import`, method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data' } }) }, // 导出数据 export(params) { return request({ url: `${API_BASE}/users/export`, method: 'get', params }) } } ``` #### 9. 数据库迁移 (Database/Migrations/create\_users\_table.php) ```php id(); $table->string('username', 50)->unique()->comment('用户名'); $table->string('email')->unique()->comment('邮箱'); $table->string('phone', 20)->nullable()->comment('手机号'); $table->string('password')->comment('密码'); $table->string('avatar')->nullable()->comment('头像'); $table->tinyInteger('status')->default(1)->comment('状态:0禁用,1启用'); $table->timestamp('last_login_at')->nullable()->comment('最后登录时间'); $table->timestamps(); $table->index(['username']); $table->index(['email']); $table->index(['phone']); $table->index(['status']); $table->index(['created_at']); $table->comment('用户管理插件-用户表'); }); } public function down(): void { Schema::dropIfExists('plugin_user_manager_users'); } } ``` #### 10. 安装脚本 (src/InstallScript.php) ```php runMigrations(); // 2. 初始化数据 $this->seedData(); // 3. 创建必要目录 $this->createDirectories(); // 4. 初始化配置 $this->initConfig(); echo "用户管理插件安装成功!\n"; return true; } catch (\Exception $e) { echo "安装失败: " . $e->getMessage() . "\n"; return false; } } private function runMigrations(): void { $migrationPath = __DIR__ . '/../Database/Migrations'; if (!is_dir($migrationPath)) { return; } $files = glob($migrationPath . '/*.php'); sort($files); foreach ($files as $file) { require_once $file; $className = $this->getMigrationClassName($file); $migration = new $className(); if (method_exists($migration, 'up')) { $migration->up(); echo "执行迁移: " . basename($file) . "\n"; } } } private function seedData(): void { // 插入默认管理员用户 Db::table('plugin_user_manager_users')->insertOrIgnore([ 'username' => 'admin', 'email' => 'admin@example.com', 'password' => password_hash('123456', PASSWORD_DEFAULT), 'status' => 1, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); echo "初始化默认数据完成\n"; } private function createDirectories(): void { $directories = [ BASE_PATH . '/public/uploads/avatars', BASE_PATH . '/storage/user-manager', ]; foreach ($directories as $dir) { if (!is_dir($dir)) { mkdir($dir, 0755, true); echo "创建目录: {$dir}\n"; } } } private function initConfig(): void { $configPath = BASE_PATH . '/config/autoload/user_manager.php'; if (!file_exists($configPath)) { $defaultConfig = [ 'avatar_upload_path' => '/uploads/avatars', 'default_password' => '123456', 'password_reset_expire' => 3600, 'max_login_attempts' => 5, ]; file_put_contents($configPath, " HS : 管理钩子 PM --> RM : 注册路由 PM --> VAI : 安装插件 PA --> PM : 注册 PB --> PM : 注册 PC --> PM : 配置 HS --> VR : 路由钩子 HS --> PS : 状态管理钩子 HS --> EP : 组件钩子 note right of PM : 插件生命周期管理\n动态启停控制\n依赖关系处理 note left of HS : 支持多种钩子类型\n异步钩子处理\n钩子执行顺序控制 @enduml ``` ### 核心特性 * **零侵入设计**: 插件开发无需修改核心代码 * **动态加载**: 支持插件的动态启用和禁用 * **生命周期管理**: 完整的插件生命周期钩子 * **类型安全**: 完整的 TypeScript 类型定义 * **性能优化**: 懒加载和按需加载支持 * **错误隔离**: 插件错误不影响主应用运行 ## 插件数据类型介绍 ::: info 类型定义文件 类型定义在 `types/global.d.ts` 内 ::: :::details 点击查看完整类型定义 ```ts declare namespace Plugin { /** * 插件基础信息 */ interface Info { /** 插件名称,格式:作者名称空间/插件名 */ name: string /** 插件版本,遵循语义化版本 */ version: string /** 插件作者 */ author: string /** 插件描述 */ description: string /** 插件启动顺序,数值越大越先启动,默认为 0 */ order?: number /** 插件依赖列表 */ dependencies?: string[] /** 插件关键词,用于搜索 */ keywords?: string[] /** 插件主页地址 */ homepage?: string /** 插件许可证 */ license?: string /** 最低系统版本要求 */ minSystemVersion?: string } /** * 插件配置 */ interface Config { /** 插件基础信息 */ info: Info /** 是否启用插件 */ enable: boolean /** 插件开发模式,用于调试 */ devMode?: boolean /** 插件自定义配置项 */ settings?: Record } /** * 插件视图路由定义 */ interface Views extends Route.RouteRecordRaw { /** 路由元信息扩展 */ meta?: { /** 页面标题 */ title?: string /** 国际化键值 */ i18n?: string /** 页面图标 */ icon?: string /** 是否需要权限验证 */ requireAuth?: boolean /** 所需权限列表 */ permissions?: string[] /** 是否缓存页面 */ keepAlive?: boolean /** 页面是否隐藏 */ hidden?: boolean /** 菜单排序 */ order?: number } } /** * 钩子函数类型定义 */ interface HookHandlers { /** 插件启动钩子,可用于初始化验证 */ start?: (config: Config) => Promise | boolean | void /** 系统初始化完成钩子,可访问 Vue 上下文 */ setup?: () => Promise | void /** 路由注册钩子,可修改路由配置 */ registerRoute?: (router: Router, routesRaw: Route.RouteRecordRaw[] | Views[] | MineRoute.routeRecord[]) => Promise | void /** 用户登录钩子 */ login?: (formInfo: LoginFormData) => Promise | void /** 用户退出登录钩子 */ logout?: () => Promise | void /** 获取用户信息钩子 */ getUserInfo?: (userInfo: UserInfo) => Promise | void /** 路由跳转钩子(外链无效) */ routerRedirect?: (context: { from: RouteLocationNormalized, to: RouteLocationNormalized }, router: Router) => Promise | void /** 网络请求拦截钩子 */ networkRequest?: (request: AxiosRequestConfig) => Promise | AxiosRequestConfig /** 网络响应拦截钩子 */ networkResponse?: (response: AxiosResponse) => Promise> | AxiosResponse /** 错误处理钩子 */ error?: (error: Error, context?: string) => Promise | void /** 页面加载完成钩子 */ mounted?: () => Promise | void /** 页面销毁钩子 */ beforeDestroy?: () => Promise | void } /** * 插件主配置接口 */ interface PluginConfig { /** 插件安装函数,注册组件、指令等 */ install: (app: App) => Promise | void /** 插件配置信息 */ config: Config /** 插件路由定义 */ views?: Views[] /** 插件钩子函数 */ hooks?: HookHandlers /** 插件自定义属性 */ [key: string]: any } /** * 插件存储状态 */ interface PluginStore { /** 已安装的插件列表 */ plugins: Map /** 插件启用状态 */ enabledPlugins: Set /** 插件加载状态 */ loadingPlugins: Set /** 插件错误信息 */ pluginErrors: Map } /** * 插件管理器接口 */ interface PluginManager { /** 注册插件 */ register(name: string, plugin: PluginConfig): Promise /** 卸载插件 */ unregister(name: string): Promise /** 启用插件 */ enable(name: string): Promise /** 禁用插件 */ disable(name: string): Promise /** 获取插件信息 */ getPlugin(name: string): PluginConfig | null /** 获取所有插件 */ getAllPlugins(): Map /** 检查插件依赖 */ checkDependencies(name: string): Promise } } /** * 登录表单数据类型 */ interface LoginFormData { username: string password: string captcha?: string remember?: boolean } /** * 用户信息类型 */ interface UserInfo { id: number username: string nickname: string email: string avatar: string roles: string[] permissions: string[] [key: string]: any } ``` ::: ## 创建插件 ### 目录结构与命名规范 所有插件都放在 `src/plugins` 目录下,且插件有别名 `$` 指向了此目录,插件跟后端结构相同, 由 `开发作者名称空间/插件名称` 组成插件目录。斜杠左边是**作者名称空间**,可在 [MineAdmin官网设置](https://www.mineadmin.com), 斜杠右边则为**插件名称**,在这个作者名称空间下唯一。 #### 标准插件目录结构 ```bash src/plugins/ ├── mine-admin/ # 官方插件命名空间 │ ├── app-store/ # 应用商店插件 │ ├── basic-ui/ # 基础UI库插件 │ └── demo/ # 官方演示插件 ├── author-name/ # 第三方开发者命名空间 │ └── plugin-name/ # 具体插件目录 │ ├── index.ts # 插件入口文件(必须) │ ├── config.ts # 插件配置文件(可选) │ ├── package.json # 插件包信息(推荐) │ ├── README.md # 插件说明文档(推荐) │ ├── views/ # 页面组件目录 │ │ ├── index.vue │ │ └── components/ │ ├── components/ # 可复用组件 │ ├── composables/ # 组合式函数 │ ├── utils/ # 工具函数 │ ├── assets/ # 静态资源 │ ├── locales/ # 国际化文件 │ │ ├── zh.json │ │ ├── en.json │ │ └── ja.json │ ├── types/ # TypeScript 类型定义 │ └── tests/ # 测试文件 ``` #### 命名规范建议 * **插件名称**: 使用小写字母和连字符,如 `file-manager`、`data-export` * **作者空间**: 使用小写字母和连字符,避免特殊字符 * **文件命名**: 遵循 kebab-case 规范 * **组件名称**: 使用 PascalCase,如 `FileUploader.vue` ::: tip 最佳实践 * 本地开发的插件也可以被系统识别,但无法上传到 MineAdmin 应用市场 * 建议为插件添加 `package.json` 以便管理依赖和版本 * 使用 TypeScript 开发可获得更好的类型提示和错误检查 * 遵循 Vue 3 组合式 API 最佳实践 ::: ::: warning 注意事项 * 插件名称在同一作者空间下必须唯一 * 避免使用系统保留字作为插件名称 * 插件目录一旦创建,不建议随意更改名称 ::: ### 插件生命周期 ```plantuml @startuml !theme plain start :系统启动; :扫描插件目录; :加载插件配置; if (插件已启用?) then (yes) :检查依赖关系; if (依赖满足?) then (yes) :执行 start 钩子; if (启动成功?) then (yes) :执行 install 方法; :注册组件和指令; :执行 setup 钩子; :注册路由; :执行 registerRoute 钩子; :插件初始化完成; else (no) :标记插件启动失败; stop endif else (no) :显示依赖错误; stop endif else (no) :跳过插件加载; stop endif :插件运行中; note right: 运行时钩子\n- login\n- logout\n- getUserInfo\n- routerRedirect\n- networkRequest\n- networkResponse if (插件被禁用?) then (yes) :执行 beforeDestroy 钩子; :清理资源; :移除路由; :卸载组件; :插件停止; stop else (no) :继续运行; endif @enduml ``` ## 插件开发指南 ### 基础插件示例 让我们通过一个完整的文件管理插件来了解插件开发的完整流程: #### 1. 创建插件入口文件 `index.ts` ```ts // src/plugins/zhang-san/file-manager/index.ts import type { App } from 'vue' import type { Router, RouteRecordRaw } from 'vue-router' import type { Plugin } from '#/global' import { ElMessage, ElNotification } from 'element-plus' // 导入插件组件 import FileManagerComponent from './components/FileManager.vue' import FileUploader from './components/FileUploader.vue' // 导入工具函数 import { formatFileSize, validateFileType } from './utils/fileUtils' // 插件配置 const pluginConfig: Plugin.PluginConfig = { // 插件安装方法 - 在这里注册全局组件、指令、插件等 async install(app: App) { try { // 注册全局组件 app.component('FileManager', FileManagerComponent) app.component('FileUploader', FileUploader) // 注册全局指令 app.directive('file-drop', { mounted(el, binding) { el.addEventListener('dragover', (e: DragEvent) => { e.preventDefault() e.stopPropagation() }) el.addEventListener('drop', async (e: DragEvent) => { e.preventDefault() e.stopPropagation() const files = Array.from(e.dataTransfer?.files || []) await binding.value(files) }) } }) // 添加全局属性 app.config.globalProperties.$fileUtils = { formatSize: formatFileSize, validateType: validateFileType } console.log('文件管理插件安装成功') } catch (error) { console.error('文件管理插件安装失败:', error) throw error } }, // 插件基础配置 config: { enable: import.meta.env.NODE_ENV !== 'production', // 生产环境禁用 devMode: import.meta.env.DEV, info: { name: 'zhang-san/file-manager', version: '2.1.0', author: '张三', description: '企业级文件管理插件,支持上传、下载、预览、权限控制等功能', keywords: ['文件管理', '文件上传', '权限控制'], homepage: 'https://github.com/zhang-san/file-manager', license: 'MIT', minSystemVersion: '3.0.0', dependencies: ['mine-admin/basic-ui'], order: 10 // 较高优先级 }, settings: { maxFileSize: 50 * 1024 * 1024, // 50MB allowedTypes: ['image/*', 'application/pdf', '.docx', '.xlsx'], uploadChunkSize: 1024 * 1024, // 1MB enablePreview: true, enableVersionControl: false } }, // 插件钩子函数 hooks: { // 插件启动验证 async start(config) { console.log('文件管理插件启动中...', config.info.name) // 检查必要的权限 const hasPermission = await checkFilePermissions() if (!hasPermission) { ElMessage.error('文件管理插件需要文件操作权限') return false // 阻止插件启动 } // 初始化插件设置 await initializeSettings(config.settings) return true }, // 系统初始化完成后执行 async setup() { // 初始化文件存储 await initFileStorage() // 注册文件类型映射 registerFileTypes() // 监听系统事件 window.addEventListener('beforeunload', handleBeforeUnload) }, // 路由注册钩子 async registerRoute(router: Router, routesRaw) { // 动态添加文件管理相关路由 const adminRoutes = routesRaw.find(route => route.path === '/admin') if (adminRoutes && adminRoutes.children) { adminRoutes.children.push({ path: 'files', name: 'FileManagement', component: () => import('./views/FileManagement.vue'), meta: { title: '文件管理', icon: 'FolderOpened', requireAuth: true, permissions: ['file:read'], keepAlive: true } }) } console.log('文件管理路由注册完成') }, // 用户登录后钩子 async login(formInfo) { console.log('用户登录,初始化文件权限') await refreshFilePermissions(formInfo.username) }, // 用户登出钩子 async logout() { console.log('用户登出,清理文件缓存') await clearFileCache() }, // 获取用户信息后钩子 async getUserInfo(userInfo) { // 根据用户角色设置文件权限 await setFilePermissions(userInfo.roles, userInfo.permissions) }, // 网络请求拦截 async networkRequest(config) { // 为文件上传请求添加特殊处理 if (config.url?.includes('/upload')) { config.timeout = 300000 // 5分钟超时 config.headers = { ...config.headers, 'X-File-Plugin': 'zhang-san/file-manager' } } return config }, // 网络响应拦截 async networkResponse(response) { // 处理文件下载响应 if (response.headers['content-type']?.includes('application/octet-stream')) { const contentDisposition = response.headers['content-disposition'] if (contentDisposition) { const filename = extractFilename(contentDisposition) response.metadata = { filename } } } return response }, // 错误处理 async error(error, context) { if (context === 'file-upload') { ElNotification.error({ title: '文件上传失败', message: error.message, duration: 5000 }) } }, // 插件销毁前清理 async beforeDestroy() { console.log('文件管理插件即将销毁,清理资源...') // 取消进行中的上传任务 await cancelAllUploads() // 清理事件监听器 window.removeEventListener('beforeunload', handleBeforeUnload) // 清理临时文件 await cleanupTempFiles() } }, // 插件路由定义 views: [ { name: 'zhangsan:filemanager:index', path: '/plugins/file-manager', component: () => import('./views/FileManagerIndex.vue'), meta: { title: '文件管理器', i18n: 'plugin.fileManager.title', icon: 'FolderOpened', requireAuth: true, permissions: ['file:read'], keepAlive: true, hidden: false } }, { name: 'zhangsan:filemanager:upload', path: '/plugins/file-manager/upload', component: () => import('./views/FileUpload.vue'), meta: { title: '文件上传', i18n: 'plugin.fileManager.upload', icon: 'Upload', requireAuth: true, permissions: ['file:create'], keepAlive: false } } ] } // 辅助函数 async function checkFilePermissions(): Promise { try { // 检查文件API是否可用 return 'File' in window && 'FileReader' in window && 'FileList' in window } catch { return false } } async function initializeSettings(settings: Record) { // 初始化插件配置 const userSettings = await getUserPluginSettings('zhang-san/file-manager') Object.assign(settings, userSettings) } async function initFileStorage() { // 初始化文件存储配置 console.log('初始化文件存储系统') } function registerFileTypes() { // 注册支持的文件类型 console.log('注册文件类型映射') } function handleBeforeUnload(event: BeforeUnloadEvent) { // 检查是否有未完成的上传任务 if (hasOngoingUploads()) { event.preventDefault() event.returnValue = '您有文件正在上传,确定要离开吗?' } } // 导出插件配置 export default pluginConfig // 导出类型定义供其他插件使用 export type { FileManagerConfig } from './types/index' ``` #### 2. 插件配置文件 `config.ts` ```ts // src/plugins/zhang-san/file-manager/config.ts export interface FileManagerUserConfig { // 上传配置 upload: { maxFileSize: number allowedTypes: string[] chunkSize: number concurrent: number } // 预览配置 preview: { enabled: boolean supportedTypes: string[] maxPreviewSize: number } // 存储配置 storage: { provider: 'local' | 'oss' | 's3' | 'cos' bucket?: string region?: string accessKey?: string secretKey?: string } // 安全配置 security: { enableVirusScan: boolean allowExecutableFiles: boolean quarantineEnabled: boolean } } export const defaultConfig: FileManagerUserConfig = { upload: { maxFileSize: 50 * 1024 * 1024, // 50MB allowedTypes: [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain' ], chunkSize: 1024 * 1024, // 1MB concurrent: 3 }, preview: { enabled: true, supportedTypes: ['image/*', 'application/pdf', 'text/plain'], maxPreviewSize: 10 * 1024 * 1024 // 10MB }, storage: { provider: 'local' }, security: { enableVirusScan: false, allowExecutableFiles: false, quarantineEnabled: true } } ``` ::: info 开发完成 以上展示了一个完整的企业级插件开发示例,包含了错误处理、权限验证、资源清理等最佳实践。 ::: ### Vue 组件集成示例 #### 创建插件组件 ```vue ``` ## 组合式函数(Composables) 插件可以提供可复用的组合式函数,供其他组件使用: ```ts // src/plugins/zhang-san/file-manager/composables/useFileManager.ts import { ref, reactive, computed } from 'vue' import { ElMessage } from 'element-plus' import type { FileItem, FileManagerState } from '../types/index' export function useFileManager() { // 状态管理 const state = reactive({ currentPath: '/', files: [], selectedFiles: [], loading: false, uploadProgress: new Map() }) // 计算属性 const currentFiles = computed(() => state.files) const breadcrumbs = computed(() => { const paths = state.currentPath.split('/').filter(Boolean) const breadcrumbs = [{ name: '根目录', path: '/' }] let currentPath = '' for (const path of paths) { currentPath += `/${path}` breadcrumbs.push({ name: path, path: currentPath }) } return breadcrumbs }) // 文件操作方法 const loadFiles = async (path: string = state.currentPath): Promise => { state.loading = true try { const response = await fetch(`/api/files?path=${encodeURIComponent(path)}`) if (!response.ok) throw new Error('Failed to load files') const files = await response.json() state.files = files state.currentPath = path } catch (error) { ElMessage.error('加载文件列表失败') throw error } finally { state.loading = false } } const uploadFile = async (file: File, path: string): Promise => { const uploadId = `${path}/${file.name}` state.uploadProgress.set(uploadId, 0) try { const formData = new FormData() formData.append('file', file) formData.append('path', path) const response = await fetch('/api/files/upload', { method: 'POST', body: formData, onUploadProgress: (progressEvent) => { if (progressEvent.total) { const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total) state.uploadProgress.set(uploadId, percent) } } }) if (!response.ok) throw new Error('Upload failed') ElMessage.success(`文件 ${file.name} 上传成功`) await loadFiles(path) } catch (error) { ElMessage.error(`文件 ${file.name} 上传失败`) throw error } finally { state.uploadProgress.delete(uploadId) } } const deleteFile = async (file: FileItem): Promise => { try { const response = await fetch(`/api/files?path=${encodeURIComponent(file.path)}`, { method: 'DELETE' }) if (!response.ok) throw new Error('Delete failed') ElMessage.success('文件删除成功') } catch (error) { ElMessage.error('文件删除失败') throw error } } const createFolder = async (parentPath: string, folderName: string): Promise => { try { const response = await fetch('/api/folders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ parent: parentPath, name: folderName }) }) if (!response.ok) throw new Error('Create folder failed') ElMessage.success('文件夹创建成功') } catch (error) { ElMessage.error('文件夹创建失败') throw error } } const navigateToFolder = async (path: string): Promise => { await loadFiles(path) } const downloadFile = async (file: FileItem): Promise => { try { const response = await fetch(`/api/files/download?path=${encodeURIComponent(file.path)}`) if (!response.ok) throw new Error('Download failed') const blob = await response.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = file.name a.click() window.URL.revokeObjectURL(url) } catch (error) { ElMessage.error('文件下载失败') throw error } } return { // 状态 ...toRefs(state), // 计算属性 currentFiles, breadcrumbs, // 方法 loadFiles, uploadFile, deleteFile, createFolder, navigateToFolder, downloadFile } } ``` ## 高级插件模式 ### 插件间通信 插件可以通过事件系统进行通信: ```ts // 插件A:发布事件 import { EventBus } from '@/utils/eventBus' // 在插件钩子中 hooks: { setup() { // 发布文件上传完成事件 EventBus.emit('file:uploaded', { fileName: 'example.pdf', fileSize: 1024, uploadTime: new Date() }) } } // 插件B:监听事件 hooks: { setup() { // 监听文件上传完成事件 EventBus.on('file:uploaded', (fileInfo) => { console.log('文件上传完成:', fileInfo) // 执行相关业务逻辑 updateFileStats(fileInfo) }) }, beforeDestroy() { // 清理事件监听器 EventBus.off('file:uploaded') } } ``` ### 插件依赖管理 ```ts // 高级插件依赖示例 const pluginConfig: Plugin.PluginConfig = { config: { info: { name: 'zhang-san/advanced-file-manager', dependencies: [ 'mine-admin/basic-ui@^2.0.0', // 版本范围 'li-si/image-processor@latest', // 最新版本 'wang-wu/cloud-storage' // 任意版本 ] } }, hooks: { async start(config) { // 检查依赖是否满足 const dependencyChecker = usePluginDependencies() const unsatisfiedDeps = await dependencyChecker.check(config.info.dependencies) if (unsatisfiedDeps.length > 0) { console.error('未满足的依赖:', unsatisfiedDeps) return false } return true } } } ``` ### 插件配置发布与管理 ```bash # 发布插件配置到用户可编辑目录 pnpm plugin:publish zhang-san/file-manager # 批量发布所有插件配置 pnpm plugin:publish-all # 重置插件配置到默认状态 pnpm plugin:reset zhang-san/file-manager ``` 插件配置发布后的使用: ```ts // 获取用户自定义配置 import { usePluginConfig } from '@/composables/usePlugin' const { config, updateConfig } = usePluginConfig('zhang-san/file-manager') // 在组件中使用配置 const maxFileSize = computed(() => config.value.upload.maxFileSize) // 更新配置 const updateUploadConfig = async (newConfig: Partial) => { await updateConfig({ upload: { ...config.value.upload, ...newConfig } }) } ``` ## 动态插件管理 ### 插件状态控制 ```ts // 获取插件管理器实例 const pluginManager = usePluginManager() // 启用插件 const enablePlugin = async (pluginName: string) => { try { const success = await pluginManager.enable(pluginName) if (success) { ElMessage.success(`插件 ${pluginName} 已启用`) } else { ElMessage.error('插件启用失败,请检查依赖关系') } } catch (error) { ElMessage.error(`启用插件时发生错误: ${error.message}`) } } // 禁用插件 const disablePlugin = async (pluginName: string) => { try { const success = await pluginManager.disable(pluginName) if (success) { ElMessage.success(`插件 ${pluginName} 已禁用`) } } catch (error) { ElMessage.error(`禁用插件时发生错误: ${error.message}`) } } // 传统方式(兼容性保持) const { disabled, enabled } = usePluginStore() // 启用插件 enabled('zhang-san/demo') // 停用插件 disabled('li-si/demo') ``` ### 插件热重载(开发环境) ```ts // 开发环境下支持插件热重载 if (import.meta.hot) { import.meta.hot.accept('./index.ts', (newModule) => { console.log('插件热重载中...') // 重新注册插件 pluginManager.unregister('zhang-san/file-manager') pluginManager.register('zhang-san/file-manager', newModule.default) console.log('插件热重载完成') }) } ``` ## 插件故障排查 ### 常见问题与解决方案 #### 1. 插件加载失败 **问题现象**: 插件在系统启动时不被识别或加载失败 **排查步骤**: 1. 检查插件目录结构是否正确 2. 确认 `index.ts` 文件存在且语法正确 3. 验证插件配置是否完整 4. 查看浏览器控制台错误信息 **解决方案**: ```ts // 插件诊断工具 const diagnosePlugin = (pluginName: string) => { console.group(`诊断插件: ${pluginName}`) // 检查插件是否存在 const plugin = pluginManager.getPlugin(pluginName) if (!plugin) { console.error('❌ 插件未找到,请检查目录结构') return false } // 检查必要配置 const requiredFields = ['config', 'install'] for (const field of requiredFields) { if (!(field in plugin)) { console.error(`❌ 缺少必要字段: ${field}`) return false } } console.log('✅ 插件配置检查通过') console.groupEnd() return true } ``` #### 2. 依赖关系错误 **问题现象**: 插件因依赖不满足而无法启动 **解决方案**: ```ts // 检查并安装缺失依赖 const fixDependencies = async (pluginName: string) => { const plugin = pluginManager.getPlugin(pluginName) const dependencies = plugin.config.info.dependencies || [] for (const dep of dependencies) { const [name, version] = dep.split('@') if (!pluginManager.getPlugin(name)) { console.warn(`缺少依赖: ${name}`) // 提示用户安装依赖 ElMessageBox.confirm( `插件 ${pluginName} 需要依赖 ${name},是否现在安装?`, '依赖确认', { type: 'warning' } ).then(() => { // 跳转到应用商店安装依赖 router.push(`/plugins/app-store?search=${name}`) }) } } } ``` #### 3. 性能问题 **问题现象**: 插件运行缓慢或占用资源过多 **解决方案**: ```ts // 性能监控工具 const monitorPluginPerformance = (pluginName: string) => { const metrics = { memory: 0, executionTime: new Map(), errorCount: 0 } // 监控内存使用 const checkMemory = () => { if (performance.memory) { metrics.memory = performance.memory.usedJSHeapSize } } // 监控函数执行时间 const wrapFunction = (obj: any, methodName: string) => { const originalMethod = obj[methodName] obj[methodName] = function(...args: any[]) { const start = performance.now() const result = originalMethod.apply(this, args) const end = performance.now() metrics.executionTime.set(methodName, end - start) if (end - start > 100) { // 超过100ms警告 console.warn(`${pluginName}.${methodName} 执行时间: ${(end - start).toFixed(2)}ms`) } return result } } return metrics } ``` ### 调试技巧 #### 1. 启用详细日志 ```ts // 在插件中添加详细日志 const debug = (message: string, data?: any) => { if (import.meta.env.DEV) { console.log(`[${pluginName}] ${message}`, data) } } hooks: { start(config) { debug('插件启动', config) return true }, setup() { debug('插件初始化完成') } } ``` #### 2. 使用开发者工具 ```ts // 暴露调试接口到浏览器控制台 if (import.meta.env.DEV) { window.__PLUGIN_DEBUG__ = { getPlugin: (name: string) => pluginManager.getPlugin(name), listPlugins: () => pluginManager.getAllPlugins(), enablePlugin: (name: string) => pluginManager.enable(name), disablePlugin: (name: string) => pluginManager.disable(name), reloadPlugin: (name: string) => { pluginManager.unregister(name) // 重新导入并注册插件 } } } ``` ## 插件生态系统 ### 官方插件 在 `src/plugins/mine-admin` 下是官方插件,目前内置了: #### `basic-ui` - 基础UI组件库 * **功能**: 提供系统基础UI组件和样式 * **版本**: 2.0.0+ * **依赖**: 无 * **说明**: 为其他插件提供统一的UI基础 ```ts // 使用基础UI组件 import { MButton, MCard, MTable } from 'mine-admin/basic-ui' // 在插件中使用 install(app) { // basic-ui 已经全局注册,可直接使用 app.component('CustomButton', { template: `` }) } ``` #### `app-store` - 应用市场 * **功能**: 插件商店,支持插件的安装、更新、卸载 * **版本**: 1.5.0+ * **依赖**: `mine-admin/basic-ui` * **说明**: 连接MineAdmin应用市场,管理第三方插件 #### `demo` - 演示插件 * **功能**: 展示插件系统各种功能的示例代码 * **版本**: 1.0.0+ * **依赖**: `mine-admin/basic-ui` * **说明**: 开发者参考示例,包含各种钩子使用方法 ### 第三方插件生态 #### 推荐插件类别 **文件管理类** * `file-manager/core` - 企业级文件管理 * `storage/cloud-sync` - 云存储同步 * `media/gallery` - 多媒体画廊 **数据处理类** * `data/excel-tools` - Excel处理工具 * `report/builder` - 报表构建器 * `chart/visualization` - 数据可视化 **系统增强类** * `theme/switcher` - 主题切换 * `security/two-fa` - 双因素认证 * `performance/optimizer` - 性能优化 ### 插件开发资源 * **官方文档**: * **插件模板**: * **开发工具**: [MineAdmin CLI](https://www.npmjs.com/package/@mineadmin/cli) * **社区论坛**: ## 总结 MineAdmin 3.0 的前端插件系统提供了企业级的扩展能力,通过本文档的指导,您可以: ### 核心优势 1. **零侵入设计** - 无需修改核心代码即可扩展功能 2. **类型安全** - 完整的 TypeScript 类型定义确保开发质量 3. **生命周期管理** - 丰富的钩子系统支持精细控制 4. **动态管理** - 支持插件的热插拔和状态管理 5. **性能优化** - 内置懒加载和资源管理机制 ### 开发建议 1. **遵循规范** - 使用标准的目录结构和命名规范 2. **注重测试** - 编写充分的单元测试和集成测试 3. **考虑性能** - 采用最佳实践避免内存泄漏和性能问题 4. **保障安全** - 实施输入验证和权限控制 5. **维护文档** - 为插件提供清晰的使用文档 ### 未来展望 插件系统将持续发展,计划支持更多特性: * **可视化插件编辑器** - 图形化配置插件 * **插件市场集成** - 一键安装和更新 * **云端同步** - 插件配置云端备份 * **AI辅助开发** - 智能代码生成和优化建议 ::: tip 最佳实践 建议开发者从简单的插件开始,逐步掌握系统的各项功能。参考官方演示插件的实现,在实践中不断改进和优化。 ::: ::: warning 兼容性提醒 在开发插件时,请注意 MineAdmin 版本兼容性,确保插件能在目标环境中稳定运行。建议定期关注系统更新,及时适配新版本特性。 ::: --- --- url: /backend/frameworks/hyperf/3.1/data-permission/troubleshooting.md --- # 故障排除 ## 常见问题诊断 ### 1. 权限策略不生效 **问题现象**: 用户设置了数据权限策略,但查询结果仍显示所有数据 **排查步骤**: #### 1.1 检查用户是否为超级管理员 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Factory.php:37-39 $user = User::find($userId); // 超级管理员会自动跳过所有数据权限检查 if ($user->isSuperAdmin()) { echo "用户是超级管理员,会绕过所有数据权限检查"; } ``` #### 1.2 检查用户策略配置 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Model/Permission/User.php:160-179 $user = User::find($userId); // 检查用户直接策略 $userPolicy = $user->policy()->first(); if ($userPolicy) { echo "用户策略存在:"; var_dump([ 'policy_type' => $userPolicy->policy_type, 'value' => $userPolicy->value, 'is_default' => $userPolicy->is_default ]); } else { echo "用户无直接策略,检查岗位策略:"; // 检查岗位策略 $user->load('position'); foreach ($user->position as $position) { $positionPolicy = $position->policy()->first(); if ($positionPolicy) { echo "找到岗位策略:"; var_dump([ 'position_id' => $position->id, 'position_name' => $position->name, 'policy_type' => $positionPolicy->policy_type, 'value' => $positionPolicy->value ]); break; } } } ``` #### 1.3 验证 DataScope 注解配置 确保方法有正确的 DataScope 注解: ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Service/Permission/UserService.php:94-98 use App\Library\DataPermission\Attribute\DataScope; use App\Library\DataPermission\ScopeType; class UserService { #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['user'], createdByColumn: 'id' )] public function page(array $params, int $page = 1, int $pageSize = 10): array { return parent::page($params, $page, $pageSize); } } ``` #### 1.4 检查数据权限上下文 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Context.php use App\Library\DataPermission\Context; // 检查当前上下文配置 $context = [ 'dept_column' => Context::getDeptColumn(), 'created_by_column' => Context::getCreatedByColumn(), 'scope_type' => Context::getScopeType(), 'only_tables' => Context::getOnlyTables() ]; var_dump($context); ``` ### 2. 协程上下文丢失 **问题现象**: 在协程中数据权限配置丢失 **解决方案**: 在新协程中重新设置上下文: ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Context.php use Hyperf\Utils\Coroutine; use App\Library\DataPermission\Context; use App\Library\DataPermission\ScopeType; // 正确的协程上下文处理 Coroutine::create(function () use ($userId) { // 在新协程中重新设置数据权限上下文 Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); Context::setScopeType(ScopeType::DEPT_CREATED_BY); Context::setOnlyTables(['user']); // 执行业务逻辑 $result = UserService::page($params); }); ``` ### 3. 查询性能问题 **问题现象**: 启用数据权限后查询速度明显变慢 #### 3.1 添加必要的数据库索引 ```sql -- 基础索引(根据实际表结构调整) CREATE INDEX idx_user_dept_id ON user(dept_id); CREATE INDEX idx_user_created_by ON user(created_by); CREATE INDEX idx_dept_parent_id ON department(parent_id); -- 数据权限策略相关索引 CREATE INDEX idx_policy_user_id ON data_permission_policy(user_id); CREATE INDEX idx_policy_position_id ON data_permission_policy(position_id); CREATE INDEX idx_policy_type ON data_permission_policy(policy_type); -- 组合索引优化复合查询 CREATE INDEX idx_user_dept_created ON user(dept_id, created_by); ``` #### 3.2 启用 SQL 查询日志进行分析 ```php // 在调试环境中启用查询日志 use Hyperf\Database\Model\Events\QueryExecuted; use Hyperf\Event\Annotation\Listener; use Hyperf\Event\Contract\ListenerInterface; #[Listener] class QueryListener implements ListenerInterface { public function listen(): array { return [QueryExecuted::class]; } public function process(object $event) { if ($event instanceof QueryExecuted) { // 记录慢查询(超过100ms) if ($event->time > 100) { Logger::warning('慢查询检测', [ 'sql' => $event->sql, 'bindings' => $event->bindings, 'time' => $event->time . 'ms' ]); } } } } ``` #### 3.3 优化部门树查询 ```php // 来源:优化 /Users/zhuzhu/project/mineadmin/app/Model/Permission/Department.php 的 getFlatChildren 方法 use Hyperf\Database\Model\Collection; use Hyperf\DbConnection\Db; class Department extends Model { // 使用递归 CTE 优化部门树查询 public function getFlatChildrenOptimized(): Collection { $sql = " WITH RECURSIVE dept_tree AS ( SELECT id, parent_id, name, 0 as level FROM department WHERE id = ? UNION ALL SELECT d.id, d.parent_id, d.name, dt.level + 1 FROM department d INNER JOIN dept_tree dt ON d.parent_id = dt.id WHERE dt.level < 10 -- 防止无限递归 ) SELECT * FROM dept_tree ORDER BY level, id "; $results = Db::select($sql, [$this->id]); return new Collection($results); } } ``` ### 4. 数据不一致问题 **问题现象**: 同一用户在不同时间查询到的数据不一致 #### 4.1 清除相关缓存 ```php // 如果使用了缓存,需要及时清除 use Hyperf\Cache\Cache; function clearDataPermissionCache(int $userId): void { $cache = ApplicationContext::getContainer()->get(Cache::class); // 清除用户策略缓存 $cache->delete("user_policy_{$userId}"); // 清除部门树缓存 $user = User::find($userId); if ($user && $user->department) { foreach ($user->department as $dept) { $cache->delete("dept_tree_{$dept->id}"); } } } ``` #### 4.2 强制从数据库重新加载 ```php // 强制从数据库重新加载用户及相关数据 $user = User::find($userId); $user->refresh(); // 刷新用户数据 $user->load(['policy', 'position.policy', 'department']); // 重新加载关联数据 // 获取最新的策略 $policy = $user->getPolicy(); ``` ### 5. AOP 切面不生效 **问题现象**: DataScope 注解不起作用 #### 5.1 检查 AOP 配置 ```php // 检查 config/autoload/annotations.php 中是否正确配置了 AOP return [ 'scan' => [ 'paths' => [ BASE_PATH . '/app', ], 'ignore_annotations' => [ 'mixin', ], 'class_map' => [], ], ]; ``` #### 5.2 确认切面类存在 ```php // 来源:确认 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Aspects/DataScopeAspect.php 文件存在 use App\Library\DataPermission\Aspects\DataScopeAspect; // 检查切面是否正确注册 if (class_exists(DataScopeAspect::class)) { echo "DataScopeAspect 类存在"; } else { echo "DataScopeAspect 类不存在,请检查文件路径"; } ``` ### 6. 调试方法 #### 6.1 启用调试模式 ```php // 在需要调试的地方添加日志 use Hyperf\Logger\LoggerFactory; use Hyperf\Utils\ApplicationContext; $logger = ApplicationContext::getContainer()->get(LoggerFactory::class)->get('data_permission'); // 记录当前用户信息 $logger->debug('数据权限调试', [ 'user_id' => $userId, 'is_super_admin' => $user->isSuperAdmin(), 'user_policy' => $user->policy ? $user->policy->toArray() : null, 'position_policies' => $user->position->map(function ($pos) { return $pos->policy ? $pos->policy->toArray() : null; })->filter()->toArray() ]); ``` #### 6.2 手动检查权限过滤 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Factory.php use App\Library\DataPermission\Factory; use App\Model\Permission\User; use Hyperf\Database\Query\Builder; function testDataPermission(int $userId): void { $user = User::find($userId); $query = User::query(); $builder = $query->getQuery(); // 手动应用数据权限过滤 $factory = new Factory(); $factory->build($builder, $user); // 输出生成的 SQL echo "生成的 SQL: " . $builder->toSql() . PHP_EOL; echo "绑定参数: " . json_encode($builder->getBindings()) . PHP_EOL; // 执行查询查看结果 $results = $query->get(); echo "查询结果数量: " . $results->count() . PHP_EOL; } ``` ### 7. 常用检查命令 创建一个简单的检查脚本: ```php // 创建文件:check_data_permission.php '用户不存在']; } $result = [ 'user_id' => $userId, 'is_super_admin' => $user->isSuperAdmin(), 'user_policy' => null, 'position_policies' => [], 'context' => [ 'dept_column' => Context::getDeptColumn(), 'created_by_column' => Context::getCreatedByColumn(), 'scope_type' => Context::getScopeType()?->value, 'only_tables' => Context::getOnlyTables() ] ]; // 检查用户策略 $userPolicy = $user->policy()->first(); if ($userPolicy) { $result['user_policy'] = $userPolicy->toArray(); } // 检查岗位策略 $user->load('position'); foreach ($user->position as $position) { $positionPolicy = $position->policy()->first(); if ($positionPolicy) { $result['position_policies'][] = [ 'position' => $position->toArray(), 'policy' => $positionPolicy->toArray() ]; } } return $result; } // 使用示例 // $status = checkDataPermissionStatus(1); // var_dump($status); ``` ## 总结 在排查 MineAdmin 数据权限问题时,建议按照以下顺序进行: 1. **检查超级管理员状态** - 超级管理员会绕过所有权限检查 2. **验证策略配置** - 确保用户或岗位有正确的权限策略 3. **检查注解配置** - 确认方法上的 DataScope 注解正确 4. **验证 AOP 是否生效** - 确认切面能够正常拦截方法调用 5. **检查协程上下文** - 在新协程中重新设置权限上下文 6. **分析查询性能** - 添加必要的数据库索引 7. **清除缓存** - 在数据变更后及时清除相关缓存 这些基于实际代码的诊断方法可以有效地解决大部分数据权限相关的问题。 --- --- url: /backend/frameworks/hyperf/3.2/data-permission/troubleshooting.md --- # 故障排除 ## 常见问题诊断 ### 1. 权限策略不生效 **问题现象**: 用户设置了数据权限策略,但查询结果仍显示所有数据 **排查步骤**: #### 1.1 检查用户是否为超级管理员 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Factory.php:37-39 $user = User::find($userId); // 超级管理员会自动跳过所有数据权限检查 if ($user->isSuperAdmin()) { echo "用户是超级管理员,会绕过所有数据权限检查"; } ``` #### 1.2 检查用户策略配置 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Model/Permission/User.php:160-179 $user = User::find($userId); // 检查用户直接策略 $userPolicy = $user->policy()->first(); if ($userPolicy) { echo "用户策略存在:"; var_dump([ 'policy_type' => $userPolicy->policy_type, 'value' => $userPolicy->value, 'is_default' => $userPolicy->is_default ]); } else { echo "用户无直接策略,检查岗位策略:"; // 检查岗位策略 $user->load('position'); foreach ($user->position as $position) { $positionPolicy = $position->policy()->first(); if ($positionPolicy) { echo "找到岗位策略:"; var_dump([ 'position_id' => $position->id, 'position_name' => $position->name, 'policy_type' => $positionPolicy->policy_type, 'value' => $positionPolicy->value ]); break; } } } ``` #### 1.3 验证 DataScope 注解配置 确保方法有正确的 DataScope 注解: ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Service/Permission/UserService.php:94-98 use App\Library\DataPermission\Attribute\DataScope; use App\Library\DataPermission\ScopeType; class UserService { #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['user'], createdByColumn: 'id' )] public function page(array $params, int $page = 1, int $pageSize = 10): array { return parent::page($params, $page, $pageSize); } } ``` #### 1.4 检查数据权限上下文 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Context.php use App\Library\DataPermission\Context; // 检查当前上下文配置 $context = [ 'dept_column' => Context::getDeptColumn(), 'created_by_column' => Context::getCreatedByColumn(), 'scope_type' => Context::getScopeType(), 'only_tables' => Context::getOnlyTables() ]; var_dump($context); ``` ### 2. 协程上下文丢失 **问题现象**: 在协程中数据权限配置丢失 **解决方案**: 在新协程中重新设置上下文: ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Context.php use Hyperf\Utils\Coroutine; use App\Library\DataPermission\Context; use App\Library\DataPermission\ScopeType; // 正确的协程上下文处理 Coroutine::create(function () use ($userId) { // 在新协程中重新设置数据权限上下文 Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); Context::setScopeType(ScopeType::DEPT_CREATED_BY); Context::setOnlyTables(['user']); // 执行业务逻辑 $result = UserService::page($params); }); ``` ### 3. 查询性能问题 **问题现象**: 启用数据权限后查询速度明显变慢 #### 3.1 添加必要的数据库索引 ```sql -- 基础索引(根据实际表结构调整) CREATE INDEX idx_user_dept_id ON user(dept_id); CREATE INDEX idx_user_created_by ON user(created_by); CREATE INDEX idx_dept_parent_id ON department(parent_id); -- 数据权限策略相关索引 CREATE INDEX idx_policy_user_id ON data_permission_policy(user_id); CREATE INDEX idx_policy_position_id ON data_permission_policy(position_id); CREATE INDEX idx_policy_type ON data_permission_policy(policy_type); -- 组合索引优化复合查询 CREATE INDEX idx_user_dept_created ON user(dept_id, created_by); ``` #### 3.2 启用 SQL 查询日志进行分析 ```php // 在调试环境中启用查询日志 use Hyperf\Database\Model\Events\QueryExecuted; use Hyperf\Event\Annotation\Listener; use Hyperf\Event\Contract\ListenerInterface; #[Listener] class QueryListener implements ListenerInterface { public function listen(): array { return [QueryExecuted::class]; } public function process(object $event) { if ($event instanceof QueryExecuted) { // 记录慢查询(超过100ms) if ($event->time > 100) { Logger::warning('慢查询检测', [ 'sql' => $event->sql, 'bindings' => $event->bindings, 'time' => $event->time . 'ms' ]); } } } } ``` #### 3.3 优化部门树查询 ```php // 来源:优化 /Users/zhuzhu/project/mineadmin/app/Model/Permission/Department.php 的 getFlatChildren 方法 use Hyperf\Database\Model\Collection; use Hyperf\DbConnection\Db; class Department extends Model { // 使用递归 CTE 优化部门树查询 public function getFlatChildrenOptimized(): Collection { $sql = " WITH RECURSIVE dept_tree AS ( SELECT id, parent_id, name, 0 as level FROM department WHERE id = ? UNION ALL SELECT d.id, d.parent_id, d.name, dt.level + 1 FROM department d INNER JOIN dept_tree dt ON d.parent_id = dt.id WHERE dt.level < 10 -- 防止无限递归 ) SELECT * FROM dept_tree ORDER BY level, id "; $results = Db::select($sql, [$this->id]); return new Collection($results); } } ``` ### 4. 数据不一致问题 **问题现象**: 同一用户在不同时间查询到的数据不一致 #### 4.1 清除相关缓存 ```php // 如果使用了缓存,需要及时清除 use Hyperf\Cache\Cache; function clearDataPermissionCache(int $userId): void { $cache = ApplicationContext::getContainer()->get(Cache::class); // 清除用户策略缓存 $cache->delete("user_policy_{$userId}"); // 清除部门树缓存 $user = User::find($userId); if ($user && $user->department) { foreach ($user->department as $dept) { $cache->delete("dept_tree_{$dept->id}"); } } } ``` #### 4.2 强制从数据库重新加载 ```php // 强制从数据库重新加载用户及相关数据 $user = User::find($userId); $user->refresh(); // 刷新用户数据 $user->load(['policy', 'position.policy', 'department']); // 重新加载关联数据 // 获取最新的策略 $policy = $user->getPolicy(); ``` ### 5. AOP 切面不生效 **问题现象**: DataScope 注解不起作用 #### 5.1 检查 AOP 配置 ```php // 检查 config/autoload/annotations.php 中是否正确配置了 AOP return [ 'scan' => [ 'paths' => [ BASE_PATH . '/app', ], 'ignore_annotations' => [ 'mixin', ], 'class_map' => [], ], ]; ``` #### 5.2 确认切面类存在 ```php // 来源:确认 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Aspects/DataScopeAspect.php 文件存在 use App\Library\DataPermission\Aspects\DataScopeAspect; // 检查切面是否正确注册 if (class_exists(DataScopeAspect::class)) { echo "DataScopeAspect 类存在"; } else { echo "DataScopeAspect 类不存在,请检查文件路径"; } ``` ### 6. 调试方法 #### 6.1 启用调试模式 ```php // 在需要调试的地方添加日志 use Hyperf\Logger\LoggerFactory; use Hyperf\Utils\ApplicationContext; $logger = ApplicationContext::getContainer()->get(LoggerFactory::class)->get('data_permission'); // 记录当前用户信息 $logger->debug('数据权限调试', [ 'user_id' => $userId, 'is_super_admin' => $user->isSuperAdmin(), 'user_policy' => $user->policy ? $user->policy->toArray() : null, 'position_policies' => $user->position->map(function ($pos) { return $pos->policy ? $pos->policy->toArray() : null; })->filter()->toArray() ]); ``` #### 6.2 手动检查权限过滤 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Factory.php use App\Library\DataPermission\Factory; use App\Model\Permission\User; use Hyperf\Database\Query\Builder; function testDataPermission(int $userId): void { $user = User::find($userId); $query = User::query(); $builder = $query->getQuery(); // 手动应用数据权限过滤 $factory = new Factory(); $factory->build($builder, $user); // 输出生成的 SQL echo "生成的 SQL: " . $builder->toSql() . PHP_EOL; echo "绑定参数: " . json_encode($builder->getBindings()) . PHP_EOL; // 执行查询查看结果 $results = $query->get(); echo "查询结果数量: " . $results->count() . PHP_EOL; } ``` ### 7. 常用检查命令 创建一个简单的检查脚本: ```php // 创建文件:check_data_permission.php '用户不存在']; } $result = [ 'user_id' => $userId, 'is_super_admin' => $user->isSuperAdmin(), 'user_policy' => null, 'position_policies' => [], 'context' => [ 'dept_column' => Context::getDeptColumn(), 'created_by_column' => Context::getCreatedByColumn(), 'scope_type' => Context::getScopeType()?->value, 'only_tables' => Context::getOnlyTables() ] ]; // 检查用户策略 $userPolicy = $user->policy()->first(); if ($userPolicy) { $result['user_policy'] = $userPolicy->toArray(); } // 检查岗位策略 $user->load('position'); foreach ($user->position as $position) { $positionPolicy = $position->policy()->first(); if ($positionPolicy) { $result['position_policies'][] = [ 'position' => $position->toArray(), 'policy' => $positionPolicy->toArray() ]; } } return $result; } // 使用示例 // $status = checkDataPermissionStatus(1); // var_dump($status); ``` ## 总结 在排查 MineAdmin 数据权限问题时,建议按照以下顺序进行: 1. **检查超级管理员状态** - 超级管理员会绕过所有权限检查 2. **验证策略配置** - 确保用户或岗位有正确的权限策略 3. **检查注解配置** - 确认方法上的 DataScope 注解正确 4. **验证 AOP 是否生效** - 确认切面能够正常拦截方法调用 5. **检查协程上下文** - 在新协程中重新设置权限上下文 6. **分析查询性能** - 添加必要的数据库索引 7. **清除缓存** - 在数据变更后及时清除相关缓存 这些基于实际代码的诊断方法可以有效地解决大部分数据权限相关的问题。 --- --- url: /v3/plugin/backend/migrate.md --- # 数据库迁移 数据库迁移文件说明 *** 1. 插件默认使用 [Migration](https://hyperf.wiki/3.1/#/zh-cn/db/migration){style="color: green;"}来管理插件的数据迁移和填充. 2. `非必要情况` 请全部以迁移文件以及填充文件来进行数据迁移. `InstallScript` 以及 `UninstallScript` 做其他检测或者文件迁移 3. 插件数据库迁移目录以[插件目录规范章节](../structure.md){style="color: green;"}为准 --- --- url: /v3/backend/data-permission/example.md --- # 数据权限 API 参考与高级用法 本文已迁移到 [Hyperf 数据权限 API 参考与高级用法](/backend/frameworks/hyperf/3.2/data-permission/example)。 Laravel 实现暂未提供数据权限章节。旧地址保留用于兼容历史链接。 --- --- url: /v3/backend/data-permission/performance.md --- # 数据权限性能优化指南 本文已迁移到 [Hyperf 数据权限性能优化指南](/backend/frameworks/hyperf/3.2/data-permission/performance)。 Laravel 实现暂未提供数据权限章节。旧地址保留用于兼容历史链接。 --- --- url: /v3/backend/data-permission/troubleshooting.md --- # 数据权限故障排除指南 本文已迁移到 [Hyperf 数据权限故障排除指南](/backend/frameworks/hyperf/3.2/data-permission/troubleshooting)。 Laravel 实现暂未提供数据权限章节。旧地址保留用于兼容历史链接。 --- --- url: /v3/backend/data-permission/architecture.md --- # 数据权限架构设计 本文已迁移到 [Hyperf 数据权限架构设计](/backend/frameworks/hyperf/3.2/data-permission/architecture)。 Laravel 实现暂未提供数据权限章节。旧地址保留用于兼容历史链接。 --- --- url: /v3/backend/data-permission/overview.md --- # 数据权限核心概念 本文已迁移到 [Hyperf 数据权限核心概念](/backend/frameworks/hyperf/3.2/data-permission/overview)。 Laravel 实现暂未提供数据权限章节。旧地址保留用于兼容历史链接。 --- --- url: /v3/backend/data-permission/notice.md --- # 数据权限注意事项与最佳实践 本文已迁移到 [Hyperf 数据权限注意事项与最佳实践](/backend/frameworks/hyperf/3.2/data-permission/notice)。 Laravel 实现暂未提供数据权限章节。旧地址保留用于兼容历史链接。 --- --- url: /backend/frameworks/hyperf/3.1/data-permission/config.md --- # 数据权限配置与使用示例 本文将讲解在数据权限配置中各种策略的配置与使用方式 ## 数据隔离方式 数据隔离目前仅支持行级隔离,但是支持多种隔离策略。 主要分为以创建人、所属部门为依据的隔离方式。 * `部门`隔离以用户当前所属部门为依据,查询数据时会自动添加部门过滤条件。 * `创建人`隔离以数据创建人作为依据,查询数据时会自动添加创建人过滤条件。 ## 优先级 目前支持`对指定用户设置隔离策略`,`为用户指定岗位,对岗位设置隔离策略`两种方式 如果用户同时设置了隔离策略和岗位隔离策略,则会优先使用对指定用户设置的隔离策略。 ```plantuml @startuml title 获取数据隔离策略 start :获取当前用户策略; if (用户有隔离策略) then (yes) :返回用户隔离策略; else (no) :获取岗位隔离策略; if (岗位有隔离策略) then (yes) :返回岗位隔离策略; endif endif if(如果找不到隔离策略) then (yes) :返回空策略; endif end @enduml ``` 逻辑代码为 ```php // /mineadmin/app/Model/Permission/User.php:160-179 public function getPolicy(): ?Policy { /** * @var null|Policy $policy */ $policy = $this->policy()->first(); if (! empty($policy)) { return $policy; } $this->load('position'); $positionList = $this->position; foreach ($positionList as $position) { $current = $position->policy()->first(); if (! empty($current)) { return $current; } } return null; } ``` ## 示例 以现在的表 `user` 为隔离表,假设有以下数据: ### 示例数据 部门表 *** | id | name | parent\_id | |----|------|-----------| | 1 | 部门1 | 0 | | 2 | 部门2 | 1 | | 3 | 部门3 | 0 | 部门 1 是顶级部门,没有父部门。 部门 2 属于部门 1 的子部门。 部门 3 是顶级部门,没有父部门。 *** 岗位表 | id | name | dept\_id | |----|------|---------| | 1 | 岗位1 | 1 | | 2 | 岗位2 | 2 | | 3 | 岗位3 | 3 | 部门 1 有岗位 1,部门 2 有岗位 2,部门 3 有岗位 3。 *** 用户表 | id | name | dept\_id | created\_by | post\_id | |----|-------|---------|------------|---------| | 1 | 超级管理员 | 0 | 0 | 0 | | 2 | a1 | 1 | 1 | 1 | | 3 | a2 | 2 | 1 | 1 | | 4 | a3 | 1 | 2 | 2 | | 5 | a4 | 2 | 2 | 0 | | 6 | a5 | 0 | 4 | 0 | 用户表中,`dept_id` 为 0 的用户标识没有部门,`created_by` 为 0 的用户标识没有创建人。 超管员可以看到所有数据。 a1、a3 属于部门1,a2、a4 属于部门2。 a1、a2 的创建人是超级管理员,a3、a4 的创建人是 a1。 a1、a2 的岗位是岗位1,a3 的岗位是岗位2,a4 没有岗位。 以下举一些例子,讲明在不同策略中数据的查询结果。 ### PolicyType::SELF `仅查询自己` 假设当前用户 id 为 2 的 a1 用户,设置了仅查询自己策略。 1. 隔离方式为仅根据创建人隔离。 则会拼接查询条件为 `创建人为当前用户 id`,也就是查询到用户 a3、a4。 ```sql SELECT * FROM user WHERE created_by in (4,5); ``` 2. 隔离方式为仅根据部门隔离,则会拼接查询条件为 `部门为当前用户所在部门`,也就是查询到用户 a1、a3。 ```sql SELECT * FROM user WHERE dept_id in(1); ``` 3. 隔离方式为根据创建人和部门隔离,则会拼接查询条件为 `创建人为当前用户 id` 并且 `部门为当前用户所在部门`,也就是查询到用户 a3。 ```sql SELECT * FROM user WHERE created_by in(2) AND dept_id in(1); ``` 4. 隔离方式为根据部门 or 创建人过滤,则会拼接查询条件为 `创建人为当前用户 id` 或者 `部门为当前用户所在部门`,也就是查询到用户 a1、a3、a4。 ```sql SELECT * FROM user WHERE dept_id in(1) OR created_by in(2); ``` ### PolicyType::DEPT\_SELF `仅查询本部门` 假设当前用户 id 为 2 的 a1 用户,设置了仅查询本部门策略。 1. 隔离方式为仅根据创建人隔离。则会拼接查询条件为 `创建人为当前用户同属部门下的所有用户 id`,也就是查询到用户 a3、a4、a5。 ```sql SELECT * FROM user WHERE created_by in (2,4,5); ``` 2. 隔离方式为仅根据部门隔离。则会拼接查询条件为 `部门为当前用户所在部门`,也就是查询到用户 a1、a3。 ```sql SELECT * FROM user WHERE dept_id in(1); ``` 3. 隔离方式为根据创建人和部门隔离。则会拼接查询条件为 `创建人为当前用户同属部门下的所有用户 id` 并且 `部门为当前用户所在部门`,也就是查询到用户 a3。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) AND dept_id in(1); ``` 4. 隔离方式为根据部门 or 创建人过滤。则会拼接查询条件为 `创建人为当前用户同属部门下的所有用户 id` 或者 `部门为当前用户所在部门`,也就是查询到用户 a1、a3、a4、a5。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) OR dept_id in(1); ``` ### PolicyType::DEPT\_TREE `查询本部门及子部门` 假设当前用户 id 为 2 的 a1 用户,设置了查询本部门及子部门策略。 1. 隔离方式为仅根据创建人隔离。则会拼接查询条件为 `创建人为当前用户同属部门以及下级部门的所有用户 id`,也就是查询到用户 a3、a4、a5。 ```sql SELECT * FROM user WHERE created_by in (2,4,5); ``` 2. 隔离方式为仅根据部门隔离。则会拼接查询条件为 `部门为当前用户所在部门及下级部门`,也就是查询到用户 a1、a2、a3、a4。 ```sql SELECT * FROM user WHERE dept_id in(1,2); ``` 3. 隔离方式为根据创建人和部门隔离。则会拼接查询条件为 `创建人为当前用户同属部门以及下级部门的所有用户 id` 并且 `部门为当前用户所在部门及下级部门`,也就是查询到用户 a3、a4。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) AND dept_id in(1,2); ``` 4. 隔离方式为根据部门 or 创建人过滤。则会拼接查询条件为 `创建人为当前用户同属部门以及下级部门的所有用户 id` 或者 `部门为当前用户所在部门及下级部门`,也就是查询到用户 a1、a2、a3、a4、a5。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) OR dept_id in(1,2); ``` ### PolicyType::ALL `查询所有` 假设当前用户 id 为 2 的 a1 用户,设置了查询所有策略。则会取消所有限制 ### PolicyType::CUSTOM\_DEPT `自定义部门` 假设当前用户 id 为 2 的 a1 用户,设置了只能查看部门 2 和 3 的数据。 1. 隔离方式为仅根据创建人隔离。则会拼接查询条件为 `创建人所属部门为 2 和 3 的所有用户 id`,也就是查询到用户 a2、a4、a5。 ```sql SELECT * FROM user WHERE created_by in (2,4,5); ``` 2. 隔离方式为仅根据部门隔离。则会拼接查询条件为 `部门为 2 和 3`,也就是查询到用户 a2、a4。 ```sql SELECT * FROM user WHERE dept_id in(2,3); ``` 3. 隔离方式为根据创建人和部门隔离。则会拼接查询条件为 `创建人所属部门为 2 和 3 的所有用户 id` 并且 `部门为 2 和 3`,也就是查询到用户 a2、a4。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) AND dept_id in(2,3); ``` 4. 隔离方式为根据部门 or 创建人过滤。则会拼接查询条件为 `创建人所属部门为 2 和 3 的所有用户 id` 或者 `部门为 2 和 3`,也就是查询到用户 a2、a4、a5。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) OR dept_id in(2,3); ``` ### PolicyType::CUSTOM\_FUNC `自定义函数` 假设当前用户 id 为 2 的 a1 用户,设置了自定义函 `testction` 的策略 在 `/Users/zhuzhu/project/mineadmin/config/autoload/department/custom.php` 中定义了自定义函数 `testction`: ```php // /mineadmin/config/autoload/department/custom.php return [ 'testction' => function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) { // 只针对 id 为 2 的用户生效 if ($user->id !== 2) { return; } // 获取当前上下文中的创建人字段名称 $createdByColumn = Context::getCreatedByColumn(); // 获取当前上下文中的部门字段名称 $deptColumn = Context::getDeptColumn(); switch ($scopeType){ // 隔离类型为根据创建人 case ScopeType::CREATED_BY: // 创建人字段为当前用户 $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT: // 部门字段为当前用户部门 $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); break; case ScopeType::DEPT_CREATED_BY: // 部门字段为当前用户部门 $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); // 创建人为当前用户 $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT_OR_CREATED_BY: // 部门字段为当前用户部门 $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); // 创建人为当前用户 $builder->orWhere($createdByColumn, $user->id); break; } } ]; ``` 则会在隔离生效时将当前上下文中的用户、隔离方式、权限策略传入自定义函数 `testction` 中进行处理。 以便开发者自定义复杂的隔离逻辑。 --- --- url: /backend/frameworks/hyperf/3.2/data-permission/config.md --- # 数据权限配置与使用示例 本文将讲解在数据权限配置中各种策略的配置与使用方式 ## 数据隔离方式 数据隔离目前仅支持行级隔离,但是支持多种隔离策略。 主要分为以创建人、所属部门为依据的隔离方式。 * `部门`隔离以用户当前所属部门为依据,查询数据时会自动添加部门过滤条件。 * `创建人`隔离以数据创建人作为依据,查询数据时会自动添加创建人过滤条件。 ## 优先级 目前支持`对指定用户设置隔离策略`,`为用户指定岗位,对岗位设置隔离策略`两种方式 如果用户同时设置了隔离策略和岗位隔离策略,则会优先使用对指定用户设置的隔离策略。 ```plantuml @startuml title 获取数据隔离策略 start :获取当前用户策略; if (用户有隔离策略) then (yes) :返回用户隔离策略; else (no) :获取岗位隔离策略; if (岗位有隔离策略) then (yes) :返回岗位隔离策略; endif endif if(如果找不到隔离策略) then (yes) :返回空策略; endif end @enduml ``` 逻辑代码为 ```php // /mineadmin/app/Model/Permission/User.php:160-179 public function getPolicy(): ?Policy { /** * @var null|Policy $policy */ $policy = $this->policy()->first(); if (! empty($policy)) { return $policy; } $this->load('position'); $positionList = $this->position; foreach ($positionList as $position) { $current = $position->policy()->first(); if (! empty($current)) { return $current; } } return null; } ``` ## 示例 以现在的表 `user` 为隔离表,假设有以下数据: ### 示例数据 部门表 *** | id | name | parent\_id | |----|------|-----------| | 1 | 部门1 | 0 | | 2 | 部门2 | 1 | | 3 | 部门3 | 0 | 部门 1 是顶级部门,没有父部门。 部门 2 属于部门 1 的子部门。 部门 3 是顶级部门,没有父部门。 *** 岗位表 | id | name | dept\_id | |----|------|---------| | 1 | 岗位1 | 1 | | 2 | 岗位2 | 2 | | 3 | 岗位3 | 3 | 部门 1 有岗位 1,部门 2 有岗位 2,部门 3 有岗位 3。 *** 用户表 | id | name | dept\_id | created\_by | post\_id | |----|-------|---------|------------|---------| | 1 | 超级管理员 | 0 | 0 | 0 | | 2 | a1 | 1 | 1 | 1 | | 3 | a2 | 2 | 1 | 1 | | 4 | a3 | 1 | 2 | 2 | | 5 | a4 | 2 | 2 | 0 | | 6 | a5 | 0 | 4 | 0 | 用户表中,`dept_id` 为 0 的用户标识没有部门,`created_by` 为 0 的用户标识没有创建人。 超管员可以看到所有数据。 a1、a3 属于部门1,a2、a4 属于部门2。 a1、a2 的创建人是超级管理员,a3、a4 的创建人是 a1。 a1、a2 的岗位是岗位1,a3 的岗位是岗位2,a4 没有岗位。 以下举一些例子,讲明在不同策略中数据的查询结果。 ### PolicyType::SELF `仅查询自己` 假设当前用户 id 为 2 的 a1 用户,设置了仅查询自己策略。 1. 隔离方式为仅根据创建人隔离。 则会拼接查询条件为 `创建人为当前用户 id`,也就是查询到用户 a3、a4。 ```sql SELECT * FROM user WHERE created_by in (4,5); ``` 2. 隔离方式为仅根据部门隔离,则会拼接查询条件为 `部门为当前用户所在部门`,也就是查询到用户 a1、a3。 ```sql SELECT * FROM user WHERE dept_id in(1); ``` 3. 隔离方式为根据创建人和部门隔离,则会拼接查询条件为 `创建人为当前用户 id` 并且 `部门为当前用户所在部门`,也就是查询到用户 a3。 ```sql SELECT * FROM user WHERE created_by in(2) AND dept_id in(1); ``` 4. 隔离方式为根据部门 or 创建人过滤,则会拼接查询条件为 `创建人为当前用户 id` 或者 `部门为当前用户所在部门`,也就是查询到用户 a1、a3、a4。 ```sql SELECT * FROM user WHERE dept_id in(1) OR created_by in(2); ``` ### PolicyType::DEPT\_SELF `仅查询本部门` 假设当前用户 id 为 2 的 a1 用户,设置了仅查询本部门策略。 1. 隔离方式为仅根据创建人隔离。则会拼接查询条件为 `创建人为当前用户同属部门下的所有用户 id`,也就是查询到用户 a3、a4、a5。 ```sql SELECT * FROM user WHERE created_by in (2,4,5); ``` 2. 隔离方式为仅根据部门隔离。则会拼接查询条件为 `部门为当前用户所在部门`,也就是查询到用户 a1、a3。 ```sql SELECT * FROM user WHERE dept_id in(1); ``` 3. 隔离方式为根据创建人和部门隔离。则会拼接查询条件为 `创建人为当前用户同属部门下的所有用户 id` 并且 `部门为当前用户所在部门`,也就是查询到用户 a3。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) AND dept_id in(1); ``` 4. 隔离方式为根据部门 or 创建人过滤。则会拼接查询条件为 `创建人为当前用户同属部门下的所有用户 id` 或者 `部门为当前用户所在部门`,也就是查询到用户 a1、a3、a4、a5。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) OR dept_id in(1); ``` ### PolicyType::DEPT\_TREE `查询本部门及子部门` 假设当前用户 id 为 2 的 a1 用户,设置了查询本部门及子部门策略。 1. 隔离方式为仅根据创建人隔离。则会拼接查询条件为 `创建人为当前用户同属部门以及下级部门的所有用户 id`,也就是查询到用户 a3、a4、a5。 ```sql SELECT * FROM user WHERE created_by in (2,4,5); ``` 2. 隔离方式为仅根据部门隔离。则会拼接查询条件为 `部门为当前用户所在部门及下级部门`,也就是查询到用户 a1、a2、a3、a4。 ```sql SELECT * FROM user WHERE dept_id in(1,2); ``` 3. 隔离方式为根据创建人和部门隔离。则会拼接查询条件为 `创建人为当前用户同属部门以及下级部门的所有用户 id` 并且 `部门为当前用户所在部门及下级部门`,也就是查询到用户 a3、a4。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) AND dept_id in(1,2); ``` 4. 隔离方式为根据部门 or 创建人过滤。则会拼接查询条件为 `创建人为当前用户同属部门以及下级部门的所有用户 id` 或者 `部门为当前用户所在部门及下级部门`,也就是查询到用户 a1、a2、a3、a4、a5。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) OR dept_id in(1,2); ``` ### PolicyType::ALL `查询所有` 假设当前用户 id 为 2 的 a1 用户,设置了查询所有策略。则会取消所有限制 ### PolicyType::CUSTOM\_DEPT `自定义部门` 假设当前用户 id 为 2 的 a1 用户,设置了只能查看部门 2 和 3 的数据。 1. 隔离方式为仅根据创建人隔离。则会拼接查询条件为 `创建人所属部门为 2 和 3 的所有用户 id`,也就是查询到用户 a2、a4、a5。 ```sql SELECT * FROM user WHERE created_by in (2,4,5); ``` 2. 隔离方式为仅根据部门隔离。则会拼接查询条件为 `部门为 2 和 3`,也就是查询到用户 a2、a4。 ```sql SELECT * FROM user WHERE dept_id in(2,3); ``` 3. 隔离方式为根据创建人和部门隔离。则会拼接查询条件为 `创建人所属部门为 2 和 3 的所有用户 id` 并且 `部门为 2 和 3`,也就是查询到用户 a2、a4。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) AND dept_id in(2,3); ``` 4. 隔离方式为根据部门 or 创建人过滤。则会拼接查询条件为 `创建人所属部门为 2 和 3 的所有用户 id` 或者 `部门为 2 和 3`,也就是查询到用户 a2、a4、a5。 ```sql SELECT * FROM user WHERE created_by in(2,4,5) OR dept_id in(2,3); ``` ### PolicyType::CUSTOM\_FUNC `自定义函数` 假设当前用户 id 为 2 的 a1 用户,设置了自定义函 `testction` 的策略 在 `/Users/zhuzhu/project/mineadmin/config/autoload/department/custom.php` 中定义了自定义函数 `testction`: ```php // /mineadmin/config/autoload/department/custom.php return [ 'testction' => function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) { // 只针对 id 为 2 的用户生效 if ($user->id !== 2) { return; } // 获取当前上下文中的创建人字段名称 $createdByColumn = Context::getCreatedByColumn(); // 获取当前上下文中的部门字段名称 $deptColumn = Context::getDeptColumn(); switch ($scopeType){ // 隔离类型为根据创建人 case ScopeType::CREATED_BY: // 创建人字段为当前用户 $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT: // 部门字段为当前用户部门 $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); break; case ScopeType::DEPT_CREATED_BY: // 部门字段为当前用户部门 $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); // 创建人为当前用户 $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT_OR_CREATED_BY: // 部门字段为当前用户部门 $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); // 创建人为当前用户 $builder->orWhere($createdByColumn, $user->id); break; } } ]; ``` 则会在隔离生效时将当前上下文中的用户、隔离方式、权限策略传入自定义函数 `testction` 中进行处理。 以便开发者自定义复杂的隔离逻辑。 --- --- url: /v3/backend/data-permission/config.md --- # 数据权限配置与效果演示 本文已迁移到 [Hyperf 数据权限配置与效果演示](/backend/frameworks/hyperf/3.2/data-permission/config)。 Laravel 实现暂未提供数据权限章节。旧地址保留用于兼容历史链接。 --- --- url: /v3/backend/contracts/data-model.md --- # 数据模型契约 数据模型契约定义 MineAdmin 后端实现之间共享的核心实体、字段语义和关联关系。不同框架可以使用不同 ORM 或数据访问方式,但对外暴露给前台模板、权限系统、接口元数据和审计日志的模型含义需要保持一致。 本文依据 MineAdmin 当前迁移文件和模型关系整理,重点覆盖管理后台稳定依赖的实体。 ## 核心实体 | 实体 | 表 | 作用 | |------|----|------| | 用户 | `user` | 后台账号主体,保存登录凭据、用户类型、昵称、联系方式、头像、状态、最后登录 IP/时间、后台个人设置和审计创建/更新人。 | | 角色 | `role` | 权限分组主体,保存角色名称、唯一角色代码、状态和排序;用户通过角色获得菜单、按钮和接口权限。 | | 菜单/权限 | `menu` | 前台路由、菜单树和权限标识的统一载体;`parent_id` 组织菜单层级,`path`、`component`、`redirect` 和 `meta` 支撑前台渲染,`name` 参与权限判断。 | | 部门 | `department` | 组织树节点,通过 `parent_id` 表达上下级部门;用于用户归属、部门负责人和数据权限范围计算。 | | 岗位 | `position` | 部门下的岗位节点,通过 `dept_id` 归属部门;用户可以关联多个岗位,岗位也可以承载数据权限策略。 | | 数据权限策略 | `data_permission_policy` | 数据范围控制规则,按当前迁移通过 `user_id` 或 `position_id` 绑定到用户或岗位,使用 `policy_type` 和 `value` 描述部门、自定义范围或函数规则。 | | 附件 | `attachment` | 上传文件索引,记录存储模式、原始文件名、对象名、哈希、MIME、存储路径、后缀、大小、访问 URL 和审计创建/更新人;前台文件选择、预览和下载能力依赖该实体。 | | 登录日志 | `user_login_log` | 登录审计记录,保存用户名、登录 IP、操作系统、浏览器、登录状态、提示消息和登录时间,用于安全审计和登录轨迹查询。 | | 操作日志 | `user_operation_log` | 操作审计记录,保存用户名、请求方法、路由、业务名称、请求 IP 和时间信息,用于追踪后台功能调用和问题排查。 | ## 关联表 | 关联表 | 关系 | 说明 | |--------|------|------| | `user_belongs_role` | 用户 N:N 角色 | 一个用户可以拥有多个角色,一个角色可以分配给多个用户。 | | `role_belongs_menu` | 角色 N:N 菜单 | 一个角色可以拥有多个菜单/权限,一个菜单/权限也可以授权给多个角色。 | | `user_dept` | 用户 N:N 部门 | 一个用户可以归属多个部门,用于组织展示和数据权限上下文。 | | `user_position` | 用户 N:N 岗位 | 一个用户可以拥有多个岗位,岗位策略可作为用户策略的后备来源。 | | `dept_leader` | 部门 N:N 负责人用户 | 一个部门可以配置多个负责人,一个用户也可以负责多个部门。 | ## 关系约定 * 用户与角色通过 `user_belongs_role` 多对多关联;用户权限由角色关联的菜单/权限集合汇总而来。 * 角色与菜单通过 `role_belongs_menu` 多对多关联;菜单树由 `menu.parent_id` 自关联构成。 * 用户与部门通过 `user_dept` 多对多关联;部门树由 `department.parent_id` 自关联构成。 * 岗位通过 `position.dept_id` 归属部门,用户与岗位通过 `user_position` 多对多关联。 * 部门负责人通过 `dept_leader` 关联用户和部门,不等同于普通部门归属。 * 数据权限策略按当前迁移使用 `data_permission_policy.user_id` 或 `position_id` 绑定;用户读取策略时优先使用用户策略,没有用户策略时再检查用户岗位上的策略。 * 附件、登录日志、操作日志是支撑型实体,不改变权限主链路,但前台和后台接口应保持字段语义稳定。 * 登录日志和操作日志按 `username` 建索引,不包含 `user_id` 字段;它们与用户实体是审计追踪关系,不是数据库外键关系。 * 图中实线表示模型关系或关联表关系,点线表示审计字段形成的逻辑追踪关系。 ## ER 图 ```plantuml @startuml hide circle skinparam linetype ortho entity "user\n用户" as user { * id : bigint -- username : string password : string user_type : string nickname : string phone : string email : string avatar : string signed : string status : tinyint login_ip : ip login_time : timestamp backend_setting : json } entity "role\n角色" as role { * id : bigint -- name : string code : string status : tinyint sort : smallint } entity "menu\n菜单/权限" as menu { * id : bigint -- parent_id : bigint name : string meta : json path : string component : string redirect : string status : tinyint sort : smallint } entity "department\n部门" as department { * id : bigint -- name : string parent_id : bigint deleted_at : timestamp } entity "position\n岗位" as position { * id : bigint -- name : string dept_id : bigint deleted_at : timestamp } entity "data_permission_policy\n数据权限策略" as policy { * id : bigint -- user_id : bigint position_id : bigint policy_type : string is_default : boolean value : json deleted_at : timestamp } entity "attachment\n附件" as attachment { * id : bigint -- storage_mode : string origin_name : string object_name : string hash : string mime_type : string storage_path : string suffix : string size_byte : bigint url : string } entity "user_login_log\n登录日志" as login_log { * id : bigint -- username : string ip : ip os : string browser : string status : smallint message : string login_time : datetime } entity "user_operation_log\n操作日志" as operation_log { * id : bigint -- username : string method : string router : string service_name : string ip : ip } entity "user_belongs_role" as user_role { * id : bigint -- user_id : bigint role_id : bigint } entity "role_belongs_menu" as role_menu { * id : bigint -- role_id : bigint menu_id : bigint } entity "user_dept" as user_dept { user_id : bigint dept_id : bigint } entity "user_position" as user_position { user_id : bigint position_id : bigint } entity "dept_leader" as dept_leader { dept_id : bigint user_id : bigint } user ||--o{ user_role role ||--o{ user_role role ||--o{ role_menu menu ||--o{ role_menu menu ||--o{ menu : parent_id department ||--o{ department : parent_id department ||--o{ position : dept_id user ||--o{ user_dept department ||--o{ user_dept user ||--o{ user_position position ||--o{ user_position user ||--o{ dept_leader department ||--o{ dept_leader user ||--o{ policy : user_id position ||--o{ policy : position_id user ||..o{ attachment : created_by user ||..o{ login_log : username user ||..o{ operation_log : username @enduml ``` --- --- url: /backend/frameworks/hyperf/3.1/base/upload.md --- # 文件上传 ## 后端上传 ::: tip 文件上传由 MineAdmin 接入 [mineadmin/upload](https://github.com/mineadmin/upload) 组件建设而成 ::: MineAdmin 提供了一个默认的服务端文件上传逻辑。接口 `/admin/attachment/upload` 并且在上传成功后接入了资源管理器。 ::: code-group ```php [ControllerUpload] #[Post( path: '/admin/attachment/upload', operationId: 'UploadAttachment', summary: '上传附件', security: [['Bearer' => [], 'ApiKey' => []]], tags: ['数据中心'], )] #[Permission(code: 'attachment:upload')] #[ResultResponse(instance: new Result())] public function upload(UploadRequest $request): Result { $uploadFile = $request->file('file'); $newTmpPath = sys_get_temp_dir() . '/' . uniqid() . '.' . $uploadFile->getExtension(); $uploadFile->moveTo($newTmpPath); $splFileInfo = new SplFileInfo($newTmpPath, '', ''); return $this->success( $this->service->upload($splFileInfo, $uploadFile, $this->currentUser->id()) ); } ``` ```php [Service->Upload] namespace App\Service; use App\Model\Attachment; use App\Repository\AttachmentRepository; use Hyperf\HttpMessage\Upload\UploadedFile; use Mine\Upload\UploadInterface; use Symfony\Component\Finder\SplFileInfo; /** * @extends IService */ final class AttachmentService extends IService { public function __construct( protected readonly AttachmentRepository $repository, protected readonly UploadInterface $upload ) {} public function upload(SplFileInfo $fileInfo, UploadedFile $uploadedFile, int $userId): Attachment { $fileHash = md5_file($fileInfo->getRealPath()); if ($attachment = $this->repository->findByHash($fileHash)) { return $attachment; } $upload = $this->upload->upload( $fileInfo, ); return $this->repository->create([ 'created_by' => $userId, 'origin_name' => $uploadedFile->getClientFilename(), 'storage_mode' => $upload->getStorageMode(), 'object_name' => $upload->getObjectName(), 'mime_type' => $upload->getMimeType(), 'storage_path' => $upload->getStoragePath(), 'hash' => $fileHash, 'suffix' => $upload->getSuffix(), 'size_byte' => $upload->getSizeByte(), 'size_info' => $upload->getSizeInfo(), 'url' => $upload->getUrl(), ]); } ``` ::: ### 替换本地存储为 Oss 存储 在日常的业务场景中,一般文件存储在 OSS 上。那么此时就需要替换默认的文件上传处理了。以阿里云举例 首先我们要配置 `config/autoload/file.php` 文件。新增阿里云通道。 然后新建一个 `AliyunUploadSubscribe` 替换 `config/autoload/listeners.php` 中默认的 `UploadSubscribe` 指定为阿里云通道 ::: code-group ```php{19} [AliyunUploadSubscribe] 'local', 'storage' => [ 'local' => [ 'driver' => LocalAdapterFactory::class, 'root' => BASE_PATH . '/storage/uploads', 'public_url' => env('APP_URL') . '/uploads', ], 'oss' => [ 'driver' => AliyunOssAdapterFactory::class, 'accessId' => '', 'accessSecret' => '', 'bucket' => '', 'endpoint' => '', 'domain' => '', 'schema' => 'http://', 'isCName' => false, // oss 域名地址,不填写则会生成访问路径失败 'public_url' => env('APP_URL') . '/uploads', // 'timeout' => 3600, // 'connectTimeout' => 10, // 'token' => '', ], ], ]; ``` ```php{20,25-28} [listeners.php] // config/autoload/listeners.php generatorPath()` 和 `Mine\Upload\Listener\UploadListener->generatorId()` 实现的,而所有的上传处理类都是继承与这个类。 那么以上一张替换 OSS 存储为例。只需要在你的上传处理类中替换这两个方法即可 ```php{22-32} isUploaded() 是否上传成功` 如果上传成功则返回 `Upload` 上传实例 如果没有上传成功则抛出异常上传失败 #### 流程图 ```plantuml |前端| start :调用接口 /admin/attachment/upload,传入文件参数 file; |服务端文件控制器| :调用文件服务处理 file; |文件服务| :判断上传文件 hash 值是否已上传过; if (已上传过) then (是) :查询数据库返回上次上传信息; else (否) :调用 UploadInterface 实例分发 UploadEvent; |UploadEvent| :判断 isUploaded() 是否上传成功; if (上传成功) then (是) :返回 Upload 上传实例; else (否) :抛出异常上传失败; endif endif ``` #### 时序图 ```plantuml participant "前端" as Frontend participant "服务端文件控制器" as Controller participant "文件服务" as FileService participant "UploadInterface" as Uploader participant "数据库" as Database Frontend -> Controller : 调用接口 /admin/attachment/upload,传入 file Controller -> FileService : 处理 file FileService -> FileService : 判断文件 hash 值是否已上传过 alt 已上传过 FileService -> Database : 查询数据库 Database -> FileService : 返回上次上传信息 FileService -> Controller : 返回上次上传信息 Controller -> Frontend : 返回上次上传信息 else 未上传过 FileService -> Uploader : 分发 UploadEvent Uploader -> Uploader : 判断 isUploaded() 是否上传成功 alt 上传成功 Uploader -> FileService : 返回 Upload 上传实例 FileService -> Controller : 返回 Upload 上传实例 Controller -> Frontend : 返回 Upload 上传实例 else 上传失败 Uploader -> FileService : 抛出异常上传失败 FileService -> Controller : 抛出异常上传失败 Controller -> Frontend : 抛出异常上传失败 end end ``` ## 前端直传 OSS TODO --- --- url: /v3/backend/base/upload.md --- # 文件上传 本文已迁移到 [Hyperf 文件上传](/backend/frameworks/hyperf/3.2/base/upload)。 跨框架一致的前台对接要求,请阅读 [前台模板对接契约](/v3/backend/contracts/frontend-template)。 --- --- url: /backend/frameworks/hyperf/3.2/base/upload.md --- # 文件上传 ## 后端上传 ::: tip 文件上传由 MineAdmin 接入 [mineadmin/upload](https://github.com/mineadmin/upload) 组件建设而成 ::: MineAdmin 提供了一个默认的服务端文件上传逻辑。接口 `/admin/attachment/upload` 并且在上传成功后接入了资源管理器。 ::: code-group ```php [ControllerUpload] #[Post( path: '/admin/attachment/upload', operationId: 'UploadAttachment', summary: '上传附件', security: [['Bearer' => [], 'ApiKey' => []]], tags: ['数据中心'], )] #[Permission(code: 'attachment:upload')] #[ResultResponse(instance: new Result())] public function upload(UploadRequest $request): Result { $uploadFile = $request->file('file'); $newTmpPath = sys_get_temp_dir() . '/' . uniqid() . '.' . $uploadFile->getExtension(); $uploadFile->moveTo($newTmpPath); $splFileInfo = new SplFileInfo($newTmpPath, '', ''); return $this->success( $this->service->upload($splFileInfo, $uploadFile, $this->currentUser->id()) ); } ``` ```php [Service->Upload] namespace App\Service; use App\Model\Attachment; use App\Repository\AttachmentRepository; use Hyperf\HttpMessage\Upload\UploadedFile; use Mine\Upload\UploadInterface; use Symfony\Component\Finder\SplFileInfo; /** * @extends IService */ final class AttachmentService extends IService { public function __construct( protected readonly AttachmentRepository $repository, protected readonly UploadInterface $upload ) {} public function upload(SplFileInfo $fileInfo, UploadedFile $uploadedFile, int $userId): Attachment { $fileHash = md5_file($fileInfo->getRealPath()); if ($attachment = $this->repository->findByHash($fileHash)) { return $attachment; } $upload = $this->upload->upload( $fileInfo, ); return $this->repository->create([ 'created_by' => $userId, 'origin_name' => $uploadedFile->getClientFilename(), 'storage_mode' => $upload->getStorageMode(), 'object_name' => $upload->getObjectName(), 'mime_type' => $upload->getMimeType(), 'storage_path' => $upload->getStoragePath(), 'hash' => $fileHash, 'suffix' => $upload->getSuffix(), 'size_byte' => $upload->getSizeByte(), 'size_info' => $upload->getSizeInfo(), 'url' => $upload->getUrl(), ]); } ``` ::: ### 替换本地存储为 Oss 存储 在日常的业务场景中,一般文件存储在 OSS 上。那么此时就需要替换默认的文件上传处理了。以阿里云举例 首先我们要配置 `config/autoload/file.php` 文件。新增阿里云通道。 然后新建一个 `AliyunUploadSubscribe` 替换 `config/autoload/listeners.php` 中默认的 `UploadSubscribe` 指定为阿里云通道 ::: code-group ```php{19} [AliyunUploadSubscribe] 'local', 'storage' => [ 'local' => [ 'driver' => LocalAdapterFactory::class, 'root' => BASE_PATH . '/storage/uploads', 'public_url' => env('APP_URL') . '/uploads', ], 'oss' => [ 'driver' => AliyunOssAdapterFactory::class, 'accessId' => '', 'accessSecret' => '', 'bucket' => '', 'endpoint' => '', 'domain' => '', 'schema' => 'http://', 'isCName' => false, // oss 域名地址,不填写则会生成访问路径失败 'public_url' => env('APP_URL') . '/uploads', // 'timeout' => 3600, // 'connectTimeout' => 10, // 'token' => '', ], ], ]; ``` ```php{20,25-28} [listeners.php] // config/autoload/listeners.php generatorPath()` 和 `Mine\Upload\Listener\UploadListener->generatorId()` 实现的,而所有的上传处理类都是继承与这个类。 那么以上一张替换 OSS 存储为例。只需要在你的上传处理类中替换这两个方法即可 ```php{22-32} isUploaded() 是否上传成功` 如果上传成功则返回 `Upload` 上传实例 如果没有上传成功则抛出异常上传失败 #### 流程图 ```plantuml |前端| start :调用接口 /admin/attachment/upload,传入文件参数 file; |服务端文件控制器| :调用文件服务处理 file; |文件服务| :判断上传文件 hash 值是否已上传过; if (已上传过) then (是) :查询数据库返回上次上传信息; else (否) :调用 UploadInterface 实例分发 UploadEvent; |UploadEvent| :判断 isUploaded() 是否上传成功; if (上传成功) then (是) :返回 Upload 上传实例; else (否) :抛出异常上传失败; endif endif ``` #### 时序图 ```plantuml participant "前端" as Frontend participant "服务端文件控制器" as Controller participant "文件服务" as FileService participant "UploadInterface" as Uploader participant "数据库" as Database Frontend -> Controller : 调用接口 /admin/attachment/upload,传入 file Controller -> FileService : 处理 file FileService -> FileService : 判断文件 hash 值是否已上传过 alt 已上传过 FileService -> Database : 查询数据库 Database -> FileService : 返回上次上传信息 FileService -> Controller : 返回上次上传信息 Controller -> Frontend : 返回上次上传信息 else 未上传过 FileService -> Uploader : 分发 UploadEvent Uploader -> Uploader : 判断 isUploaded() 是否上传成功 alt 上传成功 Uploader -> FileService : 返回 Upload 上传实例 FileService -> Controller : 返回 Upload 上传实例 Controller -> Frontend : 返回 Upload 上传实例 else 上传失败 Uploader -> FileService : 抛出异常上传失败 FileService -> Controller : 抛出异常上传失败 Controller -> Frontend : 抛出异常上传失败 end end ``` ## 前端直传 OSS TODO --- --- url: /v3/backend/base/logger.md --- # 日志 本文已迁移到 [Hyperf 日志](/backend/frameworks/hyperf/3.2/base/logger)。 新的后端文档按 [公共契约](/v3/backend/contracts/) 和 [框架实现](/backend/frameworks/hyperf/) 组织。旧地址保留用于兼容历史链接。 --- --- url: /backend/frameworks/hyperf/3.2/base/logger.md --- # 日志处理 ## 开发模式下的命令行日志 在 `.env` 文件中,如果 `APP_DEBUG=true`,那么服务端会自动把所有错误日志输出到命令行。方便开发者本地进行调试。 如果 `APP_DEBUG=false`,那么服务端会尽可能的把日志输出到默认的 `loggerFactor->get('xxx','default')` 日志通道中 ::: tip 关于更多日志使用的文档,请参考 [hyperf](https://hyperf.io) 文档,本文不再另行说明基础用法 ::: --- --- url: /backend/frameworks/hyperf/3.1/base/logger.md --- # 日志处理 ## 开发模式下的命令行日志 在 `.env` 文件中,如果 `APP_DEBUG=true`,那么服务端会自动把所有错误日志输出到命令行。方便开发者本地进行调试。 如果 `APP_DEBUG=false`,那么服务端会尽可能的把日志输出到默认的 `loggerFactor->get('xxx','default')` 日志通道中 ::: tip 关于更多日志使用的文档,请参考 [hyperf](https://hyperf.io) 文档,本文不再另行说明基础用法 ::: --- --- url: /v3/front/high/provider.md --- # 服务提供器 ## 概述 ### 核心作用 服务提供器(Provider)是 MineAdmin 3.0 前端架构的核心特性之一,借鉴了后端服务提供器的设计理念,为前端应用提供了模块化的服务注册和管理机制。 ::: tip 主要功能 * **全局服务注册**: 将服务注册到 Vue 的 `globalProperties` 或 `provide/inject` 系统 * **组件初始化**: 自动初始化和配置全局组件 * **插件配置管理**: 提供插件的默认配置和参数管理 * **依赖注入**: 实现服务之间的依赖关系管理 * **模块化架构**: 支持按功能模块组织服务 ::: ### 初始化顺序 ::: danger 重要提示 服务提供器在应用初始化的早期阶段加载,**早于** `pinia`、`vue-router`、`vue-i18n` 等库的初始化。因此在服务提供器中无法直接使用这些库的功能。 **初始化顺序**: 1. 服务提供器扫描和注册 ⚡ 2. Pinia 状态管理初始化 3. Vue Router 路由初始化 4. Vue I18n 国际化初始化 5. 应用主体启动 ::: ## 架构设计 ### 目录结构 ``` src/provider/ ├── dictionary/ # 字典服务提供器 │ ├── index.ts # 服务提供器主文件 │ └── data/ # 字典数据文件 ├── echarts/ # 图表服务提供器 │ └── index.ts ├── plugins/ # 插件配置服务提供器 │ └── index.ts ├── mine-core/ # 核心组件服务提供器 │ └── index.ts ├── settings/ # 系统配置服务提供器 │ ├── index.ts │ └── settings.config.ts └── toolbars/ # 工具栏服务提供器 └── index.ts ``` ### 自动发现机制 系统启动时会自动扫描 `src/provider/` 目录下的所有子目录,每个子目录的 `index.ts` 文件都会被识别为一个服务提供器并自动注册。 ## 系统内置服务 ### Dictionary(字典服务) **功能说明**: 提供统一的数据字典管理功能,支持多语言和主题配色。 **源码位置**: * GitHub: [src/provider/dictionary/](https://github.com/mineadmin/mineadmin/tree/master/web/src/provider/dictionary) * 本地: `/Users/zhuzhu/project/mineadmin/web/src/provider/dictionary/` **核心特性**: * 支持多语言国际化标识 * 内置主题色彩系统 * 自动类型推导 * 响应式数据更新 **字典数据示例** (`src/provider/dictionary/data/system-status.ts`): ```ts import type { Dictionary } from '#/global' export default [ { label: '启用', value: 1, i18n: 'dictionary.system.statusEnabled', color: 'primary' }, { label: '禁用', value: 2, i18n: 'dictionary.system.statusDisabled', color: 'danger' }, ] as Dictionary[] ``` **使用方法**: ```ts // 在组件中使用字典数据 import { useDictionary } from '@/composables/useDictionary' const { getDictionary } = useDictionary() const statusDict = getDictionary('system-status') ``` ### ECharts(图表服务) **功能说明**: 提供 ECharts 图表库的初始化、配置和主题管理功能。 **源码位置**: * GitHub: [src/provider/echarts/](https://github.com/mineadmin/mineadmin/tree/master/web/src/provider/echarts) * 本地: `/Users/zhuzhu/project/mineadmin/web/src/provider/echarts/` **核心特性**: * 按需引入图表组件,减少包体积 * 自动适配系统主题(明暗模式) * 全局实例注册到 Vue * 响应式图表尺寸调整 **使用方法**: ```ts // 在组件中获取 ECharts 实例 import { useGlobal } from '@/composables/useGlobal' const { $echarts } = useGlobal() // 初始化图表 const chartInstance = $echarts.init(chartRef.value) ``` 参考组件: [MaEcharts](/v3/front/component/ma-echarts) ### Plugins(插件配置服务) **功能说明**: 为 MineAdmin 插件系统提供默认配置管理,支持插件参数的统一配置和管理。 **源码位置**: * GitHub: [src/provider/plugins/](https://github.com/mineadmin/mineadmin/tree/master/web/src/provider/plugins) * 本地: `/Users/zhuzhu/project/mineadmin/web/src/provider/plugins/` **核心特性**: * 插件配置集中管理 * 默认参数注册 * 配置热更新支持 * 插件依赖关系管理 参考文档: [插件系统](/v3/front/high/plugins) ### MineCore(核心组件服务) **功能说明**: 初始化 MineAdmin 核心组件库,提供全局配置和组件注册服务。 **源码位置**: * GitHub: [src/provider/mine-core/](https://github.com/mineadmin/mineadmin/tree/master/web/src/provider/mine-core) * 本地: `/Users/zhuzhu/project/mineadmin/web/src/provider/mine-core/` **管理的组件**: * `ma-table` - 数据表格组件 * `ma-search` - 搜索表单组件 * `ma-form` - 表单组件 * `ma-pro-table` - 高级表格组件 **使用方法**: ```ts import { useGlobal } from '@/composables/useGlobal' const { $mineCore } = useGlobal() const tableConfig = $mineCore.table ``` ### Settings(系统配置服务) **功能说明**: 提供前端应用的全局配置参数管理,支持开发和生产环境的配置分离。 **源码位置**: * GitHub: [src/provider/settings/](https://github.com/mineadmin/mineadmin/tree/master/web/src/provider/settings) * 本地: `/Users/zhuzhu/project/mineadmin/web/src/provider/settings/` **配置文件**: * `index.ts` - 默认配置(请勿直接修改) * `settings.config.ts` - 用户自定义配置文件 **配置示例**: ```ts // settings.config.ts export default { // 系统基础配置 app: { name: 'MineAdmin', version: '3.0.0', logo: '/logo.png' }, // API 配置 api: { baseUrl: process.env.NODE_ENV === 'development' ? 'http://localhost:9501' : 'https://api.example.com', timeout: 10000 }, // 主题配置 theme: { primaryColor: '#409eff', darkMode: 'auto' } } ``` ## 开发指南 ### 创建基础服务提供器 **步骤 1**: 创建服务目录 ```bash mkdir src/provider/my-service ``` **步骤 2**: 创建服务提供器文件 (`src/provider/my-service/index.ts`) ```ts import type { App } from 'vue' import type { ProviderService } from '#/global' // 定义服务接口 interface MyService { version: string getName: () => string setConfig: (config: any) => void } const provider: ProviderService.Provider = { name: 'myService', init() { console.log('MyService 正在初始化...') }, setProvider(app: App) { const service: MyService = { version: '1.0.0', getName: () => 'My Custom Service', setConfig: (config) => { console.log('配置已更新:', config) } } // 注册到全局属性 app.config.globalProperties.$myService = service // 或者使用 provide/inject app.provide('myService', service) }, getProvider() { return useGlobal().$myService } } export default provider ``` ### 创建带配置的高级服务提供器 ```ts import type { App } from 'vue' import type { ProviderService } from '#/global' // 服务配置接口 interface ServiceConfig { apiUrl: string timeout: number retries: number } // 服务实例接口 interface AdvancedService { config: ServiceConfig request: (url: string) => Promise updateConfig: (newConfig: Partial) => void } const provider: ProviderService.Provider = { name: 'advancedService', config: { enabled: true, priority: 10, dependencies: ['settings'] // 依赖 settings 服务 }, async init() { // 异步初始化逻辑 await this.loadExternalLibrary() }, setProvider(app: App) { const defaultConfig: ServiceConfig = { apiUrl: '/api/v1', timeout: 5000, retries: 3 } const service: AdvancedService = { config: { ...defaultConfig }, async request(url: string) { // 实现请求逻辑 return fetch(`${this.config.apiUrl}${url}`, { timeout: this.config.timeout }) }, updateConfig(newConfig) { Object.assign(this.config, newConfig) } } app.config.globalProperties.$advancedService = service }, getProvider() { return useGlobal().$advancedService }, async loadExternalLibrary() { // 加载外部依赖库的逻辑 } } export default provider ``` ### 使用服务提供器 **在 Vue 组件中使用**: ```vue ``` **在 Composable 中使用**: ```ts // composables/useMyService.ts import { useGlobal } from '@/composables/useGlobal' export function useMyService() { const { $myService } = useGlobal() const updateServiceConfig = (config: any) => { $myService.setConfig(config) } return { service: $myService, updateServiceConfig } } ``` ## 最佳实践 ### 1. 命名规范 * 服务提供器名称使用 **camelCase** 格式 * 目录名使用 **kebab-case** 格式 * 全局属性使用 `$` 前缀 ### 2. 类型安全 ```ts // 扩展全局属性类型 declare module '@vue/runtime-core' { interface ComponentCustomProperties { $myService: MyService } } ``` ### 3. 依赖管理 ```ts const provider: ProviderService.Provider = { name: 'dependentService', config: { dependencies: ['settings', 'dictionary'] }, // ...其他配置 } ``` ### 4. 错误处理 ```ts setProvider(app: App) { try { // 服务初始化逻辑 app.config.globalProperties.$service = createService() } catch (error) { console.error(`服务 ${this.name} 初始化失败:`, error) // 提供降级方案 app.config.globalProperties.$service = createFallbackService() } } ``` ## 服务管理 ### 禁用服务提供器 ```ts const provider: ProviderService.Provider = { name: 'optionalService', config: { enabled: false // 禁用该服务 }, // ...其他配置 } ``` ### 移除服务提供器 删除对应的服务提供器目录即可: ```bash rm -rf src/provider/unwanted-service ``` ### 调试服务提供器 ```ts const provider: ProviderService.Provider = { name: 'debugService', init() { if (process.env.NODE_ENV === 'development') { console.log(`[Provider] ${this.name} 初始化完成`) } }, setProvider(app: App) { // 开发环境下添加调试信息 if (process.env.NODE_ENV === 'development') { window.__DEBUG_PROVIDERS__ = window.__DEBUG_PROVIDERS__ || {} window.__DEBUG_PROVIDERS__[this.name] = this } // 正常的服务注册逻辑 } } ``` ## 常见问题 | 问题 | 原因 | 解决方案 | |------|------|----------| | 服务未注册成功 | 缺少 `index.ts` 文件或未实现必要接口 | 检查文件存在性和接口实现 | | 无法使用 Pinia | 服务提供器初始化早于 Pinia | 将 Pinia 相关逻辑移至组件或 Composable 中 | | 服务依赖冲突 | 循环依赖或依赖顺序错误 | 重新设计依赖关系或使用事件总线 | | 类型推导错误 | 全局属性类型未正确扩展 | 添加 TypeScript 模块声明 | | 热更新失效 | 服务缓存问题 | 重启开发服务器 | ## 相关资源 **源码参考**: * GitHub 仓库: [MineAdmin 源码](https://github.com/mineadmin/mineadmin) * 服务提供器目录: [web/src/provider/](https://github.com/mineadmin/mineadmin/tree/master/web/src/provider) * 本地源码: `/Users/zhuzhu/project/mineadmin/web/src/provider/` **相关文档**: * [插件系统](/v3/front/high/plugins) * [MaEcharts 组件](/v3/front/component/ma-echarts) --- --- url: /v3/front/base/build-preview.md --- # 构建与预览 本文档详细介绍 MineAdmin 前端项目的构建、预览和部署流程,包含性能优化、环境配置和常见问题解决方案。 ## 构建流程概览 ```plantuml @startuml start :开发环境配置; :代码质量检查; :环境变量配置; :执行构建命令; :生成静态文件; :本地预览测试; :部署到服务器; stop @enduml ``` ## 构建(打包) ### 基础构建 项目开发完成后,需要进行生产环境构建以部署到服务器。 ```bash # 执行构建命令 pnpm run build ``` 构建成功后,会在项目根目录的 `./web` 下生成 `dist` 文件夹,包含所有打包好的静态文件。 ### 构建前检查 为确保构建质量,建议在构建前执行代码质量检查: ```bash # 完整的代码质量检查 pnpm run lint # 或分别执行 pnpm run lint:tsc # TypeScript 类型检查 pnpm run lint:eslint # ESLint 代码规范检查 pnpm run lint:stylelint # 样式代码检查 ``` ### 环境变量配置 #### 基础路径配置 ::: warning 重要配置 如果访问地址不是域名的根节点,必须正确配置 `VITE_APP_ROOT_BASE` ::: ```bash # 域名根节点部署:https://www.example.com/ VITE_APP_ROOT_BASE = / # 子路径部署:https://www.example.com/app/ VITE_APP_ROOT_BASE = /app/ # 多级子路径:https://www.example.com/admin/system/ VITE_APP_ROOT_BASE = /admin/system/ ``` #### 生产环境变量 在 `.env.production` 文件中配置生产环境变量: ```bash # API 服务地址 VITE_APP_API_BASEURL = http://hyperf:9501 # 代理前缀 VITE_PROXY_PREFIX = /prod # 是否生成 Source Map(建议生产环境关闭) VITE_BUILD_SOURCEMAP = false # 压缩配置 VITE_BUILD_COMPRESS = gzip,brotli # 打包归档(可选) VITE_BUILD_ARCHIVE = ``` ## 本地预览 ### 预览构建结果 构建完成后,通过本地服务器预览确保项目正常运行: ```bash # 启动预览服务器 pnpm run serve ``` 预览服务器会启动一个 HTTP 服务,自动打开浏览器访问构建后的项目。 ### 预览配置说明 预览服务使用 `http-server` 工具,默认配置: * 服务目录:`./dist` * 自动打开浏览器:`-o` 参数 * 访问地址:通常为 `http://localhost:8080` ### E2E 测试 在预览阶段可以执行端到端测试: ```bash # 运行 E2E 测试 pnpm run test:e2e ``` ## 构建优化 ### 压缩配置 MineAdmin 支持多种压缩算法以减小文件体积: ```bash # 仅启用 Gzip 压缩 VITE_BUILD_COMPRESS = gzip # 仅启用 Brotli 压缩(压缩率更高) VITE_BUILD_COMPRESS = brotli # 同时启用两种压缩(推荐) VITE_BUILD_COMPRESS = gzip,brotli ``` ::: info 压缩算法对比 * **Gzip**: 兼容性好,压缩比约 70-80% * **Brotli**: 压缩比约 75-85%,但需要较新的浏览器支持 * **建议**: 同时启用两种算法,服务器根据客户端支持情况自动选择 ::: ### 性能优化建议 #### 1. Source Map 控制 ```bash # 生产环境建议关闭(提升构建速度,减小文件体积) VITE_BUILD_SOURCEMAP = false # 开发阶段可以开启(便于调试) VITE_BUILD_SOURCEMAP = true ``` #### 2. 代码分割 Vite 默认会进行代码分割,无需额外配置。构建后会生成: * `index.[hash].js` - 主入口文件 * `vendor.[hash].js` - 第三方依赖 * `[name].[hash].js` - 异步模块 #### 3. 资源优化 构建过程会自动进行: * CSS 压缩和合并 * 图片资源优化 * 字体文件处理 * 静态资源 Hash 命名 ## 部署配置 ### Nginx 配置示例 针对不同的压缩配置,Nginx 需要相应的模块支持: ```nginx server { listen 80; server_name your-domain.com; root /path/to/dist; index index.html; # 启用 Gzip 压缩 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; # 启用 Brotli 压缩(需要 nginx-module-brotli) brotli on; brotli_comp_level 6; brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; # SPA 路由支持 location / { try_files $uri $uri/ /index.html; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; } } ``` ### CDN 部署 如果使用 CDN 部署,需要配置: ```bash # CDN 域名 VITE_APP_CDN_URL = https://cdn.example.com # 启用 CDN 资源路径 VITE_APP_USE_CDN = true ``` ## 常见问题与解决方案 ### 构建失败 #### 1. TypeScript 类型错误 ```bash # 错误信息示例 error TS2307: Cannot find module 'xxx' # 解决方案 pnpm run lint:tsc # 先检查类型错误 # 修复类型问题后重新构建 ``` #### 2. 内存不足 ```bash # 增加 Node.js 内存限制 NODE_OPTIONS="--max-old-space-size=4096" pnpm run build ``` #### 3. 依赖问题 ```bash # 清理依赖并重新安装 rm -rf node_modules rm pnpm-lock.yaml pnpm install ``` ### 预览问题 #### 1. 接口请求失败 检查 `.env.production` 中的 API 地址配置: ```bash # 确保 API 地址可访问 VITE_APP_API_BASEURL = http://your-api-server:port ``` #### 2. 路由访问 404 确保服务器配置了 SPA 路由支持,或检查路由模式配置: ```bash # Hash 模式(兼容性更好) VITE_APP_ROUTE_MODE = hash # History 模式(需要服务器支持) VITE_APP_ROUTE_MODE = history ``` #### 3. 静态资源加载失败 检查基础路径配置: ```bash # 确保与部署路径一致 VITE_APP_ROOT_BASE = /your-app-path/ ``` ### 性能问题 #### 1. 构建时间过长 ```bash # 使用并行构建 VITE_BUILD_PARALLEL = true # 跳过某些检查(仅在必要时使用) VITE_SKIP_TYPE_CHECK = true ``` #### 2. 打包体积过大 分析打包体积: ```bash # 安装分析工具 pnpm add -D vite-bundle-analyzer # 分析构建结果 pnpm run build --analyze ``` ## 自动化部署 ### CI/CD 配置示例 ```yaml # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Install dependencies run: pnpm install - name: Lint code run: pnpm run lint - name: Build project run: pnpm run build - name: Deploy to server run: | # 部署脚本 rsync -avz ./dist/ user@server:/path/to/deployment/ ``` --- --- url: /backend/frameworks/hyperf/3.1/data-permission/overview.md --- # 核心概念 本功能指在系统中实现数据的分层管理和权限控制,主要包括部门管理、岗位管理、数据权限等模块。 相对比 `master` 分支来说新增了部门管理和岗位管理的功能模块、实现了多种数据隔离方式,增强了系统的组织架构和角色定义能力。 ## 系统架构图 ```plantuml @startuml !theme plain title 数据权限系统架构 package "核心模型" { class User { +getPolicy(): ?Policy +isSuperAdmin(): bool +department(): BelongsToMany +position(): BelongsToMany } class Policy { +user_id: int +position_id: int +policy_type: PolicyType +value: array } class Department { +parent_id: int +getFlatChildren(): Collection } class Position { +dept_id: int +policy(): HasOne } } package "权限引擎" { class Factory { +build(Builder, User): void } class Context { +setDeptColumn(string): void +setScopeType(ScopeType): void } enum PolicyType { DEPT_SELF DEPT_TREE ALL SELF CUSTOM_DEPT CUSTOM_FUNC } enum ScopeType { DEPT = 1 CREATED_BY = 2 DEPT_CREATED_BY = 3 DEPT_OR_CREATED_BY = 4 } } package "AOP层" { annotation DataScope { +deptColumn: string +createdByColumn: string +scopeType: ScopeType +onlyTables: array } class DataScopeAspect { +process(): void } } User --> Policy : 用户策略 Position --> Policy : 岗位策略 User --> Position : 多岗位 Position --> Department : 部门归属 Factory --> Context : 读取配置 DataScope --> DataScopeAspect : 触发拦截 DataScopeAspect --> Factory : 执行权限过滤 @enduml ``` ## 权限验证流程 ```plantuml @startuml !theme plain title 数据权限验证流程 start :接收查询请求; :DataScope注解拦截; if (用户是超级管理员?) then (是) :跳过权限检查; :返回完整数据; stop endif :获取当前用户; :查询用户策略; if (用户有直接策略?) then (有) :使用用户策略; else (无) :查询用户岗位; if (岗位有策略?) then (有) :使用岗位策略; else (无) :返回空数据; stop endif endif :解析策略类型; switch (PolicyType) case (SELF) :过滤个人数据; case (DEPT_SELF) :过滤本部门数据; case (DEPT_TREE) :过滤部门树数据; case (ALL) :访问全部数据; case (CUSTOM_DEPT) :过滤自定义部门; case (CUSTOM_FUNC) :执行自定义函数; endswitch :构建查询条件; :注入到 Query Builder; :执行过滤查询; :返回权限内数据; stop @enduml ``` ## 核心组件 ### 部门管理 #### 功能定位 组织架构的基础单元,实现树形层级管理。 #### 核心特性 * 支持无限级父子部门结构 * 部门关联岗位和用户 * 支持设置部门负责人 #### 数据模型 ```php // /mineadmin/app/Model/Permission/Department.php class Department { int $id; string $name; int $parent_id; HasMany $children; // 子部门 BelongsToMany $users; // 部门用户 BelongsToMany $leaders; // 部门领导 // 递归获取所有子部门 public function getFlatChildren(): Collection { $flat = collect(); $this->load('children'); $traverse = static function ($departments) use (&$traverse, $flat) { foreach ($departments as $department) { $flat->push($department); if ($department->children->isNotEmpty()) { $traverse($department->children); } } }; $traverse($this->children); return $flat->prepend($this); } } ``` *** ### 岗位管理 #### 功能定位 部门内的职能角色定义 #### 核心特性 * 必须归属于具体部门 * 可设置数据权限策略 * 支持用户多岗位分配 #### 数据模型 ```php // /mineadmin/app/Model/Permission/Position.php class Position { int $id; string $name; int $dept_id; public function policy(): HasOne { return $this->hasOne(Policy::class, 'position_id', 'id'); } } ``` ## 数据权限体系 ### 策略类型 ```php // /mineadmin/app/Model/Enums/DataPermission/PolicyType.php enum PolicyType: string { case DeptSelf = 'DEPT_SELF'; // 本部门 case DeptTree = 'DEPT_TREE'; // 本部门及下级部门 case All = 'ALL'; // 全部数据 case Self = 'SELF'; // 仅本人 case CustomDept = 'CUSTOM_DEPT'; // 自定义部门 case CustomFunc = 'CUSTOM_FUNC'; // 自定义函数 } ``` | 权限标识码 | 类型 | 作用域 | 备注 | |-------|----|-----|----| | DEPT\_SELF | 部门 | 当前部门 | 仅限当前部门数据 | | DEPT\_TREE | 部门 | 当前部门及子部门 | 包括当前部门和所有子部门数据 | | ALL | 全局 | 全部数据 | 包括所有部门和用户数据 | | SELF | 个人 | 个人数据 | 仅限当前用户数据 | | CUSTOM\_DEPT | 自定义 | 自定义部门 | 允许选择特定部门 | | CUSTOM\_FUNC | 自定义 | 自定义函数 | 允许自定义处理逻辑 | ### 隔离方式 ```php // /mineadmin/app/Library/DataPermission/ScopeType.php enum ScopeType: int { case DEPT = 1; // 只根据部门过滤 case CREATED_BY = 2; // 只根据创建人过滤 case DEPT_CREATED_BY = 3; // 根据部门 and 创建人过滤 case DEPT_OR_CREATED_BY = 4; // 根据部门 or 创建人过滤 } ``` ### 实现机制 数据权限通过与`岗位` or `用户` 关联的`数据权限策略`实现。每个岗位或用户可以有一个或多个数据权限策略,系统根据这些策略来过滤和控制数据访问。 #### 策略模型 ```php // /mineadmin/app/Model/DataPermission/Policy.php class Policy { int $user_id; // 用户ID int $position_id; // 岗位ID PolicyType $policy_type; bool $is_default; array $value; // 策略值 } ``` #### 策略解析优先级 ```php // /mineadmin/app/Model/Permission/User.php:160-179 public function getPolicy(): ?Policy { // 1. 优先检查用户专属策略 $policy = $this->policy()->first(); if (! empty($policy)) { return $policy; } // 2. 如果用户没有直接策略,则查找岗位策略 $this->load('position'); $positionList = $this->position; foreach ($positionList as $position) { $current = $position->policy()->first(); if (! empty($current)) { return $current; } } return null; } ``` #### 执行流程 ```plantuml @startuml !theme plain participant Controller participant DataScopeAspect participant Factory participant Context participant QueryBuilder Controller -> DataScopeAspect: 数据查询请求 DataScopeAspect -> Factory: 获取当前用户策略 Factory -> Context: 读取权限配置 Context --> Factory: 返回配置信息 Factory -> QueryBuilder: 注入查询条件 QueryBuilder --> Controller: 返回过滤后数据 @enduml ``` ## 核心API ### DataScope 注解 ```php // /mineadmin/app/Library/DataPermission/Attribute/DataScope.php #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class DataScope extends AbstractAnnotation { public function __construct( private readonly string $deptColumn = 'dept_id', private readonly string $createdByColumn = 'created_by', private readonly ScopeType $scopeType = ScopeType::DEPT_CREATED_BY, private readonly ?array $onlyTables = null ) {} } ``` ### Context 上下文管理 ```php // /mineadmin/app/Library/DataPermission/Context.php final class Context { public static function setDeptColumn(string $column = 'dept_id'): void; public static function setCreatedByColumn(string $column = 'created_by'): void; public static function setScopeType(ScopeType $scopeType): void; public static function setOnlyTables(?array $tables): void; public static function getDeptColumn(): string; public static function getCreatedByColumn(): string; public static function getScopeType(): ScopeType; public static function getOnlyTables(): array; } ``` ### Factory 权限工厂 ```php // /mineadmin/app/Library/DataPermission/Factory.php class Factory { public static function make(): self; public function build(Builder $builder, User $user): void { if ($user->isSuperAdmin()) { return; // 超级管理员跳过权限检查 } if (($policy = $user->getPolicy()) === null) { return; // 无策略则跳过 } // 根据 ScopeType 处理不同的数据权限逻辑 $scopeType = Context::getScopeType(); // ... 权限处理逻辑 } } ``` ## 安全特性 ### 超级管理员绕过 超级管理员会自动跳过所有数据权限检查: ```php // /mineadmin/app/Library/DataPermission/Factory.php:37-39 if ($user->isSuperAdmin()) { return; // 超级管理员跳过所有数据权限检查 } ``` ### 自定义函数支持 系统支持通过配置文件定义自定义权限函数: ```php // /mineadmin/config/autoload/department/custom.php return [ 'testction' => function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) { // 自定义权限逻辑 if ($user->id !== 2) { return; } $createdByColumn = Context::getCreatedByColumn(); $deptColumn = Context::getDeptColumn(); switch ($scopeType) { case ScopeType::CREATED_BY: $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); break; // ... 其他逻辑 } } ]; ``` --- --- url: /backend/frameworks/hyperf/3.2/data-permission/overview.md --- # 核心概念 本功能指在系统中实现数据的分层管理和权限控制,主要包括部门管理、岗位管理、数据权限等模块。 相对比 `master` 分支来说新增了部门管理和岗位管理的功能模块、实现了多种数据隔离方式,增强了系统的组织架构和角色定义能力。 ## 系统架构图 ```plantuml @startuml !theme plain title 数据权限系统架构 package "核心模型" { class User { +getPolicy(): ?Policy +isSuperAdmin(): bool +department(): BelongsToMany +position(): BelongsToMany } class Policy { +user_id: int +position_id: int +policy_type: PolicyType +value: array } class Department { +parent_id: int +getFlatChildren(): Collection } class Position { +dept_id: int +policy(): HasOne } } package "权限引擎" { class Factory { +build(Builder, User): void } class Context { +setDeptColumn(string): void +setScopeType(ScopeType): void } enum PolicyType { DEPT_SELF DEPT_TREE ALL SELF CUSTOM_DEPT CUSTOM_FUNC } enum ScopeType { DEPT = 1 CREATED_BY = 2 DEPT_CREATED_BY = 3 DEPT_OR_CREATED_BY = 4 } } package "AOP层" { annotation DataScope { +deptColumn: string +createdByColumn: string +scopeType: ScopeType +onlyTables: array } class DataScopeAspect { +process(): void } } User --> Policy : 用户策略 Position --> Policy : 岗位策略 User --> Position : 多岗位 Position --> Department : 部门归属 Factory --> Context : 读取配置 DataScope --> DataScopeAspect : 触发拦截 DataScopeAspect --> Factory : 执行权限过滤 @enduml ``` ## 权限验证流程 ```plantuml @startuml !theme plain title 数据权限验证流程 start :接收查询请求; :DataScope注解拦截; if (用户是超级管理员?) then (是) :跳过权限检查; :返回完整数据; stop endif :获取当前用户; :查询用户策略; if (用户有直接策略?) then (有) :使用用户策略; else (无) :查询用户岗位; if (岗位有策略?) then (有) :使用岗位策略; else (无) :返回空数据; stop endif endif :解析策略类型; switch (PolicyType) case (SELF) :过滤个人数据; case (DEPT_SELF) :过滤本部门数据; case (DEPT_TREE) :过滤部门树数据; case (ALL) :访问全部数据; case (CUSTOM_DEPT) :过滤自定义部门; case (CUSTOM_FUNC) :执行自定义函数; endswitch :构建查询条件; :注入到 Query Builder; :执行过滤查询; :返回权限内数据; stop @enduml ``` ## 核心组件 ### 部门管理 #### 功能定位 组织架构的基础单元,实现树形层级管理。 #### 核心特性 * 支持无限级父子部门结构 * 部门关联岗位和用户 * 支持设置部门负责人 #### 数据模型 ```php // /mineadmin/app/Model/Permission/Department.php class Department { int $id; string $name; int $parent_id; HasMany $children; // 子部门 BelongsToMany $users; // 部门用户 BelongsToMany $leaders; // 部门领导 // 递归获取所有子部门 public function getFlatChildren(): Collection { $flat = collect(); $this->load('children'); $traverse = static function ($departments) use (&$traverse, $flat) { foreach ($departments as $department) { $flat->push($department); if ($department->children->isNotEmpty()) { $traverse($department->children); } } }; $traverse($this->children); return $flat->prepend($this); } } ``` *** ### 岗位管理 #### 功能定位 部门内的职能角色定义 #### 核心特性 * 必须归属于具体部门 * 可设置数据权限策略 * 支持用户多岗位分配 #### 数据模型 ```php // /mineadmin/app/Model/Permission/Position.php class Position { int $id; string $name; int $dept_id; public function policy(): HasOne { return $this->hasOne(Policy::class, 'position_id', 'id'); } } ``` ## 数据权限体系 ### 策略类型 ```php // /mineadmin/app/Model/Enums/DataPermission/PolicyType.php enum PolicyType: string { case DeptSelf = 'DEPT_SELF'; // 本部门 case DeptTree = 'DEPT_TREE'; // 本部门及下级部门 case All = 'ALL'; // 全部数据 case Self = 'SELF'; // 仅本人 case CustomDept = 'CUSTOM_DEPT'; // 自定义部门 case CustomFunc = 'CUSTOM_FUNC'; // 自定义函数 } ``` | 权限标识码 | 类型 | 作用域 | 备注 | |-------|----|-----|----| | DEPT\_SELF | 部门 | 当前部门 | 仅限当前部门数据 | | DEPT\_TREE | 部门 | 当前部门及子部门 | 包括当前部门和所有子部门数据 | | ALL | 全局 | 全部数据 | 包括所有部门和用户数据 | | SELF | 个人 | 个人数据 | 仅限当前用户数据 | | CUSTOM\_DEPT | 自定义 | 自定义部门 | 允许选择特定部门 | | CUSTOM\_FUNC | 自定义 | 自定义函数 | 允许自定义处理逻辑 | ### 隔离方式 ```php // /mineadmin/app/Library/DataPermission/ScopeType.php enum ScopeType: int { case DEPT = 1; // 只根据部门过滤 case CREATED_BY = 2; // 只根据创建人过滤 case DEPT_CREATED_BY = 3; // 根据部门 and 创建人过滤 case DEPT_OR_CREATED_BY = 4; // 根据部门 or 创建人过滤 } ``` ### 实现机制 数据权限通过与`岗位` or `用户` 关联的`数据权限策略`实现。每个岗位或用户可以有一个或多个数据权限策略,系统根据这些策略来过滤和控制数据访问。 #### 策略模型 ```php // /mineadmin/app/Model/DataPermission/Policy.php class Policy { int $user_id; // 用户ID int $position_id; // 岗位ID PolicyType $policy_type; bool $is_default; array $value; // 策略值 } ``` #### 策略解析优先级 ```php // /mineadmin/app/Model/Permission/User.php:160-179 public function getPolicy(): ?Policy { // 1. 优先检查用户专属策略 $policy = $this->policy()->first(); if (! empty($policy)) { return $policy; } // 2. 如果用户没有直接策略,则查找岗位策略 $this->load('position'); $positionList = $this->position; foreach ($positionList as $position) { $current = $position->policy()->first(); if (! empty($current)) { return $current; } } return null; } ``` #### 执行流程 ```plantuml @startuml !theme plain participant Controller participant DataScopeAspect participant Factory participant Context participant QueryBuilder Controller -> DataScopeAspect: 数据查询请求 DataScopeAspect -> Factory: 获取当前用户策略 Factory -> Context: 读取权限配置 Context --> Factory: 返回配置信息 Factory -> QueryBuilder: 注入查询条件 QueryBuilder --> Controller: 返回过滤后数据 @enduml ``` ## 核心API ### DataScope 注解 ```php // /mineadmin/app/Library/DataPermission/Attribute/DataScope.php #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class DataScope extends AbstractAnnotation { public function __construct( private readonly string $deptColumn = 'dept_id', private readonly string $createdByColumn = 'created_by', private readonly ScopeType $scopeType = ScopeType::DEPT_CREATED_BY, private readonly ?array $onlyTables = null ) {} } ``` ### Context 上下文管理 ```php // /mineadmin/app/Library/DataPermission/Context.php final class Context { public static function setDeptColumn(string $column = 'dept_id'): void; public static function setCreatedByColumn(string $column = 'created_by'): void; public static function setScopeType(ScopeType $scopeType): void; public static function setOnlyTables(?array $tables): void; public static function getDeptColumn(): string; public static function getCreatedByColumn(): string; public static function getScopeType(): ScopeType; public static function getOnlyTables(): array; } ``` ### Factory 权限工厂 ```php // /mineadmin/app/Library/DataPermission/Factory.php class Factory { public static function make(): self; public function build(Builder $builder, User $user): void { if ($user->isSuperAdmin()) { return; // 超级管理员跳过权限检查 } if (($policy = $user->getPolicy()) === null) { return; // 无策略则跳过 } // 根据 ScopeType 处理不同的数据权限逻辑 $scopeType = Context::getScopeType(); // ... 权限处理逻辑 } } ``` ## 安全特性 ### 超级管理员绕过 超级管理员会自动跳过所有数据权限检查: ```php // /mineadmin/app/Library/DataPermission/Factory.php:37-39 if ($user->isSuperAdmin()) { return; // 超级管理员跳过所有数据权限检查 } ``` ### 自定义函数支持 系统支持通过配置文件定义自定义权限函数: ```php // /mineadmin/config/autoload/department/custom.php return [ 'testction' => function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) { // 自定义权限逻辑 if ($user->id !== 2) { return; } $createdByColumn = Context::getCreatedByColumn(); $deptColumn = Context::getDeptColumn(); switch ($scopeType) { case ScopeType::CREATED_BY: $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); break; // ... 其他逻辑 } } ]; ``` --- --- url: /v3/guide/introduce/declaration.md --- # 法律声明与使用条款 ::: danger 重要法律声明 本文档具有法律约束力。使用MineAdmin即表示您已阅读、理解并同意遵守以下条款。 ::: *** ## 目录 * [1. 前言与用户须知](#1-前言与用户须知) * [2. 产品定义与授权范围](#2-产品定义与授权范围) * [3. 用户责任与合规要求](#3-用户责任与合规要求) * [4. 免责声明](#4-免责声明) * [5. 知识产权保护](#5-知识产权保护) * [6. 声明条款管理](#6-声明条款管理) *** ::: tip 常见问题快速跳转 * 🤔 [可以商业使用吗?](#21-软件性质) * ⚖️ [出现问题谁负责?](#4-免责声明) * 📝 [如何合规使用?](#3-用户责任与合规要求) * 🔒 [知识产权归属?](#5-知识产权保护) ::: ## 条款摘要 | 方面 | 要点 | 详细条款 | |------|------|----------| | 使用权限 | 开源免费,可商业使用 | [§2.1](#21-软件性质) | | 用户责任 | 合法合规使用 | [§3.1](#31-合规义务) | | 团队责任 | 有限担保 | [§4.1](#41-责任限制) | | 知识产权 | MineAdmin团队所有 | [§5.1](#51-权利归属) | *** ## 1. 前言与用户须知 ::: warning 使用前必读 任何用户在使用由 **MineAdmin团队**(以下简称「本团队」)研发的后台权限管理系统及后台前端模板(以下简称「**MineAdmin**」)前,请您仔细阅读并透彻理解本声明。 ::: ### 1.1 声明的约束力 您可以选择不使用 MineAdmin,若您一旦使用 MineAdmin,您的使用行为即被视为对本声明全部内容的认可和接受。 ### 1.2 适用范围 本声明适用于: * MineAdmin 后台权限管理系统 * 相关前端模板和组件 * 所有衍生产品和服务 * 技术支持和文档资源 *** ## 2. 产品定义与授权范围 ### 2.1 软件性质 **MineAdmin** 是一款开源免费、可商业使用的后台权限管理系统,主要用于便捷的后台管理功能的开发。 ::: info 功能说明 **核心功能:** * ✅ 后台权限管理框架 * ✅ 前端模板和组件 * ✅ 开发工具和文档 **功能限制:** * ❌ MineAdmin 本身不具备具体业务模块功能 * ❌ 不包含行业特定的业务逻辑 ::: ### 2.2 开源许可 MineAdmin 采用双重许可证模式: ::: details 许可证详情 **Apache 2.0 许可证** * 适用于企业级使用 * 提供专利保护 * 要求保留版权声明 **MIT 许可证** * 更宽松的开源许可 * 适合个人开发者 * 简化的使用条件 ::: **您可以:** * ✅ 免费商业使用 * ✅ 修改源代码 * ✅ 分发和再分发 * ✅ 私有使用 *** ## 3. 用户责任与合规要求 ### 3.1 合规义务 您承诺秉着合法、合理的原则使用 MineAdmin,并承担以下义务: #### 3.1.1 合法使用承诺 ::: warning 禁止行为 * ❌ 不利用 MineAdmin 进行任何违法活动 * ❌ 不侵害他人合法利益 * ❌ 不进行任何恶意行为 * ❌ 不将 MineAdmin 运用于违反法律法规的Web平台 ::: #### 3.1.2 合规使用检查清单 在使用 MineAdmin 前,请确认您的项目: * \[x] 符合当地法律法规 * \[x] 不涉及非法内容或服务 * \[x] 已获得必要的运营许可 * \[x] 尊重他人知识产权 * \[x] 遵循开源许可证要求 *** ## 4. 免责声明 ### 4.1 责任限制 ::: danger 重要免责条款 任何单位或个人因下载使用 MineAdmin 而产生的任何损失,**本团队不承担任何法律责任**。 ::: #### 4.1.1 免责范围 包括但不限于以下情形造成的损失: * 🔸 **意外事件** - 系统故障、数据丢失等 * 🔸 **疏忽行为** - 配置错误、操作失误等 * 🔸 **合约问题** - 商业合同纠纷等 * 🔸 **声誉损害** - 诽谤、恶意传播等 * 🔸 **知识产权** - 版权或专利侵犯等 #### 4.1.2 损失类型 免责涵盖所有类型的损失: * **直接损失** - 直接经济损失 * **间接损失** - 利润损失、商誉损害 * **附带损失** - 连带产生的其他损失 * **衍生损失** - 后续引发的损失 ### 4.2 用户风险承担 ::: warning 用户责任 用户明确并同意本声明条款列举的全部内容,对使用 MineAdmin 可能存在的风险和相关后果将**完全由用户自行承担**,本团队不承担任何法律责任。 ::: *** ## 5. 知识产权保护 ### 5.1 权利归属 ::: info 知识产权声明 本团队对 MineAdmin 拥有完整的知识产权,受相关法律法规保护。 ::: #### 5.1.1 保护范围 知识产权包括但不限于: * 🏷️ **商标权** - MineAdmin品牌标识 * 📋 **专利权** - 技术发明和创新 * 📄 **著作权** - 源代码和文档 #### 5.1.2 使用限制 在遵守开源许可证的前提下: * ✅ 可以使用和修改代码 * ✅ 可以用于商业项目 * ❌ 不得恶意抄袭或盗用 *** ## 6. 声明条款管理 ### 6.1 修改权利 ::: warning 条款变更 本团队有权随时对本声明条款及附件内容进行单方面的变更。 ::: #### 6.1.1 变更通知方式 条款变更将通过以下方式公布: * 📱 消息推送通知 * 🌐 官方网页公告 * 📧 邮件通知(注册用户) * 📚 文档更新说明 #### 6.1.2 生效规则 * **立即生效** - 公布后立即自动生效 * **无需单独通知** - 不另行单独通知用户 * **持续使用即同意** - 继续使用即表示接受新条款 ### 6.2 条款执行 #### 6.2.1 可分割性 如果本声明的任何部分被认为无效或不可执行: * 该部分将被解释为反映本团队的初衷 * 其余部分仍具有完全法律效力 * 不可执行的部分不构成放弃执行权利 #### 6.2.2 完整性保护 本声明条款构成完整的法律文件,任何口头承诺或其他文档均不能修改或替代本声明的内容。 *** ## 联系方式 如对本声明有疑问,请通过以下方式联系我们: * 📧 **邮箱:** <261091613@qq.com> or * 🌐 **官网:** * 📚 **文档:** * 💬 **社区:** [GitHub Issues](https://github.com/mineadmin/mineadmin/issues) *** *** **© 2025 MineAdmin Team. 保留所有权利。** --- --- url: /backend/frameworks/hyperf/3.1/data-permission/notice.md --- # 注意事项与最佳实践 本文档提供使用 MineAdmin 数据权限系统的重要注意事项、常见陷阱和最佳实践指南。遵循这些指导原则可以确保系统的安全性、可靠性和性能。 ## ❗ 关键注意事项 ### 1. 协程上下文隔离 ::: danger 严重警告 **协程上下文隔离是数据权限系统的核心安全特性,必须严格遵守!** ::: #### 问题描述 在 Hyperf 协程环境中,每个协程拥有独立的上下文空间。如果不正确处理协程上下文,可能导致: * **数据泄露**:用户 A 的数据被用户 B 看到 * **权限升级**:低权限用户获得高权限访问 * **数据不一致**:同一用户在不同请求中看到不同数据 #### 正确做法 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Context.php use App\Library\DataPermission\Context; use App\Library\DataPermission\ScopeType; // ✅ 正确:在每个协程开始时设置上下文 co(function () { // 设置数据权限上下文 Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); Context::setScopeType(ScopeType::DEPT_CREATED_BY); Context::setOnlyTables(['user']); // 执行业务逻辑 $data = User::query()->get(); }); // ✅ 正确:创建协程上下文管理辅助类 class CoroutineDataPermissionHelper { public static function withContext(User $user, callable $callback): mixed { return co(function () use ($user, $callback) { // 设置用户相关的权限上下文 self::setupContextForUser($user); // 执行回调 return $callback(); }); } private static function setupContextForUser(User $user): void { Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); // 根据用户策略设置权限范围 $policy = $user->getPolicy(); if ($policy) { Context::setScopeType(ScopeType::DEPT_CREATED_BY); } } } ``` #### 错误做法 ```php // ❌ 错误:跨协程共享上下文 $globalUser = auth()->user(); go(function () use ($globalUser) { // 危险!可能使用其他协程的上下文 $data = User::query()->get(); }); // ❌ 错误:在协程池中不重新设置上下文 for ($i = 0; $i < 10; $i++) { go(function () use ($i) { // 危险!协程池复用可能导致上下文污染 $user = User::find($i); // 缺少 Context 重新设置 $data = User::query()->get(); }); } ``` ### 2. 数据库字段映射 #### 确保字段名正确 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Attribute/DataScope.php // ✅ 正确:明确指定字段名 #[DataScope( deptColumn: 'department_id', // 确保数据表中有这个字段 createdByColumn: 'creator_id', // 确保数据表中有这个字段 onlyTables: ['orders', 'customers'] // 只对指定表生效 )] public function getData(): Collection { return Order::with('customer')->get(); } // ✅ 正确:验证字段存在(建议的辅助方法) function validatePermissionFields(string $table, array $fields): bool { try { $schema = DB::connection()->getSchemaBuilder()->getColumnListing($table); foreach ($fields as $field) { if (!in_array($field, $schema)) { throw new \InvalidArgumentException( "Field '{$field}' does not exist in table '{$table}'" ); } } return true; } catch (\Exception $e) { Log::error('字段验证失败', [ 'table' => $table, 'fields' => $fields, 'error' => $e->getMessage() ]); return false; } } ``` #### 错误做法 ```php // ❌ 错误:使用不存在的字段 #[DataScope( deptColumn: 'dept_id', // 如果表中字段是 'department_id' createdByColumn: 'created_by' // 如果表中字段是 'creator_id' )] public function getData(): Collection { // 会导致 SQL 错误或权限失效 return Order::query()->get(); } ``` ## ⚠️ 安全警告 ### 1. 防止权限绕过 ::: warning 安全风险 以下行为可能导致权限绕过,必须避免! ::: ```php // ❌ 危险:手动构建 SQL,绕过权限检查 $sql = "SELECT * FROM users WHERE dept_id = ?"; $users = DB::select($sql, [auth()->user()->dept_id]); // ❌ 危险:使用 whereRaw 绕过权限过滤 $users = User::whereRaw('1=1')->get(); // ❌ 危险:在管理员接口不使用权限控制 public function adminGetAllUsers(): Collection { // 危险!直接返回所有用户数据 return User::all(); } // ✅ 安全:始终使用权限系统 // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Service/Permission/UserService.php:94-98 #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['user'], createdByColumn: 'id' )] public function adminGetAllUsers(): Collection { return User::query()->get(); } ``` ### 2. 输入验证和安全过滤 ```php // ✅ 安全:验证用户输入 class SecurePermissionService { public function getFilteredData(array $filters): Collection { // 验证输入参数 $this->validateFilters($filters); // 使用白名单验证 $allowedColumns = ['name', 'email', 'status']; $filters = array_intersect_key($filters, array_flip($allowedColumns)); // 应用数据权限 Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); Context::setScopeType(ScopeType::DEPT_CREATED_BY); return User::query() ->when($filters['name'] ?? null, fn($q, $name) => $q->where('name', 'like', "%{$name}%")) ->when($filters['status'] ?? null, fn($q, $status) => $q->where('status', $status)) ->get(); } private function validateFilters(array $filters): void { foreach ($filters as $key => $value) { if (!in_array($key, ['name', 'email', 'status'])) { throw new \InvalidArgumentException("Invalid filter: {$key}"); } } } } ``` ### 3. 日志审计 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Factory.php // ✅ 重要:对敏感操作启用日志 #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['financial_records'] )] public function getFinancialData(): Collection { // 记录敏感数据访问 Log::info('敏感数据访问', [ 'user_id' => auth()->id(), 'operation' => 'get_financial_data', 'ip' => request()->ip(), 'user_agent' => request()->userAgent(), 'timestamp' => now() ]); return FinancialRecord::query()->get(); } // ✅ 权限操作监听(建议实现) class DataPermissionLogger { public static function logPermissionAccess(User $user, string $operation): void { Log::info('数据权限访问', [ 'user_id' => $user->id, 'operation' => $operation, 'policy' => $user->getPolicy()?->toArray(), 'ip' => request()->ip(), 'timestamp' => now() ]); } } ``` ## 🛡️ 最佳实践 ### 1. 权限策略设计 #### 遵循最小权限原则 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Model/Permission/User.php:160-179 class DataPermissionService { public function getDataByUserRole(User $user): Collection { // 获取用户策略 $policy = $user->getPolicy(); if (!$policy) { // 无策略时使用最保守的权限 Context::setScopeType(ScopeType::CREATED_BY); Context::setOnlyTables(['user']); return collect(); } // 根据策略类型设置权限范围 $this->configureScopeByPolicy($policy); return User::query()->get(); } private function configureScopeByPolicy($policy): void { Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); // 根据策略类型配置 match($policy->policy_type->value) { 'ALL' => Context::setScopeType(ScopeType::DEPT), 'DEPT_TREE' => Context::setScopeType(ScopeType::DEPT_CREATED_BY), 'DEPT_SELF' => Context::setScopeType(ScopeType::DEPT_CREATED_BY), 'SELF' => Context::setScopeType(ScopeType::CREATED_BY), default => Context::setScopeType(ScopeType::CREATED_BY) }; } } ``` ### 2. 错误处理策略 ```php // ✅ 最佳实践:优雅的错误处理 class SafeDataPermissionService { public function executeWithFallback(callable $primaryAction, callable $fallbackAction = null): mixed { try { return $primaryAction(); } catch (\Exception $e) { // 记录权限相关错误 Log::warning('数据权限操作失败', [ 'user_id' => auth()->id(), 'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]); // 执行回退策略 if ($fallbackAction) { return $fallbackAction(); } return collect(); // 返回空集合 } } } // 使用示例 $service = new SafeDataPermissionService(); $data = $service->executeWithFallback( fn() => $this->getComplexDataWithPermissions(), fn() => $this->getBasicUserData() // 回退方案 ); ``` ### 3. 性能优化 ```php // ✅ 最佳实践:性能监控 class DataPermissionMonitor { public function monitorExecution(callable $action, string $operationName): mixed { $startTime = microtime(true); try { $result = $action(); $executionTime = (microtime(true) - $startTime) * 1000; // 记录性能指标 if ($executionTime > 100) { // 超过100ms记录 Log::warning('数据权限操作耗时较长', [ 'operation' => $operationName, 'execution_time' => $executionTime . 'ms', 'user_id' => auth()->id() ]); } return $result; } catch (\Throwable $e) { Log::error('数据权限操作异常', [ 'operation' => $operationName, 'error' => $e->getMessage() ]); throw $e; } } } ``` ### 4. 测试最佳实践 ```php // ✅ 最佳实践:权限测试 class DataPermissionTest extends TestCase { public function test_department_isolation(): void { // 创建测试数据 $dept1 = Department::factory()->create(); $dept2 = Department::factory()->create(); $user1 = User::factory()->create(['dept_id' => $dept1->id]); $user2 = User::factory()->create(['dept_id' => $dept2->id]); $order1 = Order::factory()->create(['dept_id' => $dept1->id]); $order2 = Order::factory()->create(['dept_id' => $dept2->id]); // 测试用户1只能看到自己部门的数据 $this->actingAs($user1); Context::setDeptColumn('dept_id'); Context::setScopeType(ScopeType::DEPT); Context::setOnlyTables(['orders']); $results = Order::query()->get(); $this->assertCount(1, $results); $this->assertEquals($order1->id, $results->first()->id); } public function test_user_policy_application(): void { // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Model/Permission/User.php:160-179 $user = User::factory()->create(); // 创建用户策略 Policy::factory()->create([ 'user_id' => $user->id, 'policy_type' => PolicyType::DeptSelf, 'is_default' => true ]); // 验证策略获取 $policy = $user->getPolicy(); $this->assertNotNull($policy); $this->assertEquals(PolicyType::DeptSelf, $policy->policy_type); } } ``` ## 📝 检查清单 在使用数据权限系统前,请使用以下检查清单确保系统安全: ### 配置检查 * \[ ] **字段映射正确** - 确保 `deptColumn` 和 `createdByColumn` 对应实际数据表字段 * \[ ] **策略类型适当** - 根据业务需求选择合适的 `ScopeType` * \[ ] **表范围明确** - 使用 `onlyTables` 限制作用范围 * \[ ] **超级管理员检查** - 确认超级管理员绕过逻辑正确 ### 安全检查 * \[ ] **协程上下文管理** - 每个新协程都要重新设置上下文 * \[ ] **输入验证** - 对所有用户输入进行验证和清洗 * \[ ] **错误处理** - 实现适当的错误处理和回退策略 * \[ ] **日志审计** - 对敏感操作启用详细日志记录 ### 性能检查 * \[ ] **数据库索引** - 为权限字段创建适当的索引 * \[ ] **查询优化** - 避免 N+1 查询和不必要的联表操作 * \[ ] **监控告警** - 设置查询性能监控 ### 测试检查 * \[ ] **单元测试** - 编写各种权限场景的单元测试 * \[ ] **集成测试** - 测试权限系统与其他组件的集成 * \[ ] **边界测试** - 测试权限边界和异常情况 ## 总结 MineAdmin 数据权限系统的核心在于正确配置 Context 和 DataScope 注解。关键要点: 1. **严格管理协程上下文** - 避免权限泄露 2. **正确配置字段映射** - 确保权限过滤生效 3. **遵循最小权限原则** - 默认使用最保守的权限策略 4. **充分的错误处理** - 确保系统在异常情况下的安全性 5. **完善的测试覆盖** - 验证各种权限场景的正确性 遵循这些注意事项和最佳实践,可以确保 MineAdmin 数据权限系统在您的应用中安全、高效地运行。 --- --- url: /backend/frameworks/hyperf/3.2/data-permission/notice.md --- # 注意事项与最佳实践 本文档提供使用 MineAdmin 数据权限系统的重要注意事项、常见陷阱和最佳实践指南。遵循这些指导原则可以确保系统的安全性、可靠性和性能。 ## ❗ 关键注意事项 ### 1. 协程上下文隔离 ::: danger 严重警告 **协程上下文隔离是数据权限系统的核心安全特性,必须严格遵守!** ::: #### 问题描述 在 Hyperf 协程环境中,每个协程拥有独立的上下文空间。如果不正确处理协程上下文,可能导致: * **数据泄露**:用户 A 的数据被用户 B 看到 * **权限升级**:低权限用户获得高权限访问 * **数据不一致**:同一用户在不同请求中看到不同数据 #### 正确做法 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Context.php use App\Library\DataPermission\Context; use App\Library\DataPermission\ScopeType; // ✅ 正确:在每个协程开始时设置上下文 co(function () { // 设置数据权限上下文 Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); Context::setScopeType(ScopeType::DEPT_CREATED_BY); Context::setOnlyTables(['user']); // 执行业务逻辑 $data = User::query()->get(); }); // ✅ 正确:创建协程上下文管理辅助类 class CoroutineDataPermissionHelper { public static function withContext(User $user, callable $callback): mixed { return co(function () use ($user, $callback) { // 设置用户相关的权限上下文 self::setupContextForUser($user); // 执行回调 return $callback(); }); } private static function setupContextForUser(User $user): void { Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); // 根据用户策略设置权限范围 $policy = $user->getPolicy(); if ($policy) { Context::setScopeType(ScopeType::DEPT_CREATED_BY); } } } ``` #### 错误做法 ```php // ❌ 错误:跨协程共享上下文 $globalUser = auth()->user(); go(function () use ($globalUser) { // 危险!可能使用其他协程的上下文 $data = User::query()->get(); }); // ❌ 错误:在协程池中不重新设置上下文 for ($i = 0; $i < 10; $i++) { go(function () use ($i) { // 危险!协程池复用可能导致上下文污染 $user = User::find($i); // 缺少 Context 重新设置 $data = User::query()->get(); }); } ``` ### 2. 数据库字段映射 #### 确保字段名正确 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Attribute/DataScope.php // ✅ 正确:明确指定字段名 #[DataScope( deptColumn: 'department_id', // 确保数据表中有这个字段 createdByColumn: 'creator_id', // 确保数据表中有这个字段 onlyTables: ['orders', 'customers'] // 只对指定表生效 )] public function getData(): Collection { return Order::with('customer')->get(); } // ✅ 正确:验证字段存在(建议的辅助方法) function validatePermissionFields(string $table, array $fields): bool { try { $schema = DB::connection()->getSchemaBuilder()->getColumnListing($table); foreach ($fields as $field) { if (!in_array($field, $schema)) { throw new \InvalidArgumentException( "Field '{$field}' does not exist in table '{$table}'" ); } } return true; } catch (\Exception $e) { Log::error('字段验证失败', [ 'table' => $table, 'fields' => $fields, 'error' => $e->getMessage() ]); return false; } } ``` #### 错误做法 ```php // ❌ 错误:使用不存在的字段 #[DataScope( deptColumn: 'dept_id', // 如果表中字段是 'department_id' createdByColumn: 'created_by' // 如果表中字段是 'creator_id' )] public function getData(): Collection { // 会导致 SQL 错误或权限失效 return Order::query()->get(); } ``` ## ⚠️ 安全警告 ### 1. 防止权限绕过 ::: warning 安全风险 以下行为可能导致权限绕过,必须避免! ::: ```php // ❌ 危险:手动构建 SQL,绕过权限检查 $sql = "SELECT * FROM users WHERE dept_id = ?"; $users = DB::select($sql, [auth()->user()->dept_id]); // ❌ 危险:使用 whereRaw 绕过权限过滤 $users = User::whereRaw('1=1')->get(); // ❌ 危险:在管理员接口不使用权限控制 public function adminGetAllUsers(): Collection { // 危险!直接返回所有用户数据 return User::all(); } // ✅ 安全:始终使用权限系统 // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Service/Permission/UserService.php:94-98 #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['user'], createdByColumn: 'id' )] public function adminGetAllUsers(): Collection { return User::query()->get(); } ``` ### 2. 输入验证和安全过滤 ```php // ✅ 安全:验证用户输入 class SecurePermissionService { public function getFilteredData(array $filters): Collection { // 验证输入参数 $this->validateFilters($filters); // 使用白名单验证 $allowedColumns = ['name', 'email', 'status']; $filters = array_intersect_key($filters, array_flip($allowedColumns)); // 应用数据权限 Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); Context::setScopeType(ScopeType::DEPT_CREATED_BY); return User::query() ->when($filters['name'] ?? null, fn($q, $name) => $q->where('name', 'like', "%{$name}%")) ->when($filters['status'] ?? null, fn($q, $status) => $q->where('status', $status)) ->get(); } private function validateFilters(array $filters): void { foreach ($filters as $key => $value) { if (!in_array($key, ['name', 'email', 'status'])) { throw new \InvalidArgumentException("Invalid filter: {$key}"); } } } } ``` ### 3. 日志审计 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Library/DataPermission/Factory.php // ✅ 重要:对敏感操作启用日志 #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['financial_records'] )] public function getFinancialData(): Collection { // 记录敏感数据访问 Log::info('敏感数据访问', [ 'user_id' => auth()->id(), 'operation' => 'get_financial_data', 'ip' => request()->ip(), 'user_agent' => request()->userAgent(), 'timestamp' => now() ]); return FinancialRecord::query()->get(); } // ✅ 权限操作监听(建议实现) class DataPermissionLogger { public static function logPermissionAccess(User $user, string $operation): void { Log::info('数据权限访问', [ 'user_id' => $user->id, 'operation' => $operation, 'policy' => $user->getPolicy()?->toArray(), 'ip' => request()->ip(), 'timestamp' => now() ]); } } ``` ## 🛡️ 最佳实践 ### 1. 权限策略设计 #### 遵循最小权限原则 ```php // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Model/Permission/User.php:160-179 class DataPermissionService { public function getDataByUserRole(User $user): Collection { // 获取用户策略 $policy = $user->getPolicy(); if (!$policy) { // 无策略时使用最保守的权限 Context::setScopeType(ScopeType::CREATED_BY); Context::setOnlyTables(['user']); return collect(); } // 根据策略类型设置权限范围 $this->configureScopeByPolicy($policy); return User::query()->get(); } private function configureScopeByPolicy($policy): void { Context::setDeptColumn('dept_id'); Context::setCreatedByColumn('created_by'); // 根据策略类型配置 match($policy->policy_type->value) { 'ALL' => Context::setScopeType(ScopeType::DEPT), 'DEPT_TREE' => Context::setScopeType(ScopeType::DEPT_CREATED_BY), 'DEPT_SELF' => Context::setScopeType(ScopeType::DEPT_CREATED_BY), 'SELF' => Context::setScopeType(ScopeType::CREATED_BY), default => Context::setScopeType(ScopeType::CREATED_BY) }; } } ``` ### 2. 错误处理策略 ```php // ✅ 最佳实践:优雅的错误处理 class SafeDataPermissionService { public function executeWithFallback(callable $primaryAction, callable $fallbackAction = null): mixed { try { return $primaryAction(); } catch (\Exception $e) { // 记录权限相关错误 Log::warning('数据权限操作失败', [ 'user_id' => auth()->id(), 'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]); // 执行回退策略 if ($fallbackAction) { return $fallbackAction(); } return collect(); // 返回空集合 } } } // 使用示例 $service = new SafeDataPermissionService(); $data = $service->executeWithFallback( fn() => $this->getComplexDataWithPermissions(), fn() => $this->getBasicUserData() // 回退方案 ); ``` ### 3. 性能优化 ```php // ✅ 最佳实践:性能监控 class DataPermissionMonitor { public function monitorExecution(callable $action, string $operationName): mixed { $startTime = microtime(true); try { $result = $action(); $executionTime = (microtime(true) - $startTime) * 1000; // 记录性能指标 if ($executionTime > 100) { // 超过100ms记录 Log::warning('数据权限操作耗时较长', [ 'operation' => $operationName, 'execution_time' => $executionTime . 'ms', 'user_id' => auth()->id() ]); } return $result; } catch (\Throwable $e) { Log::error('数据权限操作异常', [ 'operation' => $operationName, 'error' => $e->getMessage() ]); throw $e; } } } ``` ### 4. 测试最佳实践 ```php // ✅ 最佳实践:权限测试 class DataPermissionTest extends TestCase { public function test_department_isolation(): void { // 创建测试数据 $dept1 = Department::factory()->create(); $dept2 = Department::factory()->create(); $user1 = User::factory()->create(['dept_id' => $dept1->id]); $user2 = User::factory()->create(['dept_id' => $dept2->id]); $order1 = Order::factory()->create(['dept_id' => $dept1->id]); $order2 = Order::factory()->create(['dept_id' => $dept2->id]); // 测试用户1只能看到自己部门的数据 $this->actingAs($user1); Context::setDeptColumn('dept_id'); Context::setScopeType(ScopeType::DEPT); Context::setOnlyTables(['orders']); $results = Order::query()->get(); $this->assertCount(1, $results); $this->assertEquals($order1->id, $results->first()->id); } public function test_user_policy_application(): void { // 来源:基于 /Users/zhuzhu/project/mineadmin/app/Model/Permission/User.php:160-179 $user = User::factory()->create(); // 创建用户策略 Policy::factory()->create([ 'user_id' => $user->id, 'policy_type' => PolicyType::DeptSelf, 'is_default' => true ]); // 验证策略获取 $policy = $user->getPolicy(); $this->assertNotNull($policy); $this->assertEquals(PolicyType::DeptSelf, $policy->policy_type); } } ``` ## 📝 检查清单 在使用数据权限系统前,请使用以下检查清单确保系统安全: ### 配置检查 * \[ ] **字段映射正确** - 确保 `deptColumn` 和 `createdByColumn` 对应实际数据表字段 * \[ ] **策略类型适当** - 根据业务需求选择合适的 `ScopeType` * \[ ] **表范围明确** - 使用 `onlyTables` 限制作用范围 * \[ ] **超级管理员检查** - 确认超级管理员绕过逻辑正确 ### 安全检查 * \[ ] **协程上下文管理** - 每个新协程都要重新设置上下文 * \[ ] **输入验证** - 对所有用户输入进行验证和清洗 * \[ ] **错误处理** - 实现适当的错误处理和回退策略 * \[ ] **日志审计** - 对敏感操作启用详细日志记录 ### 性能检查 * \[ ] **数据库索引** - 为权限字段创建适当的索引 * \[ ] **查询优化** - 避免 N+1 查询和不必要的联表操作 * \[ ] **监控告警** - 设置查询性能监控 ### 测试检查 * \[ ] **单元测试** - 编写各种权限场景的单元测试 * \[ ] **集成测试** - 测试权限系统与其他组件的集成 * \[ ] **边界测试** - 测试权限边界和异常情况 ## 总结 MineAdmin 数据权限系统的核心在于正确配置 Context 和 DataScope 注解。关键要点: 1. **严格管理协程上下文** - 避免权限泄露 2. **正确配置字段映射** - 确保权限过滤生效 3. **遵循最小权限原则** - 默认使用最保守的权限策略 4. **充分的错误处理** - 确保系统在异常情况下的安全性 5. **完善的测试覆盖** - 验证各种权限场景的正确性 遵循这些注意事项和最佳实践,可以确保 MineAdmin 数据权限系统在您的应用中安全、高效地运行。 --- --- url: /v3/front/high/store.md --- # 状态管理 (Store) MineAdmin 使用 [Pinia](https://pinia.vuejs.org/) 作为状态管理库,提供了一套完整的状态管理解决方案。系统内置了多个常用的 Store 模块,覆盖用户管理、标签页、插件系统、字典数据等核心功能。 ::: tip 自动导入说明 前端 `src/store/modules` 目录下的所有 Store 已配置自动引入,可直接使用无需显式导入。 **自动导入配置位置**:`vite/auto-import.ts` 中的 `dirs: ['./src/store/modules/**']` 配置 ::: ## useUserStore() 用户状态管理 Store,负责用户认证、权限管理、用户信息维护等核心功能。 **源码位置**: * **本地路径**:`web/src/store/modules/useUserStore.ts` * **GitHub地址**:[mineadmin/web/src/store/modules/useUserStore.ts](https://github.com/mineadmin/mineadmin/blob/master/web/src/store/modules/useUserStore.ts) ### 主要状态属性 | 属性名 | 类型 | 描述 | |--------|------|------| | `token` | `string` | 用户访问令牌 (Access Token) | | `refreshToken` | `string` | 刷新令牌 (Refresh Token) | | `expireAt` | `number` | 令牌过期时间戳 | | `userInfo` | `object \| null` | 用户基础信息 | | `menuList` | `array` | 用户菜单权限列表 | | `roleList` | `array` | 用户角色列表 | ### 核心方法 #### login(params: LoginParams) 用户登录方法,执行用户认证流程 ```typescript // 登录示例 const userStore = useUserStore() const loginData = { username: 'admin', password: '123456', captcha: 'abcd' } try { await userStore.login(loginData) // 登录成功,系统会自动跳转 } catch (error) { console.error('登录失败:', error) } ``` #### logout() 用户退出登录,清理认证信息和缓存 ```typescript // 退出登录 await userStore.logout() // 会自动清理:用户信息、令牌、页面缓存、标签页等 ``` #### requestUserInfo() 获取用户详细信息,包括权限和角色数据 ```typescript // 获取用户信息(通常在路由守卫中自动调用) await userStore.requestUserInfo() // 获取结果 const { userInfo, menuList, roleList } = userStore ``` ### 计算属性 #### isLogin 检查用户是否已登录 ```typescript const userStore = useUserStore() if (userStore.isLogin) { console.log('用户已登录') } else { console.log('用户未登录') } ``` ### 使用示例 #### 在组件中使用 ```vue ``` #### 在权限验证中使用 ```typescript // 检查用户权限 const hasPermission = (permission: string) => { const userStore = useUserStore() if (!userStore.isLogin) return false return userStore.menuList.some(menu => menu.permission === permission ) } // 检查用户角色 const hasRole = (role: string) => { const userStore = useUserStore() return userStore.roleList.some(r => r.code === role) } ``` ## useTabStore() 标签页状态管理 Store,负责管理多标签页导航、标签页缓存、页面状态保持等功能。 **源码位置**: * **本地路径**:`web/src/store/modules/useTabStore.ts` * **GitHub地址**:[mineadmin/web/src/store/modules/useTabStore.ts](https://github.com/mineadmin/mineadmin/blob/master/web/src/store/modules/useTabStore.ts) ### 主要状态属性 | 属性名 | 类型 | 描述 | |--------|------|------| | `tabs` | `MineTabbar[]` | 当前打开的标签页列表 | | `activeTab` | `string` | 当前激活的标签页名称 | ### 核心方法 #### addTab(tab: MineTabbar) 添加新标签页 ```typescript const tabStore = useTabStore() // 添加新标签页 tabStore.addTab({ name: 'user-list', title: '用户列表', path: '/admin/user', fullPath: '/admin/user?status=active', icon: 'i-heroicons:users' }) ``` #### closeTab(targetTab: MineTabbar) 关闭指定标签页 ```typescript // 关闭标签页 const targetTab = tabStore.tabs.find(tab => tab.name === 'user-list') if (targetTab) { tabStore.closeTab(targetTab) } ``` #### refreshTab() 刷新当前标签页 ```typescript // 刷新当前标签页(会重新加载页面组件) await tabStore.refreshTab() ``` #### closeOtherTab(currentTab: MineTabbar) 关闭除指定标签页外的其他标签页 ```typescript // 关闭其他标签页 const currentTab = tabStore.tabs.find(tab => tab.name === tabStore.activeTab) if (currentTab) { await tabStore.closeOtherTab(currentTab) } ``` #### clearTab() 清空所有标签页(保留固定标签页) ```typescript // 清空所有标签页 await tabStore.clearTab() ``` ### 使用示例 #### 在组件中管理标签页 ```vue ``` #### 在路由跳转中使用 ```typescript import { useRouter } from 'vue-router' const router = useRouter() const tabStore = useTabStore() // 编程式导航并添加标签页 const navigateToPage = (routeName: string, routeParams?: any) => { router.push({ name: routeName, params: routeParams }) // 标签页会通过路由守卫自动添加 // 也可以手动添加特定配置的标签页 } ``` ## usePluginStore() 插件系统状态管理 Store,负责插件的动态加载、启停控制、钩子调用等功能。 **源码位置**: * **本地路径**:`web/src/store/modules/usePluginStore.ts` * **GitHub地址**:[mineadmin/web/src/store/modules/usePluginStore.ts](https://github.com/mineadmin/mineadmin/blob/master/web/src/store/modules/usePluginStore.ts) ### 主要状态属性 | 属性名 | 类型 | 描述 | |--------|------|------| | `plugins` | `Map` | 已加载的插件配置映射 | | `enabledPlugins` | `Set` | 已启用的插件名称集合 | ### 核心方法 #### enabled(pluginName: string) 启用指定插件 ```typescript const pluginStore = usePluginStore() // 启用插件 pluginStore.enabled('mine-admin/app-store') ``` #### disabled(pluginName: string) 禁用指定插件 ```typescript // 禁用插件 pluginStore.disabled('mine-admin/demo') ``` #### callHooks(hookName: string, ...args: any\[]) 调用所有已启用插件的指定钩子 ```typescript // 调用登录钩子 await pluginStore.callHooks('login', loginFormData) // 调用网络请求钩子 await pluginStore.callHooks('networkRequest', requestConfig) ``` ### 使用示例 #### 动态控制插件状态 ```vue ``` #### 在HTTP拦截器中使用插件钩子 ```typescript // src/utils/http.ts const pluginStore = usePluginStore() // 请求拦截器中调用插件钩子 http.interceptors.request.use(async (config) => { // 调用所有插件的网络请求钩子 await pluginStore.callHooks('networkRequest', config) return config }) // 响应拦截器中调用插件钩子 http.interceptors.response.use(async (response) => { // 调用所有插件的网络响应钩子 await pluginStore.callHooks('networkResponse', response) return response }) ``` ## useDictStore() 字典数据状态管理 Store,负责系统字典数据的缓存、查询、更新等功能。 **源码位置**: * **本地路径**:`web/src/store/modules/useDictStore.ts` * **GitHub地址**:[mineadmin/web/src/store/modules/useDictStore.ts](https://github.com/mineadmin/mineadmin/blob/master/web/src/store/modules/useDictStore.ts) ### 主要状态属性 | 属性名 | 类型 | 描述 | |--------|------|------| | `dictData` | `Record` | 字典数据缓存,以字典编码为键 | | `lastUpdateTime` | `number` | 最后更新时间戳 | ### 字典数据类型 ```typescript interface DictItem { label: string // 显示标签 value: any // 实际值 color?: string // 颜色标识 status?: number // 状态(1: 启用, 0: 禁用) sort?: number // 排序 remark?: string // 备注 } ``` ### 核心方法 #### getDict(dictCode: string): Promise\ 获取指定编码的字典数据 ```typescript const dictStore = useDictStore() // 获取用户状态字典 const userStatusDict = await dictStore.getDict('user_status') console.log(userStatusDict) // [ // { label: '正常', value: 1, color: 'success' }, // { label: '禁用', value: 0, color: 'danger' } // ] ``` #### getDictLabel(dictCode: string, value: any): string 根据字典值获取对应标签 ```typescript // 获取状态值对应的标签 const statusLabel = await dictStore.getDictLabel('user_status', 1) console.log(statusLabel) // '正常' ``` #### refreshDict(dictCode?: string) 刷新字典数据 ```typescript // 刷新特定字典 await dictStore.refreshDict('user_status') // 刷新所有字典 await dictStore.refreshDict() ``` ### 使用示例 #### 在表单中使用字典 ```vue ``` #### 在表格中显示字典标签 ```vue ``` #### 创建字典选择组件 ```vue ``` ## 其他 Store 模块 除了上述核心 Store 外,系统还提供了其他辅助性的 Store 模块: ### useKeepAliveStore() 页面缓存管理,详见 [前端缓存系统](/v3/front/advanced/cache.md#页面缓存-keep-alive) ```typescript const keepAliveStore = useKeepAliveStore() // 添加页面到缓存 keepAliveStore.add('UserManagement') // 移除页面缓存 keepAliveStore.remove('UserManagement') // 清空所有缓存 keepAliveStore.clean() ``` ### useSettingStore() 系统设置管理,包括主题、语言、布局等配置 ```typescript const settingStore = useSettingStore() // 获取设置 const appSettings = settingStore.getSettings('app') // 更新设置 settingStore.updateSettings('app', { theme: 'dark', lang: 'zh-CN' }) ``` ## Store 最佳实践 ### 1. 状态更新 ```typescript // ✅ 推荐:使用 Store 方法更新状态 const userStore = useUserStore() await userStore.login(loginData) // ❌ 避免:直接修改 Store 状态 userStore.token = 'new-token' // 可能会丢失响应性 ``` ### 2. 错误处理 ```typescript // 统一的错误处理 const handleStoreAction = async (action: () => Promise, errorMessage = '操作失败') => { try { return await action() } catch (error) { console.error(error) ElMessage.error(errorMessage) throw error } } // 使用示例 await handleStoreAction( () => userStore.login(loginData), '登录失败,请检查用户名和密码' ) ``` ### 3. 组合使用多个Store ```typescript // 在一个操作中使用多个 Store const handleUserLogin = async (loginData: LoginParams) => { const userStore = useUserStore() const tabStore = useTabStore() const settingStore = useSettingStore() try { // 执行登录 await userStore.login(loginData) // 初始化用户设置 const userSettings = await settingStore.loadUserSettings() // 恢复用户的标签页状态 await tabStore.restoreUserTabs() ElMessage.success('登录成功') } catch (error) { ElMessage.error('登录失败') throw error } } ``` ### 4. 性能优化 ```typescript // 使用 storeToRefs 保持响应性 import { storeToRefs } from 'pinia' const userStore = useUserStore() const { userInfo, isLogin } = storeToRefs(userStore) // ✅ 保持响应性 const { login, logout } = userStore // ✅ 方法不需要 storeToRefs // ❌ 避免:直接解构响应式数据 const { userInfo, isLogin } = userStore // 会丢失响应性 ``` ## 相关文档 * [自动导入配置](/v3/front/advanced/auto-import.md) - Store 自动导入机制 * [前端缓存系统](/v3/front/advanced/cache.md) - 页面和数据缓存 * [插件系统](/v3/front/high/plugins.md) - 插件开发与管理 * [请求与拦截器](/v3/front/advanced/request.md) - HTTP 请求中的 Store 使用 --- --- url: /libs.md --- # 独立库 这里收录 MineAdmin 生态中独立维护、独立发版的库文档。它们不跟随 MineAdmin 主产品的 `v3`、`v4` 大版本节奏,因此统一放在 `/libs/{library}/latest/` 路径下。 ## 库列表 * [MaForm](/libs/ma-form/latest/) - `@mineadmin/form`,表单渲染与动态表单能力。 * [MaTable](/libs/ma-table/latest/) - `@mineadmin/table`,基础表格能力。 * [MaSearch](/libs/ma-search/latest/) - `@mineadmin/search`,搜索表单与筛选面板。 * [MaProTable](/libs/ma-pro-table/latest/) - `@mineadmin/pro-table`,面向业务 CRUD 的高级表格。 ## 版本规则 `latest` 表示当前推荐文档。只有某个库需要长期保留历史差异时,才会新增 `v1`、`v2` 等固定版本路径。 --- --- url: /backend/frameworks/hyperf/3.1/base/lifecycle.md --- # 生命周期 ::: tip 不论是 swoole 或 swow。在 MineAdmin 中都是由 Hyperf 通过[symfony/console](https://github.com/symfony/console) 组件接入 启动命令 `php bin/hyperf.php start` MineAdmin 是构建运行在 [PHP](https://php.net) + ([Swoole](https://swoole.com) or [Swow](https://github.com/swow/swow)) + [Hyperf](https://github.com/hyperf/hyperf) 上的,想要了解透彻 MineAdmin 的生命周期,那么理解基层架构的生命周期也是至关重要的。 本文将不再另行说明上述基层架构生命周期,如有兴趣请自行研究学习. 本文将更倾向与业务相关的生命周期描述 ::: ## 双 Token 认证刷新 双Token机制是指在用户登录过程中,除了传统的`Access Token`外,还引入了一个额外的`Refresh Token`。`Access Token`主要用于验证用户身份和保持用户会话, 而`Refresh Token`则用于在`Access Token`过期后重新获取新的`Access Token`。这种设计可以在保证安全性的同时, 提供更好的用户体验。 ::: tip 默认提供的应用程序认证机制都是由两个 token 来实现交互刷新鉴权的 也就是 `AccessToken` 以及 `RefreshToken` 而关于 Jwt 的生成和鉴权则统一由 MineAdmin 接入 [lcobucci/jwt](https://github.com/lcobucci/jwt) 组件而实现的 ::: *** ### 时序图 ```plantuml participant "客户端" as Client participant "服务端" as Server Client -> Server : 登录请求 Server -> Client : 登录成功,返回 access_token 和 refresh_token Client -> Local : 存储 access_token 和 refresh_token 到本地 Client -> Server : 发送请求 Server -> Client : 返回 401 错误码且本地 refresh_token 未过期 Client -> Queue : 暂存请求信息 Client -> Server : 用 refresh_token 换取新 token alt 换 token 接口返回 401 Client -> Local : 清除本地缓存 Client -> Server : 重定向到登录页面 else 换 token 成功 Client -> Local : 更新本地 token Client -> Server : 重试请求失败的接口 end ``` ### 流程图 ```plantuml start :登录成功; -> :存储 access_token 和 refresh_token 到本地; -> :发送请求; if (请求失败,code = 401 且本地 refresh_token 未过期) then (yes) -> :暂存请求信息到队列; -> :用 refresh_token 换新 token; if (换 token 接口返回 401) then (yes) -> :清除本地缓存,重定向到登录页面; else (no) -> :更新本地 token,重试请求失败的接口; endif else (no) endif ``` ### 讲解 在登录成功后,将 access token 与 refresh token 存储于本地。 当某次请求出现失败,且错误码为 401,同时本地的 refresh\_token 未过期时,需先将当前请求信息暂存至队列中。此队列旨在防止同一时刻多个请求同时去刷新 token。 随后,利用 refresh token 换取新的 access\_token 与 refresh\_token。 倘若换 token 的接口同样返回 401 错误码,则意味着 access\_token 与 refresh\_token 均已过期,此时需清除本地缓存,并重定向至登录页面。 若换 token 成功,则需更新本地 token,并重试之前失败的请求。 *** --- --- url: /backend/frameworks/hyperf/3.2/base/lifecycle.md --- # 生命周期 ::: tip 不论是 swoole 或 swow。在 MineAdmin 中都是由 Hyperf 通过[symfony/console](https://github.com/symfony/console) 组件接入 启动命令 `php bin/hyperf.php start` MineAdmin 是构建运行在 [PHP](https://php.net) + ([Swoole](https://swoole.com) or [Swow](https://github.com/swow/swow)) + [Hyperf](https://github.com/hyperf/hyperf) 上的,想要了解透彻 MineAdmin 的生命周期,那么理解基层架构的生命周期也是至关重要的。 本文将不再另行说明上述基层架构生命周期,如有兴趣请自行研究学习. 本文将更倾向与业务相关的生命周期描述 ::: ## 双 Token 认证刷新 双Token机制是指在用户登录过程中,除了传统的`Access Token`外,还引入了一个额外的`Refresh Token`。`Access Token`主要用于验证用户身份和保持用户会话, 而`Refresh Token`则用于在`Access Token`过期后重新获取新的`Access Token`。这种设计可以在保证安全性的同时, 提供更好的用户体验。 ::: tip 默认提供的应用程序认证机制都是由两个 token 来实现交互刷新鉴权的 也就是 `AccessToken` 以及 `RefreshToken` 而关于 Jwt 的生成和鉴权则统一由 MineAdmin 接入 [lcobucci/jwt](https://github.com/lcobucci/jwt) 组件而实现的 ::: *** ### 时序图 ```plantuml participant "客户端" as Client participant "服务端" as Server Client -> Server : 登录请求 Server -> Client : 登录成功,返回 access_token 和 refresh_token Client -> Local : 存储 access_token 和 refresh_token 到本地 Client -> Server : 发送请求 Server -> Client : 返回 401 错误码且本地 refresh_token 未过期 Client -> Queue : 暂存请求信息 Client -> Server : 用 refresh_token 换取新 token alt 换 token 接口返回 401 Client -> Local : 清除本地缓存 Client -> Server : 重定向到登录页面 else 换 token 成功 Client -> Local : 更新本地 token Client -> Server : 重试请求失败的接口 end ``` ### 流程图 ```plantuml start :登录成功; -> :存储 access_token 和 refresh_token 到本地; -> :发送请求; if (请求失败,code = 401 且本地 refresh_token 未过期) then (yes) -> :暂存请求信息到队列; -> :用 refresh_token 换新 token; if (换 token 接口返回 401) then (yes) -> :清除本地缓存,重定向到登录页面; else (no) -> :更新本地 token,重试请求失败的接口; endif else (no) endif ``` ### 讲解 在登录成功后,将 access token 与 refresh token 存储于本地。 当某次请求出现失败,且错误码为 401,同时本地的 refresh\_token 未过期时,需先将当前请求信息暂存至队列中。此队列旨在防止同一时刻多个请求同时去刷新 token。 随后,利用 refresh token 换取新的 access\_token 与 refresh\_token。 倘若换 token 的接口同样返回 401 错误码,则意味着 access\_token 与 refresh\_token 均已过期,此时需清除本地缓存,并重定向至登录页面。 若换 token 成功,则需更新本地 token,并重试之前失败的请求。 *** --- --- url: /v3/backend/base/lifecycle.md --- # 生命周期 本文已迁移到 [Hyperf 生命周期](/backend/frameworks/hyperf/3.2/base/lifecycle)。 新的后端文档按 [公共契约](/v3/backend/contracts/) 和 [框架实现](/backend/frameworks/hyperf/) 组织。旧地址保留用于兼容历史链接。 --- --- url: /backend/frameworks/hyperf/3.1/security/access.md --- # 用户授权(RBAC) ## 系统概述 MineAdmin采用基于角色的访问控制(RBAC)系统,结合JWT认证、多层权限验证和数据级权限控制,为企业级应用提供全面的安全保障。 ### 核心架构 ```plantuml @startuml !theme plain package "RBAC核心架构" { [用户 User] --> [角色 Role] [角色 Role] --> [菜单/权限 Menu] } package "权限验证流程" { [JWT Token] --> [用户认证] [用户认证] --> [角色权限检查] [角色权限检查] --> [数据权限过滤] [数据权限过滤] --> [操作审计] } package "中间件层" { component AccessTokenMiddleware component PermissionMiddleware component OperationMiddleware AccessTokenMiddleware --> PermissionMiddleware PermissionMiddleware --> OperationMiddleware } [JWT Token] --> AccessTokenMiddleware @enduml ``` ## 认证系统 ### JWT认证机制 使用双Token策略保障安全性: ```php // 登录认证流程 public function login(string $username, string $password): array { $user = $this->repository->findByUnameType($username, Type::SYSTEM); // 密码验证 if (!$user->verifyPassword($password)) { throw new BusinessException(ResultCode::UNPROCESSABLE_ENTITY, trans('auth.password_error')); } // 用户状态检查 if ($user->status->isDisable()) { throw new BusinessException(ResultCode::DISABLED); } // 生成Token $jwt = $this->getJwt(); return [ 'access_token' => $jwt->builderAccessToken((string) $user->id)->toString(), 'refresh_token' => $jwt->builderRefreshToken((string) $user->id)->toString(), 'expire_at' => (int) $jwt->getConfig('ttl', 0), ]; } ``` ### 密码安全 系统使用PHP内置的安全哈希函数: ```php // 密码设置 public function setPasswordAttribute($value): void { $this->attributes['password'] = password_hash((string) $value, \PASSWORD_DEFAULT); } // 密码验证 public function verifyPassword(string $password): bool { return password_verify($password, $this->password); } ``` ## 权限系统 ### 三层权限模型 ```plantuml @startuml !theme plain entity "USER" as user { * id : int -- username : string password : string status : enum } entity "ROLE" as role { * id : int -- name : string code : string sort : int } entity "MENU" as menu { * id : int -- name : string code : string type : string } entity "USER_BELONGS_ROLE" as user_role { * user_id : int * role_id : int } entity "ROLE_BELONGS_MENU" as role_menu { * role_id : int * menu_id : int } user ||--o{ user_role role ||--o{ user_role role ||--o{ role_menu menu ||--o{ role_menu @enduml ``` ### 权限检查实现 ```php // User模型中的权限检查方法 public function hasPermission(string $permission): bool { return $this->roles()->whereRelation('menus', 'name', $permission)->exists(); } public function getPermissions(): Collection { return $this->roles()->with('menus')->orderBy('sort')->get()->pluck('menus')->flatten(); } public function isSuperAdmin(): bool { return $this->roles()->where('code', 'SuperAdmin')->exists(); } ``` ### 权限注解使用 在控制器方法上使用`@Permission`注解进行权限控制: ```php use Mine\Annotation\Permission; class UserController { #[Permission(code: 'permission:user:index')] public function pageList(): Result { // 用户列表查询 } #[Permission(code: ['permission:user:save', 'permission:user:update'], operation: Permission::OPERATION_OR)] public function save(): Result { // 用户保存或更新 } #[Permission(code: ['permission:user:delete', 'permission:role:admin'], operation: Permission::OPERATION_AND)] public function delete(): Result { // 需要同时具备删除权限和管理员角色 } } ``` ## 中间件体系 ### 三层中间件保护 ```php #[Middleware(middleware: AccessTokenMiddleware::class, priority: 100)] #[Middleware(middleware: PermissionMiddleware::class, priority: 99)] #[Middleware(middleware: OperationMiddleware::class, priority: 98)] class AdminController { // 控制器逻辑 } ``` #### 1. AccessTokenMiddleware 验证访问令牌的有效性: ```php public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $token = $this->getTokenFromRequest($request); try { $jwt = $this->getJwt(); $token = $jwt->parseToken($token); // 检查黑名单 if ($jwt->isBlacklisted($token)) { throw new TokenValidException('Token已被列入黑名单'); } // 设置当前用户 $this->setCurrentUser($token); } catch (\Throwable $e) { throw new BusinessException(ResultCode::UNAUTHORIZED, $e->getMessage()); } return $handler->handle($request); } ``` #### 2. PermissionMiddleware 执行权限验证逻辑: ```php private function handlePermission(Permission $permission): void { $operation = $permission->getOperation(); $codes = $permission->getCode(); foreach ($codes as $code) { $hasPermission = $this->currentUser->user()->hasPermission($code); if ($operation === Permission::OPERATION_AND && !$hasPermission) { throw new BusinessException(code: ResultCode::FORBIDDEN); } if ($operation === Permission::OPERATION_OR && $hasPermission) { return; } } if ($operation === Permission::OPERATION_OR) { throw new BusinessException(code: ResultCode::FORBIDDEN); } } ``` #### 3. OperationMiddleware 记录操作日志: ```php public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); // 记录操作日志 $this->dispatcher->dispatch(new RequestOperationEvent( $this->user->id(), $operator->summary, $request->getUri()->getPath(), $request->getClientIps(), $request->getMethod(), )); return $response; } ``` ## API参考 ### CurrentUser服务 ```php use Mine\Support\CurrentUser; class ExampleController { public function __construct(private readonly CurrentUser $currentUser) {} public function getUserInfo(): array { $user = $this->currentUser->user(); return [ 'id' => $user->id, 'username' => $user->username, 'roles' => $user->roles, 'permissions' => $user->getPermissions(), 'is_super_admin' => $user->isSuperAdmin(), ]; } public function checkPermission(string $permission): bool { return $this->currentUser->user()->hasPermission($permission); } } ``` ### 权限验证方法 ```php // AND操作:需要同时具备所有权限 #[Permission(code: ['user:read', 'user:write'], operation: Permission::OPERATION_AND)] // OR操作:具备任一权限即可 #[Permission(code: ['user:read', 'admin:all'], operation: Permission::OPERATION_OR)] // 单一权限检查 #[Permission(code: 'user:delete')] // 程序化权限检查 if ($this->currentUser->user()->hasPermission('user:create')) { // 允许创建用户 } ``` ## 故障排除 ### 常见问题 #### 权限检查失败 **问题**: 用户无法访问有权限的资源 **调试步骤**: ```php // 1. 检查用户角色 $user = User::find($userId); dd($user->roles); // 2. 检查角色权限 foreach ($user->roles as $role) { dd($role->menus); } // 3. 检查权限码 $hasPermission = $user->hasPermission('permission:user:index'); dd($hasPermission); // 4. 检查SuperAdmin状态 dd($user->isSuperAdmin()); ``` --- --- url: /backend/frameworks/hyperf/3.2/security/access.md --- # 用户授权(RBAC) ## 系统概述 MineAdmin采用基于角色的访问控制(RBAC)系统,结合JWT认证、多层权限验证和数据级权限控制,为企业级应用提供全面的安全保障。 ### 核心架构 ```plantuml @startuml !theme plain package "RBAC核心架构" { [用户 User] --> [角色 Role] [角色 Role] --> [菜单/权限 Menu] } package "权限验证流程" { [JWT Token] --> [用户认证] [用户认证] --> [角色权限检查] [角色权限检查] --> [数据权限过滤] [数据权限过滤] --> [操作审计] } package "中间件层" { component AccessTokenMiddleware component PermissionMiddleware component OperationMiddleware AccessTokenMiddleware --> PermissionMiddleware PermissionMiddleware --> OperationMiddleware } [JWT Token] --> AccessTokenMiddleware @enduml ``` ## 认证系统 ### JWT认证机制 使用双Token策略保障安全性: ```php // 登录认证流程 public function login(string $username, string $password): array { $user = $this->repository->findByUnameType($username, Type::SYSTEM); // 密码验证 if (!$user->verifyPassword($password)) { throw new BusinessException(ResultCode::UNPROCESSABLE_ENTITY, trans('auth.password_error')); } // 用户状态检查 if ($user->status->isDisable()) { throw new BusinessException(ResultCode::DISABLED); } // 生成Token $jwt = $this->getJwt(); return [ 'access_token' => $jwt->builderAccessToken((string) $user->id)->toString(), 'refresh_token' => $jwt->builderRefreshToken((string) $user->id)->toString(), 'expire_at' => (int) $jwt->getConfig('ttl', 0), ]; } ``` ### 密码安全 系统使用PHP内置的安全哈希函数: ```php // 密码设置 public function setPasswordAttribute($value): void { $this->attributes['password'] = password_hash((string) $value, \PASSWORD_DEFAULT); } // 密码验证 public function verifyPassword(string $password): bool { return password_verify($password, $this->password); } ``` ## 权限系统 ### 三层权限模型 ```plantuml @startuml !theme plain entity "USER" as user { * id : int -- username : string password : string status : enum } entity "ROLE" as role { * id : int -- name : string code : string sort : int } entity "MENU" as menu { * id : int -- name : string code : string type : string } entity "USER_BELONGS_ROLE" as user_role { * user_id : int * role_id : int } entity "ROLE_BELONGS_MENU" as role_menu { * role_id : int * menu_id : int } user ||--o{ user_role role ||--o{ user_role role ||--o{ role_menu menu ||--o{ role_menu @enduml ``` ### 权限检查实现 ```php // User模型中的权限检查方法 public function hasPermission(string $permission): bool { return $this->roles()->whereRelation('menus', 'name', $permission)->exists(); } public function getPermissions(): Collection { return $this->roles()->with('menus')->orderBy('sort')->get()->pluck('menus')->flatten(); } public function isSuperAdmin(): bool { return $this->roles()->where('code', 'SuperAdmin')->exists(); } ``` ### 权限注解使用 在控制器方法上使用`@Permission`注解进行权限控制: ```php use Mine\Annotation\Permission; class UserController { #[Permission(code: 'permission:user:index')] public function pageList(): Result { // 用户列表查询 } #[Permission(code: ['permission:user:save', 'permission:user:update'], operation: Permission::OPERATION_OR)] public function save(): Result { // 用户保存或更新 } #[Permission(code: ['permission:user:delete', 'permission:role:admin'], operation: Permission::OPERATION_AND)] public function delete(): Result { // 需要同时具备删除权限和管理员角色 } } ``` ## 中间件体系 ### 三层中间件保护 ```php #[Middleware(middleware: AccessTokenMiddleware::class, priority: 100)] #[Middleware(middleware: PermissionMiddleware::class, priority: 99)] #[Middleware(middleware: OperationMiddleware::class, priority: 98)] class AdminController { // 控制器逻辑 } ``` #### 1. AccessTokenMiddleware 验证访问令牌的有效性: ```php public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $token = $this->getTokenFromRequest($request); try { $jwt = $this->getJwt(); $token = $jwt->parseToken($token); // 检查黑名单 if ($jwt->isBlacklisted($token)) { throw new TokenValidException('Token已被列入黑名单'); } // 设置当前用户 $this->setCurrentUser($token); } catch (\Throwable $e) { throw new BusinessException(ResultCode::UNAUTHORIZED, $e->getMessage()); } return $handler->handle($request); } ``` #### 2. PermissionMiddleware 执行权限验证逻辑: ```php private function handlePermission(Permission $permission): void { $operation = $permission->getOperation(); $codes = $permission->getCode(); foreach ($codes as $code) { $hasPermission = $this->currentUser->user()->hasPermission($code); if ($operation === Permission::OPERATION_AND && !$hasPermission) { throw new BusinessException(code: ResultCode::FORBIDDEN); } if ($operation === Permission::OPERATION_OR && $hasPermission) { return; } } if ($operation === Permission::OPERATION_OR) { throw new BusinessException(code: ResultCode::FORBIDDEN); } } ``` #### 3. OperationMiddleware 记录操作日志: ```php public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); // 记录操作日志 $this->dispatcher->dispatch(new RequestOperationEvent( $this->user->id(), $operator->summary, $request->getUri()->getPath(), $request->getClientIps(), $request->getMethod(), )); return $response; } ``` ## API参考 ### CurrentUser服务 ```php use Mine\Support\CurrentUser; class ExampleController { public function __construct(private readonly CurrentUser $currentUser) {} public function getUserInfo(): array { $user = $this->currentUser->user(); return [ 'id' => $user->id, 'username' => $user->username, 'roles' => $user->roles, 'permissions' => $user->getPermissions(), 'is_super_admin' => $user->isSuperAdmin(), ]; } public function checkPermission(string $permission): bool { return $this->currentUser->user()->hasPermission($permission); } } ``` ### 权限验证方法 ```php // AND操作:需要同时具备所有权限 #[Permission(code: ['user:read', 'user:write'], operation: Permission::OPERATION_AND)] // OR操作:具备任一权限即可 #[Permission(code: ['user:read', 'admin:all'], operation: Permission::OPERATION_OR)] // 单一权限检查 #[Permission(code: 'user:delete')] // 程序化权限检查 if ($this->currentUser->user()->hasPermission('user:create')) { // 允许创建用户 } ``` ## 故障排除 ### 常见问题 #### 权限检查失败 **问题**: 用户无法访问有权限的资源 **调试步骤**: ```php // 1. 检查用户角色 $user = User::find($userId); dd($user->roles); // 2. 检查角色权限 foreach ($user->roles as $role) { dd($role->menus); } // 3. 检查权限码 $hasPermission = $user->hasPermission('permission:user:index'); dd($hasPermission); // 4. 检查SuperAdmin状态 dd($user->isSuperAdmin()); ``` --- --- url: /v3/backend/security/access.md --- # 用户授权(RBAC) 本文已迁移到 [Hyperf 用户授权(RBAC)](/backend/frameworks/hyperf/3.2/security/access)。 Laravel 实现暂未提供用户授权章节。旧地址保留用于兼容历史链接。 --- --- url: /backend/frameworks/hyperf/3.1/security/passport.md --- # 用户认证 ::: tip MineAdmin 的认证流程由 [mineadmin/auth-jwt](https://github.com/mineadmin/JwtAuth) 组件加 [mineadmin/jwt](https://github.com/mineadmin/jwt) 组件接入 [lcobucci/jwt](https://github.com/lcobucci/jwt) 构建而成,本文将着重讲解如何在 MineAdmin 中使用 JWT 进行用户认证。 本文涵盖 JWT 认证的基本使用、安全配置、性能优化以及最佳实践,帮助开发者构建安全可靠的认证系统。 ::: ## 认证机制概述 MineAdmin 采用 JWT(JSON Web Token)双 token 认证机制: * **access\_token**: 用于业务接口访问,有效期较短(默认 1 小时) * **refresh\_token**: 用于无感刷新 access\_token,有效期较长(默认 2 小时) 这种设计在保证安全性的同时,提供了良好的用户体验。 ## 安全配置指南 ::: warning 重要安全提醒 1. **密钥安全**: JWT 密钥必须使用强随机字符串,长度至少 256 位 2. **环境隔离**: 生产环境和测试环境必须使用不同的 JWT 密钥 3. **传输安全**: 生产环境必须使用 HTTPS 传输 JWT token 4. **存储安全**: 客户端应将 token 存储在安全的地方(如 httpOnly cookie) 5. **时效控制**: 合理设置 token 有效期,避免长期有效的 token ::: ### JWT 密钥生成 生成安全的 JWT 密钥: ```bash # 生成 256 位随机密钥 openssl rand -base64 64 # 或使用 PHP 生成 php -r "echo base64_encode(random_bytes(64)) . PHP_EOL;" ``` ## 在控制器中快速获取当前用户 ::: danger 依赖注入范围限制 不建议在控制器以外注入此对象。对于 service 中操作 user、应将 user 实例传入到 service 方法中 从而保证获取用户是在 http 请求周期内。 **原因说明**: * `CurrentUser` 依赖于请求上下文中的 JWT token * 在非 HTTP 请求环境(如定时任务、队列消费者)中使用会导致错误 * Service 层应该保持无状态,便于测试和维护 ::: ### 基本用法 使用 `App\Http\CurrentUser` 快速获取当前请求的用户对象。该类提供了多种便捷方法来访问用户信息,无需每次都查询数据库。 ### 核心方法说明 * `user()`: 获取完整的用户模型实例(会触发数据库查询) * `id()`: 快速获取用户 ID(从 JWT token 直接读取,无数据库查询) * `refresh()`: 刷新当前用户的认证 token * `menus()`: 获取用户有权限的菜单列表 * `roles()`: 获取用户的角色信息 * `isSystem()`: 判断是否为系统用户 * `isSuperAdmin()`: 判断是否为超级管理员 ::: code-group ```php{2,5,8} [TestController] #[Middleware(AccessTokenMiddleware::class)] class TestController { public function __construct(private readonly CurrentUser $currentUser){}; public function test(){ return $this->success('CurrentUser: '. $this->currentUser->user()->username); } } ``` ```php [CurrentUser] userService->getInfo($this->id()); } // 刷新当前用户的 token、返回 [access_token=>'xxx',refresh_token=>'xxx'] public function refresh(): array { return $this->service->refreshToken($this->getToken()); } // 快速获取当前用户 id (不走 db 查询) public function id(): int { return (int) $this->getToken()->claims()->get(RegisteredClaims::ID); } /** * 用于获取当前用户的 菜单树状列表 * @return Collection */ public function menus(): Collection { // @phpstan-ignore-next-line return $this->user()->getMenus(); } /** * 用于获取当前用户的角色列表 [ [code=>'xxx',name=>'xxxx'] ] * @return Collection */ public function roles(): Collection { // @phpstan-ignore-next-line return $this->user()->getRoles()->map(static fn (Role $role) => $role->only(['name', 'code', 'remark'])); } // 判断当前用户的 user_type 是否为 system 类别 public function isSystem(): bool { return $this->user()->user_type === Type::SYSTEM; } // 判断当前用户是否具有超管权限 public function isSuperAdmin(): bool { return $this->user()->isSuperAdmin(); } } ``` ::: ## 为外部程序创建单独的 JWT 生成规则 ### 应用场景 在企业级应用开发中,通常需要将系统分为多个独立的应用域: * **管理后台**:供管理员使用的后台管理系统 * **前台应用**:面向最终用户的应用接口 * **第三方接入**:提供给合作伙伴的 API 接口 * **移动端应用**:iOS/Android 等移动端专用接口 每个应用域都应该使用独立的 JWT 配置,以实现: * **安全隔离**:不同应用使用不同的签名密钥 * **权限控制**:不同应用有不同的权限范围 * **配置独立**:可以为不同应用设置不同的过期时间等参数 ### 实施步骤 #### 步骤 1: 配置环境变量 在 `.env` 文件中新建独立的 JWT 密钥。建议为每个应用域配置独立的密钥: ```bash # 管理后台(默认) JWT_SECRET=your_admin_secret_here # 前台 API JWT_API_SECRET=your_api_secret_here # 移动端应用 JWT_MOBILE_SECRET=your_mobile_secret_here # 第三方接入 JWT_PARTNER_SECRET=your_partner_secret_here ``` #### 步骤 2: 配置 JWT 场景 在 `config/autoload/jwt.php` 中新建多个场景配置: #### 步骤 3: 创建专用中间件 为每个应用域创建专门的 token 验证中间件: #### 步骤 4: 控制器中使用中间件 在对应的控制器中使用相应的中间件进行用户验证: #### 步骤 5: 扩展认证服务 在 `PassportService` 中新增对应的登录方法: ::: code-group ```php[.env] #other ... MINE_API_SECERT=azOVxsOWt3r0ozZNz8Ss429ht0T8z6OpeIJAIwNp6X0xqrbEY2epfIWyxtC1qSNM8eD6/LQ/SahcQi2ByXa/2A== ``` ```php{46-80} [jwt.php] // config/autoload/jwt.php [ // jwt 配置 https://lcobucci-jwt.readthedocs.io/en/latest/ 'driver' => Jwt::class, // jwt 签名key 'key' => InMemory::base64Encoded(env('JWT_SECRET')), // jwt 签名算法 可选 https://lcobucci-jwt.readthedocs.io/en/latest/supported-algorithms/ 'alg' => new Sha256(), // token过期时间,单位为秒 (管理后台建议短一些) 'ttl' => (int) env('JWT_TTL', 3600), // 1小时 // 刷新token过期时间,单位为秒 'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 7200), // 2小时 // 黑名单模式 'blacklist' => [ // 是否开启黑名单 'enable' => env('JWT_BLACKLIST_ENABLE', true), // 黑名单缓存前缀 'prefix' => 'jwt_blacklist', // 黑名单缓存驱动 'connection' => 'default', // 黑名单缓存时间 该时间一定要设置比token过期时间要大一点,最好设置跟过期时间一样 'ttl' => (int) env('JWT_BLACKLIST_TTL', 7201), ], 'claims' => [ // 默认的jwt claims RegisteredClaims::ISSUER => (string) env('APP_NAME'), RegisteredClaims::AUDIENCE => 'admin', // 明确标识受众 ], ], // 前台 API 场景 'api' => [ 'key' => InMemory::base64Encoded(env('JWT_API_SECRET')), 'ttl' => (int) env('JWT_API_TTL', 7200), // 2小时,前台可以长一些 'refresh_ttl' => (int) env('JWT_API_REFRESH_TTL', 86400), // 24小时 'claims' => [ RegisteredClaims::ISSUER => (string) env('APP_NAME'), RegisteredClaims::AUDIENCE => 'api', ], ], // 移动端场景 'mobile' => [ 'key' => InMemory::base64Encoded(env('JWT_MOBILE_SECRET')), 'ttl' => (int) env('JWT_MOBILE_TTL', 86400), // 24小时,移动端更长 'refresh_ttl' => (int) env('JWT_MOBILE_REFRESH_TTL', 604800), // 7天 'blacklist' => [ 'enable' => true, 'prefix' => 'jwt_mobile_blacklist', 'ttl' => (int) env('JWT_MOBILE_BLACKLIST_TTL', 604801), ], 'claims' => [ RegisteredClaims::ISSUER => (string) env('APP_NAME'), RegisteredClaims::AUDIENCE => 'mobile', ], ], // 第三方合作伙伴场景 'partner' => [ 'key' => InMemory::base64Encoded(env('JWT_PARTNER_SECRET')), 'ttl' => (int) env('JWT_PARTNER_TTL', 3600), // 1小时,第三方建议短期 'refresh_ttl' => (int) env('JWT_PARTNER_REFRESH_TTL', 7200), // 2小时 'claims' => [ RegisteredClaims::ISSUER => (string) env('APP_NAME'), RegisteredClaims::AUDIENCE => 'partner', ], ], ]; ``` ```php{20-24} [ApiTokenMiddleware] jwtFactory->get('api'); } } ``` ```php{36-81} [TestController] input('username'); $password = (string) $request->input('password'); $ip = Arr::first(array: $request->getClientIps(), callback: static fn ($val) => $val ?: null, default: '0.0.0.0'); $browser = $request->header('User-Agent') ?: 'unknown'; // todo 用户系统的获取 $os = $request->header('User-Agent') ?: 'unknown'; return $this->success( $this->passportService->loginApi( $username, $password, Type::User, $ip, $browser, $os ) ); } ``` ```php{48-70} [PassportService] namespace App\Service; use App\Exception\BusinessException; use App\Exception\JwtInBlackException; use App\Http\Common\ResultCode; use App\Model\Enums\User\Type; use App\Repository\Permission\UserRepository; use Lcobucci\JWT\Token\RegisteredClaims; use Lcobucci\JWT\UnencryptedToken; use Mine\Jwt\Factory; use Mine\Jwt\JwtInterface; use Mine\JwtAuth\Event\UserLoginEvent; use Mine\JwtAuth\Interfaces\CheckTokenInterface; use Psr\EventDispatcher\EventDispatcherInterface; final class PassportService extends IService implements CheckTokenInterface { /** * @var string jwt场景 */ private string $jwt = 'default'; public function __construct( protected readonly UserRepository $repository, protected readonly Factory $jwtFactory, protected readonly EventDispatcherInterface $dispatcher ) {} /** * @return array */ public function login(string $username, string $password, Type $userType = Type::SYSTEM, string $ip = '0.0.0.0', string $browser = 'unknown', string $os = 'unknown'): array { $user = $this->repository->findByUnameType($username, $userType); if (! $user->verifyPassword($password)) { $this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser, false)); throw new BusinessException(ResultCode::UNPROCESSABLE_ENTITY, trans('auth.password_error')); } $this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser)); $jwt = $this->getJwt(); return [ 'access_token' => $jwt->builderAccessToken((string) $user->id)->toString(), 'refresh_token' => $jwt->builderRefreshToken((string) $user->id)->toString(), 'expire_at' => (int) $jwt->getConfig('ttl', 0), ]; } /** * @return array */ public function loginApi(string $username, string $password, Type $userType = Type::SYSTEM, string $ip = '0.0.0.0', string $browser = 'unknown', string $os = 'unknown'): array { $user = $this->repository->findByUnameType($username, $userType); if (! $user->verifyPassword($password)) { $this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser, false)); throw new BusinessException(ResultCode::UNPROCESSABLE_ENTITY, trans('auth.password_error')); } $this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser)); $jwt = $this->getApiJwt(); return [ 'access_token' => $jwt->builderAccessToken((string) $user->id)->toString(), 'refresh_token' => $jwt->builderRefreshToken((string) $user->id)->toString(), 'expire_at' => (int) $jwt->getConfig('ttl', 0), ]; } public function getApiJwt(): JwtInterface{ // 填写上一步的场景值 return $this->jwtFactory->get('api'); } public function getJwt(): JwtInterface { return $this->jwtFactory->get($this->jwt); } ``` ::: ## JWT 核心概念详解 ::: tip JWT 基础知识 如果您对 JWT(JSON Web Token)的基础概念还不够熟悉,建议先阅读 [JWT 官方文档](https://jwt.io/introduction) 了解基本原理。 ::: ### JWT 结构分析 JWT 由三部分组成,用点(.)分隔: ``` header.payload.signature ``` #### 1. Header(头部) ```json { "alg": "HS256", "typ": "JWT" } ``` #### 2. Payload(载荷) ```json { "id": "123", "iss": "MineAdmin", "aud": "admin", "exp": 1640995200, "iat": 1640991600, "nbf": 1640991600 } ``` 字段说明: * `id`: 用户 ID * `iss`: 签发者(Issuer) * `aud`: 受众(Audience) * `exp`: 过期时间(Expiration Time) * `iat`: 签发时间(Issued At) * `nbf`: 生效时间(Not Before) #### 3. Signature(签名) ``` HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) ``` ### 双 Token 认证机制详解 MineAdmin 采用双 token 设计,这是一种安全性和用户体验的最佳平衡方案。 #### Token 类型对比 | 特性 | Access Token | Refresh Token | |------------|------------------|-------------------| | **用途** | 业务接口访问 | 刷新 access\_token | | **有效期** | 短期(1-4小时) | 长期(2-24小时) | | **使用频率** | 每次 API 调用 | 仅在刷新时使用 | | **安全风险** | 低(短期有效) | 中(需妥善保管) | | **存储位置** | 内存/临时存储 | 安全存储 | #### 双 Token 工作流程 ```mermaid sequenceDiagram participant Client as 客户端 participant Server as 服务器 participant Redis as Redis缓存 Client->>Server: 1. 登录请求 Server->>Redis: 2. 验证用户凭据 Server->>Client: 3. 返回 access_token + refresh_token loop 业务请求 Client->>Server: 4. 携带 access_token 访问 API Server->>Client: 5. 返回业务数据 end Client->>Server: 6. access_token 过期,使用 refresh_token 刷新 Server->>Redis: 7. 验证 refresh_token 并标记为已使用 Server->>Client: 8. 返回新的 access_token + refresh_token ``` #### Token 内容差异 **Access Token Claims:** ```json { "id": "123", "iss": "MineAdmin", "aud": "admin", "exp": 1640995200, "iat": 1640991600, "nbf": 1640991600 } ``` **Refresh Token Claims:** ```json { "id": "123", "iss": "MineAdmin", "aud": "admin", "sub": "refresh", "exp": 1641002400, "iat": 1640991600, "nbf": 1640991600 } ``` 关键字段说明: * `sub`: 标识这是刷新 token * `exp`: 更长的过期时间 #### 关键差异说明 1. **`sub` 声明**: refresh\_token 包含 `"sub": "refresh"` 声明,用于标识其用途 2. **使用限制**: 每个 refresh\_token 只能使用一次,使用后立即失效 3. **安全机制**: 刷新时会生成全新的 token 对,防止 token 重放攻击 ### 中间件验证机制 MineAdmin 提供了两个专门的中间件来处理不同类型的 token: #### AccessTokenMiddleware * **职责**: 验证业务访问 token * **应用场景**: 所有需要用户身份认证的业务接口 * **验证逻辑**: 检查 token 有效性、是否在黑名单、权限范围等 #### RefreshTokenMiddleware * **职责**: 验证刷新 token * **应用场景**: 仅用于 token 刷新接口 * **验证逻辑**: 检查 `sub` 声明、一次性使用限制等 #### 自定义中间件示例 ```php namespace App\Http\Common\Middleware; use Mine\JwtAuth\Middleware\AbstractTokenMiddleware; class CustomTokenMiddleware extends AbstractTokenMiddleware { public function getJwt(): JwtInterface { // 指定使用的 JWT 场景 return $this->jwtFactory->get('api'); } protected function validateCustomClaims(UnencryptedToken $token): void { // 自定义验证逻辑 $audience = $token->claims()->get(RegisteredClaims::AUDIENCE); if ($audience !== 'api') { throw new InvalidTokenException('Invalid token audience'); } } } ``` ### 安全考虑 #### 1. Token 生命周期管理 * Access token 应该设置较短的有效期(1-4小时) * Refresh token 有效期应该适中(2-24小时) * 避免设置永不过期的 token #### 2. 黑名单机制 * 登出时应该将 token 加入黑名单 * 密码修改时应该使所有 token 失效 * 定期清理过期的黑名单记录 #### 3. 安全存储 * 客户端应该安全存储 refresh token * 避免将敏感信息放入 JWT payload * 使用 HTTPS 传输所有包含 token 的请求 ## 安全最佳实践 ### 1. 生产环境安全配置 ::: danger 生产环境必读 在生产环境部署前,请务必检查以下安全配置: ::: ```php // .env 生产环境配置示例 JWT_SECRET=your_super_secure_256_bit_key_here JWT_API_SECRET=another_super_secure_256_bit_key_here JWT_TTL=3600 // 1小时,建议不超过4小时 JWT_REFRESH_TTL=7200 // 2小时,建议不超过24小时 JWT_BLACKLIST_TTL=7201 // 比 refresh_ttl 多1秒 ``` --- --- url: /backend/frameworks/hyperf/3.2/security/passport.md --- # 用户认证 ::: tip MineAdmin 的认证流程由 [mineadmin/auth-jwt](https://github.com/mineadmin/JwtAuth) 组件加 [mineadmin/jwt](https://github.com/mineadmin/jwt) 组件接入 [lcobucci/jwt](https://github.com/lcobucci/jwt) 构建而成,本文将着重讲解如何在 MineAdmin 中使用 JWT 进行用户认证。 本文涵盖 JWT 认证的基本使用、安全配置、性能优化以及最佳实践,帮助开发者构建安全可靠的认证系统。 ::: ## 认证机制概述 MineAdmin 采用 JWT(JSON Web Token)双 token 认证机制: * **access\_token**: 用于业务接口访问,有效期较短(默认 1 小时) * **refresh\_token**: 用于无感刷新 access\_token,有效期较长(默认 2 小时) 这种设计在保证安全性的同时,提供了良好的用户体验。 ## 安全配置指南 ::: warning 重要安全提醒 1. **密钥安全**: JWT 密钥必须使用强随机字符串,长度至少 256 位 2. **环境隔离**: 生产环境和测试环境必须使用不同的 JWT 密钥 3. **传输安全**: 生产环境必须使用 HTTPS 传输 JWT token 4. **存储安全**: 客户端应将 token 存储在安全的地方(如 httpOnly cookie) 5. **时效控制**: 合理设置 token 有效期,避免长期有效的 token ::: ### JWT 密钥生成 生成安全的 JWT 密钥: ```bash # 生成 256 位随机密钥 openssl rand -base64 64 # 或使用 PHP 生成 php -r "echo base64_encode(random_bytes(64)) . PHP_EOL;" ``` ## 在控制器中快速获取当前用户 ::: danger 依赖注入范围限制 不建议在控制器以外注入此对象。对于 service 中操作 user、应将 user 实例传入到 service 方法中 从而保证获取用户是在 http 请求周期内。 **原因说明**: * `CurrentUser` 依赖于请求上下文中的 JWT token * 在非 HTTP 请求环境(如定时任务、队列消费者)中使用会导致错误 * Service 层应该保持无状态,便于测试和维护 ::: ### 基本用法 使用 `App\Http\CurrentUser` 快速获取当前请求的用户对象。该类提供了多种便捷方法来访问用户信息,无需每次都查询数据库。 ### 核心方法说明 * `user()`: 获取完整的用户模型实例(会触发数据库查询) * `id()`: 快速获取用户 ID(从 JWT token 直接读取,无数据库查询) * `refresh()`: 刷新当前用户的认证 token * `menus()`: 获取用户有权限的菜单列表 * `roles()`: 获取用户的角色信息 * `isSystem()`: 判断是否为系统用户 * `isSuperAdmin()`: 判断是否为超级管理员 ::: code-group ```php{2,5,8} [TestController] #[Middleware(AccessTokenMiddleware::class)] class TestController { public function __construct(private readonly CurrentUser $currentUser){}; public function test(){ return $this->success('CurrentUser: '. $this->currentUser->user()->username); } } ``` ```php [CurrentUser] userService->getInfo($this->id()); } // 刷新当前用户的 token、返回 [access_token=>'xxx',refresh_token=>'xxx'] public function refresh(): array { return $this->service->refreshToken($this->getToken()); } // 快速获取当前用户 id (不走 db 查询) public function id(): int { return (int) $this->getToken()->claims()->get(RegisteredClaims::ID); } /** * 用于获取当前用户的 菜单树状列表 * @return Collection */ public function menus(): Collection { // @phpstan-ignore-next-line return $this->user()->getMenus(); } /** * 用于获取当前用户的角色列表 [ [code=>'xxx',name=>'xxxx'] ] * @return Collection */ public function roles(): Collection { // @phpstan-ignore-next-line return $this->user()->getRoles()->map(static fn (Role $role) => $role->only(['name', 'code', 'remark'])); } // 判断当前用户的 user_type 是否为 system 类别 public function isSystem(): bool { return $this->user()->user_type === Type::SYSTEM; } // 判断当前用户是否具有超管权限 public function isSuperAdmin(): bool { return $this->user()->isSuperAdmin(); } } ``` ::: ## 为外部程序创建单独的 JWT 生成规则 ### 应用场景 在企业级应用开发中,通常需要将系统分为多个独立的应用域: * **管理后台**:供管理员使用的后台管理系统 * **前台应用**:面向最终用户的应用接口 * **第三方接入**:提供给合作伙伴的 API 接口 * **移动端应用**:iOS/Android 等移动端专用接口 每个应用域都应该使用独立的 JWT 配置,以实现: * **安全隔离**:不同应用使用不同的签名密钥 * **权限控制**:不同应用有不同的权限范围 * **配置独立**:可以为不同应用设置不同的过期时间等参数 ### 实施步骤 #### 步骤 1: 配置环境变量 在 `.env` 文件中新建独立的 JWT 密钥。建议为每个应用域配置独立的密钥: ```bash # 管理后台(默认) JWT_SECRET=your_admin_secret_here # 前台 API JWT_API_SECRET=your_api_secret_here # 移动端应用 JWT_MOBILE_SECRET=your_mobile_secret_here # 第三方接入 JWT_PARTNER_SECRET=your_partner_secret_here ``` #### 步骤 2: 配置 JWT 场景 在 `config/autoload/jwt.php` 中新建多个场景配置: #### 步骤 3: 创建专用中间件 为每个应用域创建专门的 token 验证中间件: #### 步骤 4: 控制器中使用中间件 在对应的控制器中使用相应的中间件进行用户验证: #### 步骤 5: 扩展认证服务 在 `PassportService` 中新增对应的登录方法: ::: code-group ```php[.env] #other ... MINE_API_SECERT=azOVxsOWt3r0ozZNz8Ss429ht0T8z6OpeIJAIwNp6X0xqrbEY2epfIWyxtC1qSNM8eD6/LQ/SahcQi2ByXa/2A== ``` ```php{46-80} [jwt.php] // config/autoload/jwt.php [ // jwt 配置 https://lcobucci-jwt.readthedocs.io/en/latest/ 'driver' => Jwt::class, // jwt 签名key 'key' => InMemory::base64Encoded(env('JWT_SECRET')), // jwt 签名算法 可选 https://lcobucci-jwt.readthedocs.io/en/latest/supported-algorithms/ 'alg' => new Sha256(), // token过期时间,单位为秒 (管理后台建议短一些) 'ttl' => (int) env('JWT_TTL', 3600), // 1小时 // 刷新token过期时间,单位为秒 'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 7200), // 2小时 // 黑名单模式 'blacklist' => [ // 是否开启黑名单 'enable' => env('JWT_BLACKLIST_ENABLE', true), // 黑名单缓存前缀 'prefix' => 'jwt_blacklist', // 黑名单缓存驱动 'connection' => 'default', // 黑名单缓存时间 该时间一定要设置比token过期时间要大一点,最好设置跟过期时间一样 'ttl' => (int) env('JWT_BLACKLIST_TTL', 7201), ], 'claims' => [ // 默认的jwt claims RegisteredClaims::ISSUER => (string) env('APP_NAME'), RegisteredClaims::AUDIENCE => 'admin', // 明确标识受众 ], ], // 前台 API 场景 'api' => [ 'key' => InMemory::base64Encoded(env('JWT_API_SECRET')), 'ttl' => (int) env('JWT_API_TTL', 7200), // 2小时,前台可以长一些 'refresh_ttl' => (int) env('JWT_API_REFRESH_TTL', 86400), // 24小时 'claims' => [ RegisteredClaims::ISSUER => (string) env('APP_NAME'), RegisteredClaims::AUDIENCE => 'api', ], ], // 移动端场景 'mobile' => [ 'key' => InMemory::base64Encoded(env('JWT_MOBILE_SECRET')), 'ttl' => (int) env('JWT_MOBILE_TTL', 86400), // 24小时,移动端更长 'refresh_ttl' => (int) env('JWT_MOBILE_REFRESH_TTL', 604800), // 7天 'blacklist' => [ 'enable' => true, 'prefix' => 'jwt_mobile_blacklist', 'ttl' => (int) env('JWT_MOBILE_BLACKLIST_TTL', 604801), ], 'claims' => [ RegisteredClaims::ISSUER => (string) env('APP_NAME'), RegisteredClaims::AUDIENCE => 'mobile', ], ], // 第三方合作伙伴场景 'partner' => [ 'key' => InMemory::base64Encoded(env('JWT_PARTNER_SECRET')), 'ttl' => (int) env('JWT_PARTNER_TTL', 3600), // 1小时,第三方建议短期 'refresh_ttl' => (int) env('JWT_PARTNER_REFRESH_TTL', 7200), // 2小时 'claims' => [ RegisteredClaims::ISSUER => (string) env('APP_NAME'), RegisteredClaims::AUDIENCE => 'partner', ], ], ]; ``` ```php{20-24} [ApiTokenMiddleware] jwtFactory->get('api'); } } ``` ```php{36-81} [TestController] input('username'); $password = (string) $request->input('password'); $ip = Arr::first(array: $request->getClientIps(), callback: static fn ($val) => $val ?: null, default: '0.0.0.0'); $browser = $request->header('User-Agent') ?: 'unknown'; // todo 用户系统的获取 $os = $request->header('User-Agent') ?: 'unknown'; return $this->success( $this->passportService->loginApi( $username, $password, Type::User, $ip, $browser, $os ) ); } ``` ```php{48-70} [PassportService] namespace App\Service; use App\Exception\BusinessException; use App\Exception\JwtInBlackException; use App\Http\Common\ResultCode; use App\Model\Enums\User\Type; use App\Repository\Permission\UserRepository; use Lcobucci\JWT\Token\RegisteredClaims; use Lcobucci\JWT\UnencryptedToken; use Mine\Jwt\Factory; use Mine\Jwt\JwtInterface; use Mine\JwtAuth\Event\UserLoginEvent; use Mine\JwtAuth\Interfaces\CheckTokenInterface; use Psr\EventDispatcher\EventDispatcherInterface; final class PassportService extends IService implements CheckTokenInterface { /** * @var string jwt场景 */ private string $jwt = 'default'; public function __construct( protected readonly UserRepository $repository, protected readonly Factory $jwtFactory, protected readonly EventDispatcherInterface $dispatcher ) {} /** * @return array */ public function login(string $username, string $password, Type $userType = Type::SYSTEM, string $ip = '0.0.0.0', string $browser = 'unknown', string $os = 'unknown'): array { $user = $this->repository->findByUnameType($username, $userType); if (! $user->verifyPassword($password)) { $this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser, false)); throw new BusinessException(ResultCode::UNPROCESSABLE_ENTITY, trans('auth.password_error')); } $this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser)); $jwt = $this->getJwt(); return [ 'access_token' => $jwt->builderAccessToken((string) $user->id)->toString(), 'refresh_token' => $jwt->builderRefreshToken((string) $user->id)->toString(), 'expire_at' => (int) $jwt->getConfig('ttl', 0), ]; } /** * @return array */ public function loginApi(string $username, string $password, Type $userType = Type::SYSTEM, string $ip = '0.0.0.0', string $browser = 'unknown', string $os = 'unknown'): array { $user = $this->repository->findByUnameType($username, $userType); if (! $user->verifyPassword($password)) { $this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser, false)); throw new BusinessException(ResultCode::UNPROCESSABLE_ENTITY, trans('auth.password_error')); } $this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser)); $jwt = $this->getApiJwt(); return [ 'access_token' => $jwt->builderAccessToken((string) $user->id)->toString(), 'refresh_token' => $jwt->builderRefreshToken((string) $user->id)->toString(), 'expire_at' => (int) $jwt->getConfig('ttl', 0), ]; } public function getApiJwt(): JwtInterface{ // 填写上一步的场景值 return $this->jwtFactory->get('api'); } public function getJwt(): JwtInterface { return $this->jwtFactory->get($this->jwt); } ``` ::: ## JWT 核心概念详解 ::: tip JWT 基础知识 如果您对 JWT(JSON Web Token)的基础概念还不够熟悉,建议先阅读 [JWT 官方文档](https://jwt.io/introduction) 了解基本原理。 ::: ### JWT 结构分析 JWT 由三部分组成,用点(.)分隔: ``` header.payload.signature ``` #### 1. Header(头部) ```json { "alg": "HS256", "typ": "JWT" } ``` #### 2. Payload(载荷) ```json { "id": "123", "iss": "MineAdmin", "aud": "admin", "exp": 1640995200, "iat": 1640991600, "nbf": 1640991600 } ``` 字段说明: * `id`: 用户 ID * `iss`: 签发者(Issuer) * `aud`: 受众(Audience) * `exp`: 过期时间(Expiration Time) * `iat`: 签发时间(Issued At) * `nbf`: 生效时间(Not Before) #### 3. Signature(签名) ``` HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) ``` ### 双 Token 认证机制详解 MineAdmin 采用双 token 设计,这是一种安全性和用户体验的最佳平衡方案。 #### Token 类型对比 | 特性 | Access Token | Refresh Token | |------------|------------------|-------------------| | **用途** | 业务接口访问 | 刷新 access\_token | | **有效期** | 短期(1-4小时) | 长期(2-24小时) | | **使用频率** | 每次 API 调用 | 仅在刷新时使用 | | **安全风险** | 低(短期有效) | 中(需妥善保管) | | **存储位置** | 内存/临时存储 | 安全存储 | #### 双 Token 工作流程 ```mermaid sequenceDiagram participant Client as 客户端 participant Server as 服务器 participant Redis as Redis缓存 Client->>Server: 1. 登录请求 Server->>Redis: 2. 验证用户凭据 Server->>Client: 3. 返回 access_token + refresh_token loop 业务请求 Client->>Server: 4. 携带 access_token 访问 API Server->>Client: 5. 返回业务数据 end Client->>Server: 6. access_token 过期,使用 refresh_token 刷新 Server->>Redis: 7. 验证 refresh_token 并标记为已使用 Server->>Client: 8. 返回新的 access_token + refresh_token ``` #### Token 内容差异 **Access Token Claims:** ```json { "id": "123", "iss": "MineAdmin", "aud": "admin", "exp": 1640995200, "iat": 1640991600, "nbf": 1640991600 } ``` **Refresh Token Claims:** ```json { "id": "123", "iss": "MineAdmin", "aud": "admin", "sub": "refresh", "exp": 1641002400, "iat": 1640991600, "nbf": 1640991600 } ``` 关键字段说明: * `sub`: 标识这是刷新 token * `exp`: 更长的过期时间 #### 关键差异说明 1. **`sub` 声明**: refresh\_token 包含 `"sub": "refresh"` 声明,用于标识其用途 2. **使用限制**: 每个 refresh\_token 只能使用一次,使用后立即失效 3. **安全机制**: 刷新时会生成全新的 token 对,防止 token 重放攻击 ### 中间件验证机制 MineAdmin 提供了两个专门的中间件来处理不同类型的 token: #### AccessTokenMiddleware * **职责**: 验证业务访问 token * **应用场景**: 所有需要用户身份认证的业务接口 * **验证逻辑**: 检查 token 有效性、是否在黑名单、权限范围等 #### RefreshTokenMiddleware * **职责**: 验证刷新 token * **应用场景**: 仅用于 token 刷新接口 * **验证逻辑**: 检查 `sub` 声明、一次性使用限制等 #### 自定义中间件示例 ```php namespace App\Http\Common\Middleware; use Mine\JwtAuth\Middleware\AbstractTokenMiddleware; class CustomTokenMiddleware extends AbstractTokenMiddleware { public function getJwt(): JwtInterface { // 指定使用的 JWT 场景 return $this->jwtFactory->get('api'); } protected function validateCustomClaims(UnencryptedToken $token): void { // 自定义验证逻辑 $audience = $token->claims()->get(RegisteredClaims::AUDIENCE); if ($audience !== 'api') { throw new InvalidTokenException('Invalid token audience'); } } } ``` ### 安全考虑 #### 1. Token 生命周期管理 * Access token 应该设置较短的有效期(1-4小时) * Refresh token 有效期应该适中(2-24小时) * 避免设置永不过期的 token #### 2. 黑名单机制 * 登出时应该将 token 加入黑名单 * 密码修改时应该使所有 token 失效 * 定期清理过期的黑名单记录 #### 3. 安全存储 * 客户端应该安全存储 refresh token * 避免将敏感信息放入 JWT payload * 使用 HTTPS 传输所有包含 token 的请求 ## 安全最佳实践 ### 1. 生产环境安全配置 ::: danger 生产环境必读 在生产环境部署前,请务必检查以下安全配置: ::: ```php // .env 生产环境配置示例 JWT_SECRET=your_super_secure_256_bit_key_here JWT_API_SECRET=another_super_secure_256_bit_key_here JWT_TTL=3600 // 1小时,建议不超过4小时 JWT_REFRESH_TTL=7200 // 2小时,建议不超过24小时 JWT_BLACKLIST_TTL=7201 // 比 refresh_ttl 多1秒 ``` --- --- url: /v3/backend/security/passport.md --- # 用户认证 本文已迁移到 [Hyperf 用户认证](/backend/frameworks/hyperf/3.2/security/passport)。 Laravel 实现暂未提供用户认证章节。旧地址保留用于兼容历史链接。 --- --- url: /v3/front/advanced/login-welcome.md --- # 登录与欢迎页 :::tip 概述 本章节详细介绍 MineAdmin 3.0 的登录页面架构、登录流程处理、Token 管理机制,以及登录成功后的欢迎页配置。内容包括组件结构分析、数据流转过程、路由守卫机制和自定义配置方法。 **重要说明**:本文档中所有代码示例均来自 MineAdmin 开源项目的实际代码,源代码位于 [GitHub 仓库](https://github.com/mineadmin/mineadmin)。 ::: ## 登录页面架构 ### 页面组件结构 登录页面主文件位于 `src/modules/base/views/login/index.vue`,采用组件化设计,将登录功能拆分为多个独立的子组件,提高代码可维护性和复用性。 **源代码位置**: * **GitHub 地址**:[mineadmin/web/src/modules/base/views/login/index.vue](https://github.com/mineadmin/mineadmin/blob/master/web/src/modules/base/views/login/index.vue) * **本地路径**:`src/modules/base/views/login/index.vue` ```plantuml @startuml !define COMPONENT class COMPONENT "登录主页面" as LoginIndex { index.vue -- 布局容器 组件整合 } COMPONENT "登录表单" as LoginForm { login-form.vue -- 用户输入处理 表单验证 登录请求 } COMPONENT "品牌标识" as Logo { Logo.vue -- 系统Logo显示 品牌信息展示 } COMPONENT "版权信息" as CopyRight { copyright.vue -- 版权声明 公司信息 } COMPONENT "背景效果" as Background { dashed.vue light.vue slogan.vue one-word.vue -- 视觉效果组件 } LoginIndex --> LoginForm LoginIndex --> Logo LoginIndex --> CopyRight LoginIndex --> Background @enduml ``` ### 响应式布局设计 登录页面采用响应式设计,适配桌面端和移动端: ```vue ``` ### 组件库说明 ::: warning 组件库注意事项 登录页面的表单组件并非使用 `Element Plus` 组件库,而是基于 MineAdmin 自身的基础组件库构建。这些组件专为系统设计,具有以下特点: * **轻量化设计**:只包含必要的登录功能,减少依赖 * **统一样式风格**:与整个系统的设计语言保持一致 * **定制化程度高**:可根据业务需求灵活调整 **自定义建议**: * 不建议直接修改源码,以免影响后续版本升级 * 推荐通过[插件系统](/v3/front/high/plugins.md)替换登录组件 * 可通过路由配置覆盖默认的 `login` 路由组件 ::: ## 登录流程与数据处理 ### 登录流程概览 登录流程采用现代化的前后端分离架构,基于 JWT Token 进行身份认证,支持 Token 自动刷新和权限验证。 ```plantuml @startuml participant "用户" as User participant "登录页面" as LoginPage participant "UserStore" as Store participant "HTTP拦截器" as Http participant "MineAdmin后端" as Backend participant "路由守卫" as Router participant "欢迎页面" as Welcome User -> LoginPage: 输入用户名密码 LoginPage -> Store: 调用 login() 方法 Store -> Http: 发送登录请求 Http -> Backend: POST /admin/passport/login Backend --> Http: 返回 Token 数据 Http --> Store: 处理响应 Store -> Store: 保存认证信息 Store --> LoginPage: 登录成功 LoginPage -> Router: 跳转到欢迎页 Router -> Router: 路由守卫检查 Router -> Store: 调用 requestUserInfo() Store -> Http: 获取用户信息 Http -> Backend: GET /admin/user/info Backend --> Http: 返回用户数据 Http --> Store: 更新用户信息 Store -> Store: 初始化路由 initRoutes() Store --> Router: 认证完成 Router --> Welcome: 显示欢迎页面 @enduml ``` ### 核心数据流转 ::: info 开发提示 如果只需要修改登录页面 UI 而不涉及登录逻辑,可以跳过本节的详细流程说明,直接查看[欢迎页配置](#默认欢迎页配置)部分。 ::: #### 1. 用户登录认证 **文件位置**:`src/store/modules/useUserStore.ts` `login()` 方法负责处理用户认证过程: ```typescript // 登录方法核心逻辑 async login(loginParams: LoginParams) { try { // 发送登录请求 const response = await http.post('/admin/passport/login', loginParams) // 保存认证信息到本地存储 const { access_token, refresh_token, expire_at } = response.data // 存储到 Pinia Store this.token = access_token this.refreshToken = refresh_token this.expireAt = expire_at // 存储到浏览器缓存 cache.set('token', access_token) cache.set('refresh_token', refresh_token) cache.set('expire', useDayjs().unix() + expire_at, { exp: expire_at }) return Promise.resolve(response) } catch (error) { return Promise.reject(error) } } ``` #### 2. 路由守卫拦截 登录成功后页面跳转会触发路由守卫,自动执行用户信息获取: ```typescript // 路由守卫逻辑(简化版) router.beforeEach(async (to, from, next) => { const userStore = useUserStore() if (to.path !== '/login' && !userStore.isLogin) { // 未登录,跳转到登录页 next('/login') } else if (userStore.isLogin && !userStore.userInfo) { // 已登录但未获取用户信息 try { await userStore.requestUserInfo() next() } catch (error) { // 获取用户信息失败,清除登录状态 await userStore.logout() next('/login') } } else { next() } }) ``` #### 3. 用户信息获取 **文件位置**:`src/store/modules/useUserStore.ts` `requestUserInfo()` 方法获取用户基础数据和权限信息: ```typescript async requestUserInfo() { try { // 并行请求用户数据、菜单权限、角色信息 const [userInfo, menuList, roleList] = await Promise.all([ http.get('/admin/user/info'), // 用户基础信息 http.get('/admin/menu/index'), // 菜单权限数据 http.get('/admin/role/index') // 角色权限数据 ]) // 更新 Store 状态 this.userInfo = userInfo.data this.menuList = menuList.data this.roleList = roleList.data // 初始化路由系统 const routeStore = useRouteStore() await routeStore.initRoutes() return Promise.resolve(userInfo) } catch (error) { return Promise.reject(error) } } ``` #### 4. 动态路由初始化 **文件位置**:`src/store/modules/useRouteStore.ts` `initRoutes()` 方法根据用户权限动态生成路由: ```typescript async initRoutes() { const userStore = useUserStore() const { menuList } = userStore // 根据菜单数据生成路由配置 const routes = this.generateRoutes(menuList) // 动态添加路由 routes.forEach(route => { router.addRoute(route) }) // 更新路由状态 this.isRoutesInitialized = true } ``` ### Token 管理机制 系统采用双 Token 机制确保安全性和用户体验: * **Access Token**:短期有效(默认 1 小时),用于 API 请求认证 * **Refresh Token**:长期有效(默认 2 小时),用于刷新 Access Token 详细的 Token 刷新机制请参考 [请求与拦截器](/v3/front/advanced/request.md#token-刷新机制) 文档。 ## 欢迎页配置与路由管理 ### 登录后跳转逻辑 MineAdmin 支持多种登录后跳转策略,确保用户体验的连续性: ```plantuml @startuml start :用户完成登录; if (URL中有redirect参数?) then (yes) :跳转到redirect指定页面; note right: 通常来自认证过期场景 else (no) :跳转到默认欢迎页; note right: 正常登录流程 endif :显示目标页面; stop @enduml ``` #### 跳转规则说明 1. **带重定向参数的登录** ``` /#/login?redirect=/admin/user/index ``` 登录成功后会自动跳转到 `redirect` 参数指定的页面。这种情况通常发生在: * 用户访问需要权限的页面但未登录时 * Token 过期后自动跳转到登录页时 2. **默认登录跳转** ``` /#/login ``` 没有 `redirect` 参数时,登录成功后跳转到系统配置的默认欢迎页面。 ### 欢迎页配置详解 #### 默认配置结构 **配置文件位置**:`src/provider/settings/index.ts` MineAdmin 实际的默认欢迎页配置: ```typescript // MineAdmin 默认欢迎页配置 welcomePage: { name: 'welcome', // 路由名称 path: '/welcome', // 路由路径 title: '欢迎页', // 页面标题 icon: 'icon-park-outline:jewelry', // 菜单图标 }, ``` 注意:MineAdmin 中欢迎页的组件路径是通过路由系统自动解析的,位于 `src/modules/base/views/welcome/index.vue`。 #### 配置项详细说明 | 配置项 | 类型 | 必填 | 默认值 | 说明 | |--------|------|------|---------|------| | `name` | `string` | ✅ | `'welcome'` | 路由名称,必须全局唯一 | | `path` | `string` | ✅ | `'/welcome'` | 访问路径,支持动态路由 | | `title` | `string` | ✅ | `'欢迎页'` | 页面标题,显示在浏览器标签和面包屑中 | | `icon` | `string` | ❌ | `'icon-park-outline:jewelry'` | 图标标识,用于菜单显示 | | `component` | `Function` | ❌ | 动态导入组件 | 页面组件,支持异步加载 | ### 自定义欢迎页配置 ::: tip 最佳实践 为了保证系统升级时配置不被覆盖,强烈建议在 `settings.config.ts` 中进行自定义配置,而不是直接修改 `index.ts` 文件。 ::: #### 配置方式 **步骤 1**:编辑 `src/provider/settings/settings.config.ts` 注意:该文件已存在于 MineAdmin 项目中,无需创建。 ```typescript import type { SystemSettings } from '#/global' const globalConfigSettings: SystemSettings.all = { // 自定义欢迎页配置 welcomePage: { name: 'dashboard', // 修改为仪表板 path: '/dashboard', // 路径改为仪表板路径 title: '数据概览', // 自定义标题 icon: 'mdi:view-dashboard-outline', // 使用仪表板图标 }, // 其他系统配置... app: { // 应用相关配置 } } export default globalConfigSettings ``` **步骤 2**:系统自动合并配置 系统启动时会自动将 `settings.config.ts` 中的配置与默认配置进行深度合并: ```typescript // MineAdmin 实际的配置合并逻辑 import { defaultsDeep } from 'lodash-es' import globalConfigSettings from '@/provider/settings/settings.config.ts' // 默认配置与用户配置合并 const systemSetting = defaultsDeep(globalConfigSettings, defaultGlobalConfigSettings) ``` ### 高级配置示例 #### 1. 条件化欢迎页 根据用户角色或权限设置不同的欢迎页: ```typescript const globalConfigSettings: SystemSettings.all = { welcomePage: { name: 'adaptive-welcome', path: '/adaptive-welcome', title: '个性化欢迎页', icon: 'mdi:account-star', // 使用自定义组件处理条件逻辑 component: () => import('@/views/custom/AdaptiveWelcome.vue') } } ``` #### 2. 多语言支持 结合国际化配置设置多语言欢迎页: ```typescript const globalConfigSettings: SystemSettings.all = { welcomePage: { name: 'welcome', path: '/welcome', // 使用国际化键值 title: 'menu.welcome', icon: 'icon-park-outline:jewelry', } } ``` #### 3. 外部链接跳转 配置登录后跳转到外部系统: ```typescript const globalConfigSettings: SystemSettings.all = { welcomePage: { name: 'external-system', path: 'https://external-dashboard.com', // 外部链接 title: '外部系统', icon: 'mdi:open-in-new', // 设置为外部链接类型 meta: { isExternal: true, target: '_blank' } } } ``` ### 欢迎页组件开发 #### 基础组件结构 ```vue ``` ## 安全考虑与最佳实践 ### 认证安全 1. **Token 安全存储** * Access Token 存储在内存中,避免 XSS 攻击 * Refresh Token 使用 HttpOnly Cookie 存储 * 敏感信息不存储在 localStorage 中 2. **路由权限验证** ```typescript // 路由守卫中的权限检查 router.beforeEach(async (to, from, next) => { const userStore = useUserStore() // 检查路由是否需要认证 if (to.meta.requiresAuth && !userStore.isLogin) { next(`/login?redirect=${to.fullPath}`) return } // 检查用户权限 if (to.meta.permissions && !userStore.hasPermissions(to.meta.permissions)) { next('/403') // 权限不足页面 return } next() }) ``` ### 性能优化 1. **组件懒加载** MineAdmin 使用模块化路由加载,组件会自动懒加载: ```typescript // MineAdmin 中的动态组件加载 const moduleViews = import.meta.glob('../../modules/**/views/**/**.{vue,jsx,tsx}') const pluginViews = import.meta.glob('../../plugins/*/**/views/**/**.{vue,jsx,tsx}') // 自动解析组件路径 if (moduleViews[`../../modules/${item.component}${suffix}`]) { component = moduleViews[`../../modules/${item.component}${suffix}`] } ``` 2. **数据预加载** MineAdmin 在路由守卫中处理用户信息加载: ```typescript // MineAdmin 的数据预加载机制 router.beforeEach(async (to, from, next) => { if (userStore.isLogin) { if (userStore.getUserInfo() === null) { // 预加载用户信息、菜单和权限数据 await userStore.requestUserInfo() next({ path: to.fullPath, query: to.query }) } else { next() } } }) ``` ## 常见问题与解决方案 ### Q: 登录成功后页面没有跳转? **MineAdmin 中可能的原因和解决方案**: 1. **路由配置问题** ```typescript // 检查欢迎页路由是否正确注册 const routes = [ { name: 'welcome', path: '/welcome', component: () => import('@/views/Welcome.vue'), meta: { requiresAuth: true } } ] ``` 2. **权限验证失败** ```typescript // 确保用户有访问欢迎页的权限 if (!userStore.hasPermission('welcome:access')) { // 处理权限不足情况 } ``` ### Q: 自定义欢迎页配置不生效? **解决方案**: 1. **确认配置文件路径** ```bash src/provider/settings/settings.config.ts # 正确路径 ``` 2. **检查配置语法** ```typescript // ❌ 错误:配置对象结构不正确 const config = { welcomePage: '/dashboard' } // ✅ 正确:完整的配置对象 const config = { welcomePage: { name: 'dashboard', path: '/dashboard', title: '仪表板' } } ``` 3. **重启开发服务器** ```bash pnpm run dev ``` ### Q: 如何实现登录后的个性化跳转? **解决方案**: ```typescript // 在 UserStore 中实现个性化跳转逻辑 async login(params: LoginParams) { const response = await http.post('/admin/passport/login', params) // 根据用户角色确定跳转页面 const userRole = response.data.user.role const redirectMap = { 'admin': '/dashboard', 'user': '/profile', 'guest': '/welcome' } const targetPath = redirectMap[userRole] || '/welcome' // 执行跳转 await router.push(targetPath) } ``` ## 相关文档链接 * [系统配置详解](/v3/front/advanced/system-config.md) - 系统全局配置说明 * [请求与拦截器](/v3/front/advanced/request.md) - HTTP 请求和 Token 管理 * [路由与菜单](/v3/front/base/route-menu.md) - 路由系统配置 * [插件系统](/v3/front/high/plugins.md) - 插件开发与配置 * [后端认证机制](/v3/backend/security/passport.md) - 后端 JWT 认证实现 --- --- url: /v3/backend/base/structure.md --- # 目录结构 本文已迁移到 [Hyperf 目录结构](/backend/frameworks/hyperf/3.2/base/structure)。 新的后端文档按 [公共契约](/v3/backend/contracts/) 和 [框架实现](/backend/frameworks/hyperf/) 组织。旧地址保留用于兼容历史链接。 --- --- url: /v3/front/advanced/system-config.md --- # 系统参数配置 MineAdmin 提供了强大而灵活的系统配置机制,支持多层级配置合并、环境变量集成和运行时动态修改。本文档将详细介绍如何配置和管理系统参数。 ## 配置系统概览 ::: tip 配置文件 * **默认配置文件**:`src/provider/settings/index.ts` - 系统默认配置 * **自定义配置文件**:`src/provider/settings/settings.config.ts` - 用户自定义配置 * **环境变量文件**:`.env.development` / `.env.production` - 环境相关配置 修改配置时,请将需要自定义的配置项拷贝到 `settings.config.ts` 文件中进行修改,系统会自动合并配置。 ::: ## 配置系统架构 ```plantuml @startuml !define RECTANGLE class RECTANGLE "环境变量" as env { VITE_APP_TITLE VITE_APP_PORT VITE_APP_API_BASEURL ... } RECTANGLE "默认配置" as default { index.ts defaultGlobalConfigSettings } RECTANGLE "自定义配置" as custom { settings.config.ts globalConfigSettings } RECTANGLE "Vue Provider" as provider { 合并配置 依赖注入 } RECTANGLE "应用组件" as components { 获取配置 使用配置 } env --> default : 注入 default --> provider : 合并 custom --> provider : 覆盖合并 provider --> components : 提供配置 @enduml ``` ## 配置加载流程 ```plantuml @startuml start :读取环境变量; :加载默认配置 defaultGlobalConfigSettings; :加载自定义配置 globalConfigSettings; :使用 lodash.defaultsDeep 深度合并配置; :通过 Vue Provider 注入到应用; :组件通过 inject 获取配置; stop @enduml ``` ## 核心配置详解 ### 应用基础配置 (app) | 配置项 | 类型 | 默认值 | 描述 | |--------|------|--------|------| | `colorMode` | `'lightMode' \| 'darkMode' \| 'autoMode'` | `'autoMode'` | 颜色模式,支持明亮、暗黑和自动模式 | | `useLocale` | `string` | `'zh_CN'` | 系统语言,支持国际化 | | `whiteRoute` | `string[]` | `['login']` | 白名单路由,无需认证即可访问 | | `layout` | `'classic' \| 'modern' \| 'minimal'` | `'classic'` | 布局模式 | | `pageAnimate` | `string` | `'ma-slide-down'` | 页面切换动画效果 | | `enableWatermark` | `boolean` | `false` | 是否启用水印功能 | | `primaryColor` | `string` | `'#2563EB'` | 主题色 | | `asideDark` | `boolean` | `false` | 侧边栏是否使用暗色主题 | | `showBreadcrumb` | `boolean` | `true` | 是否显示面包屑导航 | | `loadUserSetting` | `boolean` | `true` | 是否加载用户个人设置 | | `watermarkText` | `string` | `import.meta.env.VITE_APP_TITLE` | 水印文字内容 | **配置示例:** ```typescript app: { colorMode: 'autoMode', // 自动切换主题 useLocale: 'zh_CN', // 使用简体中文 whiteRoute: ['login', 'register'], // 登录和注册页面免认证 layout: 'classic', // 经典布局 pageAnimate: 'ma-fade-in', // 淡入动画效果 enableWatermark: true, // 开启水印 primaryColor: '#1890ff', // 自定义主题色 asideDark: true, // 侧边栏暗色主题 showBreadcrumb: true, // 显示面包屑 loadUserSetting: true, // 加载用户设置 watermarkText: '我的系统', // 自定义水印文字 } ``` ### 欢迎页配置 (welcomePage) | 配置项 | 类型 | 默认值 | 描述 | |--------|------|--------|------| | `name` | `string` | `'welcome'` | 路由名称 | | `path` | `string` | `'/welcome'` | 路由路径 | | `title` | `string` | `'欢迎页'` | 页面标题 | | `icon` | `string` | `'icon-park-outline:jewelry'` | 图标 | ### 主侧边栏配置 (mainAside) | 配置项 | 类型 | 默认值 | 描述 | |--------|------|--------|------| | `showIcon` | `boolean` | `true` | 是否显示图标 | | `showTitle` | `boolean` | `true` | 是否显示标题 | | `enableOpenFirstRoute` | `boolean` | `false` | 是否自动打开第一个路由 | ### 子侧边栏配置 (subAside) | 配置项 | 类型 | 默认值 | 描述 | |--------|------|--------|------| | `showIcon` | `boolean` | `true` | 是否显示图标 | | `showTitle` | `boolean` | `true` | 是否显示标题 | | `fixedAsideState` | `boolean` | `false` | 是否固定侧边栏状态 | | `showCollapseButton` | `boolean` | `true` | 是否显示折叠按钮 | ### 标签栏配置 (tabbar) | 配置项 | 类型 | 默认值 | 描述 | |--------|------|--------|------| | `enable` | `boolean` | `true` | 是否启用标签栏 | | `mode` | `'rectangle' \| 'round' \| 'card'` | `'rectangle'` | 标签栏样式 | ### 版权信息配置 (copyright) | 配置项 | 类型 | 默认值 | 描述 | |--------|------|--------|------| | `enable` | `boolean` | `true` | 是否显示版权信息 | | `dates` | `string` | `useDayjs().format('YYYY')` | 版权年份 | | `company` | `string` | `'MineAdmin Team'` | 公司名称 | | `website` | `string` | `'https://www.mineadmin.com'` | 官网地址 | | `putOnRecord` | `string` | `'豫ICP备00000000号-1'` | 备案号 | ## 环境变量配置 ### 开发环境配置 (.env.development) ```bash # 页面标题 VITE_APP_TITLE = MineAdmin开发环境 # 开发服务器端口 VITE_APP_PORT = 2888 # 应用根路径 VITE_APP_ROOT_BASE = / # API 接口地址 VITE_APP_API_BASEURL = http://127.0.0.1:9501 # 路由模式:hash 或 history VITE_APP_ROUTE_MODE = hash # 本地存储前缀 VITE_APP_STORAGE_PREFIX = mine_ # 是否开启代理 VITE_OPEN_PROXY = true # 代理前缀 VITE_PROXY_PREFIX = /dev # 是否开启 vConsole(移动端调试) VITE_OPEN_vCONSOLE = false # 是否开启开发者工具 VITE_OPEN_DEVTOOLS = true ``` ### 生产环境配置 (.env.production) ```bash # 页面标题 VITE_APP_TITLE = MineAdmin # 生产服务器端口 VITE_APP_PORT = 80 # 应用根路径 VITE_APP_ROOT_BASE = /admin/ # API 接口地址 VITE_APP_API_BASEURL = https://api.yourdomain.com # 路由模式 VITE_APP_ROUTE_MODE = history # 本地存储前缀 VITE_APP_STORAGE_PREFIX = mine_prod_ # 关闭代理 VITE_OPEN_PROXY = false # 是否生成 sourcemap VITE_BUILD_SOURCEMAP = false # 打包压缩方式 VITE_BUILD_COMPRESS = gzip,brotli # 打包后生成存档 VITE_BUILD_ARCHIVE = ``` ## 自定义配置示例 在 `src/provider/settings/settings.config.ts` 文件中添加自定义配置: ```typescript import type { SystemSettings } from '#/global' const globalConfigSettings: SystemSettings.all = { // 应用配置 app: { colorMode: 'lightMode', // 强制明亮模式 useLocale: 'en_US', // 使用英语 primaryColor: '#ff4757', // 自定义红色主题 enableWatermark: true, // 启用水印 watermarkText: '内部系统', // 自定义水印文字 pageAnimate: 'ma-fade-in', // 淡入动画 }, // 欢迎页配置 welcomePage: { name: 'dashboard', path: '/dashboard', title: '控制台', icon: 'mdi:view-dashboard', }, // 侧边栏配置 mainAside: { showIcon: true, showTitle: false, // 隐藏主菜单标题 enableOpenFirstRoute: true, // 自动打开第一个路由 }, // 标签栏配置 tabbar: { enable: true, mode: 'card', // 卡片模式 }, // 版权信息 copyright: { enable: true, company: '我的公司', website: 'https://mycompany.com', putOnRecord: '京ICP备12345678号', }, } export default globalConfigSettings ``` ## 高级配置技巧 ### 条件配置 根据不同环境或设备类型设置不同配置: ```typescript const globalConfigSettings: SystemSettings.all = { app: { // 根据环境变量决定主题 colorMode: import.meta.env.MODE === 'development' ? 'autoMode' : 'lightMode', // 移动端隐藏面包屑 showBreadcrumb: !/Mobile|Android|iPhone/i.test(navigator.userAgent), // 生产环境关闭水印 enableWatermark: import.meta.env.MODE === 'development', // 动态设置 API 地址 watermarkText: import.meta.env.VITE_APP_TITLE || '系统', }, } ``` ### 模块化配置 将大型配置拆分为多个模块: ```typescript // config/app.config.ts export const appConfig = { colorMode: 'autoMode', useLocale: 'zh_CN', primaryColor: '#2563EB', } // config/layout.config.ts export const layoutConfig = { mainAside: { showIcon: true, showTitle: true, }, subAside: { fixedAsideState: false, showCollapseButton: true, }, } // settings.config.ts import { appConfig } from './config/app.config' import { layoutConfig } from './config/layout.config' const globalConfigSettings: SystemSettings.all = { app: appConfig, ...layoutConfig, } ``` ### 运行时配置修改 在应用运行时动态修改配置: ```typescript // 在组件中使用 import { inject, reactive } from 'vue' import type { SystemSettings } from '#/global' export default defineComponent({ setup() { const settings = inject('defaultSetting') as SystemSettings.all // 动态切换主题 const switchTheme = (mode: 'lightMode' | 'darkMode') => { settings.app.colorMode = mode } // 动态修改主题色 const changePrimaryColor = (color: string) => { settings.app.primaryColor = color } return { settings, switchTheme, changePrimaryColor, } }, }) ``` ## 配置最佳实践 ### 1. 版本控制管理 ```bash # .gitignore 文件 .env.local .env.*.local src/provider/settings/settings.config.local.ts ``` ### 2. 类型安全 利用 TypeScript 确保配置类型安全: ```typescript import type { SystemSettings } from '#/global' // 使用类型断言确保配置正确 const globalConfigSettings: SystemSettings.all = { app: { // TypeScript 会提供类型检查和自动补全 colorMode: 'lightMode', // 只能是预定义的值 primaryColor: '#ffffff', // 必须是字符串 }, } satisfies SystemSettings.all ``` ### 3. 配置验证 在配置加载时添加验证: ```typescript import { z } from 'zod' const configSchema = z.object({ app: z.object({ colorMode: z.enum(['lightMode', 'darkMode', 'autoMode']), primaryColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/), }), }) // 验证配置 const validateConfig = (config: unknown) => { try { return configSchema.parse(config) } catch (error) { console.error('配置验证失败:', error) throw new Error('配置格式不正确') } } ``` ## 常见问题与排错 ### Q: 配置修改后不生效? **A:** 检查以下几点: 1. **配置文件路径是否正确** ```bash # 正确的配置文件路径 src/provider/settings/settings.config.ts ``` 2. **配置语法是否正确** ```typescript // ❌ 错误:语法错误 const config = { app: { colorMode: lightMode, // 缺少引号 } } // ✅ 正确:正确语法 const config = { app: { colorMode: 'lightMode', } } ``` 3. **是否重新启动了开发服务器** ```bash pnpm run dev ``` ### Q: 环境变量无法读取? **A:** 确保环境变量以 `VITE_` 开头: ```bash # ❌ 错误:不以 VITE_ 开头 APP_TITLE = MineAdmin # ✅ 正确:以 VITE_ 开头 VITE_APP_TITLE = MineAdmin ``` ### Q: 如何调试配置问题? **A:** 使用以下方法调试: ```typescript // 在组件中打印当前配置 const settings = inject('defaultSetting') console.log('当前配置:', settings) // 检查环境变量 console.log('环境变量:', import.meta.env) // 检查配置合并结果 import { defaultsDeep } from 'lodash-es' console.log('合并后配置:', defaultsDeep(customConfig, defaultConfig)) ``` ### Q: 配置在生产环境中不生效? **A:** 检查构建配置: 1. **确认环境变量文件** ```bash # 生产环境应该有对应的环境变量文件 .env.production ``` 2. **检查构建命令** ```bash # 确保使用正确的构建命令 pnpm run build ``` 3. **验证构建产物** ```bash # 预览构建结果 pnpm run preview ``` ## 相关参考 * [布局配置](./layout.md) - 布局系统配置详解 ::: warning 注意事项 * 修改配置后需要重启开发服务器才能生效 * 生产环境的配置修改需要重新构建和部署 * 敏感信息不要直接写在配置文件中,建议使用环境变量 ::: --- --- url: /backend/frameworks/hyperf/3.1/data-permission/architecture.md --- # 系统架构 ## 数据权限系统架构设计 MineAdmin 数据权限系统基于 AOP(面向切面编程)设计,通过注解和切面拦截的方式自动为数据查询注入权限过滤条件。 ## 核心组件架构图 ```plantuml @startuml !theme plain package "数据模型层" { class User { +id: int +isSuperAdmin(): bool +getPolicy(): ?Policy +department(): BelongsToMany +position(): BelongsToMany } class Policy { +user_id: int +position_id: int +policy_type: PolicyType +value: array +is_default: bool } class Department { +id: int +parent_id: int +getFlatChildren(): Collection } class Position { +id: int +dept_id: int +policy(): HasOne } } package "权限引擎" { class Factory { +make(): self +build(Builder, User): void } class Context { +setDeptColumn(string): void +setCreatedByColumn(string): void +setScopeType(ScopeType): void +setOnlyTables(array): void } class Rule { +getDeptIds(User, Policy): array +getCreatedByList(User, Policy): array } } package "AOP 层" { annotation DataScope { +deptColumn: string +createdByColumn: string +scopeType: ScopeType +onlyTables: array } class DataScopeAspect { +process(): void } } package "枚举定义" { enum PolicyType { DEPT_SELF DEPT_TREE ALL SELF CUSTOM_DEPT CUSTOM_FUNC } enum ScopeType { DEPT = 1 CREATED_BY = 2 DEPT_CREATED_BY = 3 DEPT_OR_CREATED_BY = 4 } } User --> Policy : 关联策略 Position --> Policy : 关联策略 User --> Department : 所属部门 Position --> Department : 归属部门 Factory --> Rule : 使用规则 Factory --> Context : 读取上下文 DataScope --> DataScopeAspect : 触发切面 DataScopeAspect --> Factory : 调用工厂 @enduml ``` ## 权限解析流程 ```plantuml @startuml !theme plain start :接收查询请求; :DataScope 注解拦截; if (用户是超级管理员?) then (是) :跳过权限检查; stop endif :获取当前用户; note right 来源:User.php:160-179 getPolicy() 方法 end note if (用户有直接策略?) then (有) :使用用户策略; else (无) :遍历用户岗位; if (岗位有策略?) then (有) :使用第一个岗位策略; else (无) :返回空结果; stop endif endif :根据 ScopeType 处理; note right 来源:Factory.php:51-65 处理不同的 ScopeType end note switch (ScopeType) case (CREATED_BY) :过滤创建人数据; case (DEPT) :过滤部门数据; case (DEPT_CREATED_BY) :AND 条件过滤; case (DEPT_OR_CREATED_BY) :OR 条件过滤; endswitch :构建 WHERE 条件; :注入到 QueryBuilder; :执行查询; stop @enduml ``` ## 数据权限执行机制 ### Factory 工厂类 权限过滤的核心实现: ```php // /mineadmin/app/Library/DataPermission/Factory.php class Factory { public function build(Builder $builder, User $user): void { // 1. 超级管理员绕过检查 if ($user->isSuperAdmin()) { return; } // 2. 获取用户策略 if (($policy = $user->getPolicy()) === null) { return; } // 3. 获取当前 ScopeType $scopeType = Context::getScopeType(); // 4. 处理自定义函数 if ($policy->policy_type === PolicyType::CustomFunc) { $customFunc = $policy->value[0] ?? null; $this->rule->loadCustomFunc($customFunc, $builder, $user, $policy, $scopeType); } // 5. 根据 ScopeType 处理不同的过滤逻辑 switch ($scopeType) { case ScopeType::CREATED_BY: $this->handleCreatedBy($user, $policy, $builder); break; case ScopeType::DEPT: $this->handleDept($user, $policy, $builder); break; case ScopeType::DEPT_CREATED_BY: $this->handleDeptCreatedBy($user, $policy, $builder); break; case ScopeType::DEPT_OR_CREATED_BY: $this->handleDeptOrCreatedBy($user, $policy, $builder); break; } } } ``` ### 具体过滤实现 各种过滤条件的具体实现: ```php // /mineadmin/app/Library/DataPermission/Factory.php:67-102 // 创建人过滤 private function handleCreatedBy(User $user, Policy $policy, Builder $builder): void { $builder->when($this->rule->getCreatedByList($user, $policy), static function (Builder $query, array $createdByList) { $query->whereIn(Context::getCreatedByColumn(), $createdByList); }); } // 部门过滤 private function handleDept(User $user, Policy $policy, Builder $builder): void { $builder->when($this->rule->getDeptIds($user, $policy), static function (Builder $query, array $deptList) { $query->whereIn(Context::getDeptColumn(), $deptList); }); } // 部门 AND 创建人过滤 private function handleDeptCreatedBy(User $user, Policy $policy, Builder $builder): void { $builder->when($this->rule->getDeptIds($user, $policy), static function (Builder $query, array $deptList) { $query->whereIn(Context::getDeptColumn(), $deptList); })->when($this->rule->getCreatedByList($user, $policy), static function (Builder $query, array $createdByList) { $query->whereIn(Context::getCreatedByColumn(), $createdByList); }); } // 部门 OR 创建人过滤 private function handleDeptOrCreatedBy(User $user, Policy $policy, Builder $builder): void { $createdByList = $this->rule->getCreatedByList($user, $policy); $deptList = $this->rule->getDeptIds($user, $policy); $builder->where(static function (Builder $query) use ($createdByList, $deptList) { if ($createdByList) { $query->whereIn(Context::getCreatedByColumn(), $createdByList); } if ($deptList) { $query->orWhereIn(Context::getDeptColumn(), $deptList); } }); } ``` ## Context 上下文管理 ### 上下文存储机制 ```php // /mineadmin/app/Library/DataPermission/Context.php final class Context { private const DEPT_COLUMN_KEY = 'data_permission_dept_column'; private const CREATED_BY_COLUMN_KEY = 'data_permission_created_by_column'; private const SCOPE_TYPE_KEY = 'data_permission_scope_type'; private const ONLY_TABLES_KEY = 'data_permission_only_tables'; public static function setDeptColumn(string $column = 'dept_id'): void { Ctx::set(self::DEPT_COLUMN_KEY, $column); } public static function getDeptColumn(): string { return Ctx::get(self::DEPT_COLUMN_KEY, 'dept_id'); } // ... 其他方法类似 } ``` ## 部门层级处理 ### 递归部门树算法 ```php // /mineadmin/app/Model/Permission/Department.php public function getFlatChildren(): Collection { $flat = collect(); $this->load('children'); // 预加载子部门 $traverse = static function ($departments) use (&$traverse, $flat) { foreach ($departments as $department) { $flat->push($department); if ($department->children->isNotEmpty()) { $traverse($department->children); // 递归处理 } } }; $traverse($this->children); return $flat->prepend($this); // 包含自身 } ``` ### 部门权限处理流程 ```plantuml @startuml !theme plain start :获取用户部门; if (策略类型 == DEPT_SELF?) then (是) :只获取当前部门ID; else (否) if (策略类型 == DEPT_TREE?) then (是) :获取当前部门; :调用 getFlatChildren(); :递归获取所有子部门; :返回部门ID数组; else (否) if (策略类型 == CUSTOM_DEPT?) then (是) :从策略配置读取部门ID; endif endif endif :构建 WHERE IN 条件; :dept_id IN (1,2,3...); stop @enduml ``` ## AOP 切面机制 ### DataScope 注解定义 ```php // /mineadmin/app/Library/DataPermission/Attribute/DataScope.php #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class DataScope extends AbstractAnnotation { public function __construct( private readonly string $deptColumn = 'dept_id', private readonly string $createdByColumn = 'created_by', private readonly ScopeType $scopeType = ScopeType::DEPT_CREATED_BY, private readonly ?array $onlyTables = null ) {} } ``` ### 切面拦截处理 ```plantuml @startuml !theme plain participant "业务方法" as method participant "DataScopeAspect" as aspect participant "Factory" as factory participant "QueryBuilder" as builder method -> aspect: 调用带@DataScope注解的方法 aspect -> aspect: 解析DataScope注解参数 aspect -> factory: 获取Factory实例 aspect -> builder: 拦截QueryBuilder factory -> builder: 注入权限过滤条件 builder -> method: 返回过滤后的查询 method -> method: 执行业务逻辑 @enduml ``` ## 自定义函数扩展 ### 自定义函数配置 ```php // /mineadmin/config/autoload/department/custom.php return [ 'testction' => function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) { // 只针对特定用户生效 if ($user->id !== 2) { return; } $createdByColumn = Context::getCreatedByColumn(); $deptColumn = Context::getDeptColumn(); switch ($scopeType) { case ScopeType::CREATED_BY: $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); break; case ScopeType::DEPT_CREATED_BY: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT_OR_CREATED_BY: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); $builder->orWhere($createdByColumn, $user->id); break; } } ]; ``` ## 实际使用示例 ### 在 Service 中的使用 ```php // /mineadmin/app/Service/Permission/UserService.php:94-98 class UserService { #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['user'], createdByColumn: 'id' )] public function page(array $params, int $page = 1, int $pageSize = 10): array { return parent::page($params, $page, $pageSize); } } ``` 这个真实的使用示例展示了如何在 MineAdmin 中应用数据权限注解来控制用户列表的访问权限。 通过这个架构设计,MineAdmin 实现了声明式的数据权限控制,开发者只需要在方法上添加 `@DataScope` 注解,系统就会自动根据当前用户的权限策略过滤数据。 --- --- url: /backend/frameworks/hyperf/3.2/data-permission/architecture.md --- # 系统架构 ## 数据权限系统架构设计 MineAdmin 数据权限系统基于 AOP(面向切面编程)设计,通过注解和切面拦截的方式自动为数据查询注入权限过滤条件。 ## 核心组件架构图 ```plantuml @startuml !theme plain package "数据模型层" { class User { +id: int +isSuperAdmin(): bool +getPolicy(): ?Policy +department(): BelongsToMany +position(): BelongsToMany } class Policy { +user_id: int +position_id: int +policy_type: PolicyType +value: array +is_default: bool } class Department { +id: int +parent_id: int +getFlatChildren(): Collection } class Position { +id: int +dept_id: int +policy(): HasOne } } package "权限引擎" { class Factory { +make(): self +build(Builder, User): void } class Context { +setDeptColumn(string): void +setCreatedByColumn(string): void +setScopeType(ScopeType): void +setOnlyTables(array): void } class Rule { +getDeptIds(User, Policy): array +getCreatedByList(User, Policy): array } } package "AOP 层" { annotation DataScope { +deptColumn: string +createdByColumn: string +scopeType: ScopeType +onlyTables: array } class DataScopeAspect { +process(): void } } package "枚举定义" { enum PolicyType { DEPT_SELF DEPT_TREE ALL SELF CUSTOM_DEPT CUSTOM_FUNC } enum ScopeType { DEPT = 1 CREATED_BY = 2 DEPT_CREATED_BY = 3 DEPT_OR_CREATED_BY = 4 } } User --> Policy : 关联策略 Position --> Policy : 关联策略 User --> Department : 所属部门 Position --> Department : 归属部门 Factory --> Rule : 使用规则 Factory --> Context : 读取上下文 DataScope --> DataScopeAspect : 触发切面 DataScopeAspect --> Factory : 调用工厂 @enduml ``` ## 权限解析流程 ```plantuml @startuml !theme plain start :接收查询请求; :DataScope 注解拦截; if (用户是超级管理员?) then (是) :跳过权限检查; stop endif :获取当前用户; note right 来源:User.php:160-179 getPolicy() 方法 end note if (用户有直接策略?) then (有) :使用用户策略; else (无) :遍历用户岗位; if (岗位有策略?) then (有) :使用第一个岗位策略; else (无) :返回空结果; stop endif endif :根据 ScopeType 处理; note right 来源:Factory.php:51-65 处理不同的 ScopeType end note switch (ScopeType) case (CREATED_BY) :过滤创建人数据; case (DEPT) :过滤部门数据; case (DEPT_CREATED_BY) :AND 条件过滤; case (DEPT_OR_CREATED_BY) :OR 条件过滤; endswitch :构建 WHERE 条件; :注入到 QueryBuilder; :执行查询; stop @enduml ``` ## 数据权限执行机制 ### Factory 工厂类 权限过滤的核心实现: ```php // /mineadmin/app/Library/DataPermission/Factory.php class Factory { public function build(Builder $builder, User $user): void { // 1. 超级管理员绕过检查 if ($user->isSuperAdmin()) { return; } // 2. 获取用户策略 if (($policy = $user->getPolicy()) === null) { return; } // 3. 获取当前 ScopeType $scopeType = Context::getScopeType(); // 4. 处理自定义函数 if ($policy->policy_type === PolicyType::CustomFunc) { $customFunc = $policy->value[0] ?? null; $this->rule->loadCustomFunc($customFunc, $builder, $user, $policy, $scopeType); } // 5. 根据 ScopeType 处理不同的过滤逻辑 switch ($scopeType) { case ScopeType::CREATED_BY: $this->handleCreatedBy($user, $policy, $builder); break; case ScopeType::DEPT: $this->handleDept($user, $policy, $builder); break; case ScopeType::DEPT_CREATED_BY: $this->handleDeptCreatedBy($user, $policy, $builder); break; case ScopeType::DEPT_OR_CREATED_BY: $this->handleDeptOrCreatedBy($user, $policy, $builder); break; } } } ``` ### 具体过滤实现 各种过滤条件的具体实现: ```php // /mineadmin/app/Library/DataPermission/Factory.php:67-102 // 创建人过滤 private function handleCreatedBy(User $user, Policy $policy, Builder $builder): void { $builder->when($this->rule->getCreatedByList($user, $policy), static function (Builder $query, array $createdByList) { $query->whereIn(Context::getCreatedByColumn(), $createdByList); }); } // 部门过滤 private function handleDept(User $user, Policy $policy, Builder $builder): void { $builder->when($this->rule->getDeptIds($user, $policy), static function (Builder $query, array $deptList) { $query->whereIn(Context::getDeptColumn(), $deptList); }); } // 部门 AND 创建人过滤 private function handleDeptCreatedBy(User $user, Policy $policy, Builder $builder): void { $builder->when($this->rule->getDeptIds($user, $policy), static function (Builder $query, array $deptList) { $query->whereIn(Context::getDeptColumn(), $deptList); })->when($this->rule->getCreatedByList($user, $policy), static function (Builder $query, array $createdByList) { $query->whereIn(Context::getCreatedByColumn(), $createdByList); }); } // 部门 OR 创建人过滤 private function handleDeptOrCreatedBy(User $user, Policy $policy, Builder $builder): void { $createdByList = $this->rule->getCreatedByList($user, $policy); $deptList = $this->rule->getDeptIds($user, $policy); $builder->where(static function (Builder $query) use ($createdByList, $deptList) { if ($createdByList) { $query->whereIn(Context::getCreatedByColumn(), $createdByList); } if ($deptList) { $query->orWhereIn(Context::getDeptColumn(), $deptList); } }); } ``` ## Context 上下文管理 ### 上下文存储机制 ```php // /mineadmin/app/Library/DataPermission/Context.php final class Context { private const DEPT_COLUMN_KEY = 'data_permission_dept_column'; private const CREATED_BY_COLUMN_KEY = 'data_permission_created_by_column'; private const SCOPE_TYPE_KEY = 'data_permission_scope_type'; private const ONLY_TABLES_KEY = 'data_permission_only_tables'; public static function setDeptColumn(string $column = 'dept_id'): void { Ctx::set(self::DEPT_COLUMN_KEY, $column); } public static function getDeptColumn(): string { return Ctx::get(self::DEPT_COLUMN_KEY, 'dept_id'); } // ... 其他方法类似 } ``` ## 部门层级处理 ### 递归部门树算法 ```php // /mineadmin/app/Model/Permission/Department.php public function getFlatChildren(): Collection { $flat = collect(); $this->load('children'); // 预加载子部门 $traverse = static function ($departments) use (&$traverse, $flat) { foreach ($departments as $department) { $flat->push($department); if ($department->children->isNotEmpty()) { $traverse($department->children); // 递归处理 } } }; $traverse($this->children); return $flat->prepend($this); // 包含自身 } ``` ### 部门权限处理流程 ```plantuml @startuml !theme plain start :获取用户部门; if (策略类型 == DEPT_SELF?) then (是) :只获取当前部门ID; else (否) if (策略类型 == DEPT_TREE?) then (是) :获取当前部门; :调用 getFlatChildren(); :递归获取所有子部门; :返回部门ID数组; else (否) if (策略类型 == CUSTOM_DEPT?) then (是) :从策略配置读取部门ID; endif endif endif :构建 WHERE IN 条件; :dept_id IN (1,2,3...); stop @enduml ``` ## AOP 切面机制 ### DataScope 注解定义 ```php // /mineadmin/app/Library/DataPermission/Attribute/DataScope.php #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class DataScope extends AbstractAnnotation { public function __construct( private readonly string $deptColumn = 'dept_id', private readonly string $createdByColumn = 'created_by', private readonly ScopeType $scopeType = ScopeType::DEPT_CREATED_BY, private readonly ?array $onlyTables = null ) {} } ``` ### 切面拦截处理 ```plantuml @startuml !theme plain participant "业务方法" as method participant "DataScopeAspect" as aspect participant "Factory" as factory participant "QueryBuilder" as builder method -> aspect: 调用带@DataScope注解的方法 aspect -> aspect: 解析DataScope注解参数 aspect -> factory: 获取Factory实例 aspect -> builder: 拦截QueryBuilder factory -> builder: 注入权限过滤条件 builder -> method: 返回过滤后的查询 method -> method: 执行业务逻辑 @enduml ``` ## 自定义函数扩展 ### 自定义函数配置 ```php // /mineadmin/config/autoload/department/custom.php return [ 'testction' => function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) { // 只针对特定用户生效 if ($user->id !== 2) { return; } $createdByColumn = Context::getCreatedByColumn(); $deptColumn = Context::getDeptColumn(); switch ($scopeType) { case ScopeType::CREATED_BY: $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); break; case ScopeType::DEPT_CREATED_BY: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); $builder->where($createdByColumn, $user->id); break; case ScopeType::DEPT_OR_CREATED_BY: $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); $builder->orWhere($createdByColumn, $user->id); break; } } ]; ``` ## 实际使用示例 ### 在 Service 中的使用 ```php // /mineadmin/app/Service/Permission/UserService.php:94-98 class UserService { #[DataScope( scopeType: ScopeType::CREATED_BY, onlyTables: ['user'], createdByColumn: 'id' )] public function page(array $params, int $page = 1, int $pageSize = 10): array { return parent::page($params, $page, $pageSize); } } ``` 这个真实的使用示例展示了如何在 MineAdmin 中应用数据权限注解来控制用户列表的访问权限。 通过这个架构设计,MineAdmin 实现了声明式的数据权限控制,开发者只需要在方法上添加 `@DataScope` 注解,系统就会自动根据当前用户的权限策略过滤数据。 --- --- url: /v3/front/advanced/auto-import.md --- # 自动导入 ## 概述 MineAdmin 使用基于 Vite 的自动导入系统,通过 `unplugin-auto-import` 和 `unplugin-vue-components` 插件,大幅简化开发体验,减少样板代码。开发者无需手动导入常用的 Vue API、组件和自定义工具函数。 ## 自动导入范围 ::: tip 自动导入清单 在开发 `*.vue、*.ts、*.tsx` 文件时,以下内容无需手动导入: **核心框架 API** * Vue 3 所有 API (`ref`, `reactive`, `computed`, `watch` 等) * Vue Router API (`useRouter`, `useRoute` 等) * Pinia API (`defineStore`, `storeToRefs` 等) **项目特定模块** * 所有 Store 模块:`./src/store/modules/*` * 自动导入的 Hooks:`./src/hooks/auto-imports/*` * 全局组件:`./src/components/*` (仅限 `.vue` 文件) ::: ### 自动导入与手动导入的区别 MineAdmin 项目中的 hooks 分为两类: | 类型 | 位置 | 导入方式 | 使用场景 | |------|------|----------|----------| | 自动导入 Hooks | `src/hooks/auto-imports/` | 无需 import | 全局通用工具函数 | | 手动导入 Hooks | `src/hooks/` | 需要 import | 特定场景的工具函数 | ## 配置详解 ### 自动导入 API 配置 `./vite/auto-import.ts` 配置文件负责自动导入 API 和函数: ```typescript import autoImport from 'unplugin-auto-import/vite' export default function createAutoImport() { return autoImport({ // 预设的包导入 imports: [ 'vue', // Vue 3 APIs 'vue-router', // Vue Router APIs 'pinia', // Pinia APIs ], // 生成的类型定义文件 dts: './types/auto-imports.d.ts', // 自定义目录扫描 dirs: [ './src/hooks/auto-imports/**', // 自动导入的工具函数 './src/store/modules/**', // Pinia stores ], }) } ``` ### 自动导入组件配置 `./vite/components.ts` 配置文件负责自动导入 Vue 组件: ```typescript import components from 'unplugin-vue-components/vite' export default function createComponents() { return components({ dirs: ['src/components'], // 组件扫描目录 include: [/\.vue$/, /\.vue\?vue/, /\.tsx$/], // 支持的文件类型 dts: './types/components.d.ts', // 生成的类型定义文件 }) } ``` ## 实际使用示例 ### 1. 在 Vue 组件中使用自动导入 ```vue ``` ### 2. 自动导入工具函数示例 以 `useDayjs.ts` 为例,展示自动导入工具函数的实现: ```typescript // src/hooks/auto-imports/useDayjs.ts import dayjs from 'dayjs' import 'dayjs/locale/zh-cn' import relativeTime from 'dayjs/plugin/relativeTime' dayjs.extend(relativeTime) dayjs.locale('zh-cn') export default function useDayjs(date?: dayjs.ConfigType, origin: boolean = false): any { return origin ? dayjs : dayjs(date) } ``` 在组件中直接使用: ```typescript // 无需导入,直接使用 const currentTime = useDayjs().format('YYYY-MM-DD HH:mm:ss') const relativeTime = useDayjs('2023-01-01').fromNow() ``` ### 3. Store 自动导入示例 Store 模块也会自动导入: ```typescript // src/store/modules/useMenuStore.ts import { defineStore } from 'pinia' export const useMenuStore = defineStore('menu', () => { const menus = ref([]) const getMenus = () => { // 获取菜单数据的逻辑 } return { menus, getMenus } }) ``` 在组件中使用: ```vue ``` ## 项目结构详解 ``` src/ ├── hooks/ │ ├── auto-imports/ # 自动导入的工具函数 │ │ ├── useDayjs.ts # 日期处理工具 │ │ ├── useGlobal.ts # 全局配置工具 │ │ ├── useHttp.ts # HTTP 请求工具 │ │ └── useTrans.ts # 国际化工具 │ │ │ └── useCache.ts # 手动导入的工具函数 │ └── useDialog.ts │ └── useDrawer.ts │ ├── store/modules/ # 自动导入的 Store 模块 │ ├── useDictStore.ts # 字典数据 Store │ ├── useMenuStore.ts # 菜单数据 Store │ └── useKeepAliveStore.ts # 页面缓存 Store │ └── components/ # 自动导入的全局组件 ├── ma-svg-icon/ # SVG 图标组件 ├── ma-upload-image/ # 图片上传组件 └── ma-dialog/ # 对话框组件 ``` ## 类型定义文件 自动导入系统会生成两个重要的类型定义文件: ### 1. auto-imports.d.ts ```typescript // types/auto-imports.d.ts declare global { const ref: typeof import('vue')['ref'] const computed: typeof import('vue')['computed'] const useRouter: typeof import('vue-router')['useRouter'] const useDayjs: typeof import('../src/hooks/auto-imports/useDayjs')['default'] const useMenuStore: typeof import('../src/store/modules/useMenuStore')['useMenuStore'] // ... 其他自动导入的声明 } ``` ### 2. components.d.ts ```typescript // types/components.d.ts declare module '@vue/runtime-core' { export interface GlobalComponents { MaSvgIcon: typeof import('../src/components/ma-svg-icon/index.vue')['default'] MaUploadImage: typeof import('../src/components/ma-upload-image/index.vue')['default'] // ... 其他组件声明 } } ``` ## 最佳实践 ### 1. 组织自动导入工具函数 将通用的工具函数放在 `src/hooks/auto-imports/` 目录: ```typescript // ✅ 适合自动导入的工具函数 export default function useGlobal() { // 全局配置相关的工具函数 return { getAppVersion: () => import.meta.env.VITE_APP_VERSION, isProduction: () => import.meta.env.PROD } } ``` ### 2. 何时使用手动导入 某些特殊场景建议使用手动导入: ```typescript // ✅ 适合手动导入的工具函数 // src/hooks/useDialog.ts export function useDialog() { // 对话框相关的特定逻辑 // 只在需要的组件中导入使用 } ``` ### 3. 组件命名规范 自动导入的组件遵循 PascalCase 命名: ```vue ``` ## 性能优化 ### 1. 按需加载 自动导入系统支持按需加载,未使用的 API 和组件不会被打包: ```typescript // 只有实际使用的 API 才会被导入 const count = ref(0) // ref 会被导入 // const state = reactive({}) // reactive 不会被导入(未使用) ``` ### 2. Tree Shaking 配合 Vite 的 Tree Shaking 功能,可以进一步优化打包体积: ```typescript // vite.config.ts export default defineConfig({ build: { rollupOptions: { treeshake: true, // 启用 Tree Shaking }, }, }) ``` ## 故障排除 ### 1. 类型定义文件未生成 如果类型定义文件未自动生成,尝试以下解决方案: ```bash # 删除现有类型文件并重新构建 rm -rf types/auto-imports.d.ts types/components.d.ts pnpm run dev ``` ### 2. IDE 类型提示问题 确保 `tsconfig.json` 包含生成的类型定义文件: ```json { "compilerOptions": { "types": ["./types/auto-imports.d.ts", "./types/components.d.ts"] }, "include": ["types/**/*"] } ``` ### 3. 组件无法自动导入 检查组件文件结构: ``` src/components/ma-svg-icon/ ├── index.vue # ✅ 主组件文件 └── types.ts # 其他辅助文件 ``` ### 4. 调试自动导入 在开发模式下检查生成的类型文件: ```bash # 查看自动导入的 API cat types/auto-imports.d.ts # 查看自动导入的组件 cat types/components.d.ts ``` ## 扩展配置 ### 添加第三方库自动导入 ```typescript // vite/auto-import.ts export default function createAutoImport() { return autoImport({ imports: [ 'vue', 'vue-router', 'pinia', // 添加自定义库 { 'lodash-es': ['debounce', 'throttle'], '@vueuse/core': ['useLocalStorage', 'useSessionStorage'], }, ], // ...其他配置 }) } ``` ### 自定义组件库集成 ```typescript // vite/components.ts export default function createComponents() { return components({ dirs: ['src/components'], // 添加第三方组件库的自动导入 resolvers: [ // Element Plus 组件自动导入 ElementPlusResolver(), ], // ...其他配置 }) } ``` --- --- url: /v3/backend/security/client-ip.md --- # 获取客户端 IP 本文已迁移到 [Hyperf 获取客户端 IP](/backend/frameworks/hyperf/3.2/security/client-ip)。 Laravel 实现暂未提供获取客户端 IP 章节。旧地址保留用于兼容历史链接。 --- --- url: /backend/frameworks/hyperf/3.1/security/client-ip.md --- # 获取客户端Ip ::: warning 使用此方法获取客户端 ip 。需要手动 require symfony/http-foundation !!! mineadmin/support >= 3.0.21 ::: 在记录用户操作记录的中间件中,会在每次请求时记录用户的访问记录。其中包括了用户 ip 地址。 而获取客户端 ip,是由 `Support` 组件中的 [ClientIpRequestTrait](https://github.com/mineadmin/components/blob/3.0/src/Support/Request/ClientIpRequestTrait.php) 封装而来的。此处则直接借鉴了 `symfony Request` 的实现方法。 而由于在生产环境中,一般来说还会会套一层或多层反向代理。这样如果不设置受信任的代理时。则会获取到真实的反向代理 ip。以下将简单介绍下如何在反向代理后的程序中获取正确的客户端 ip 而不是局域网地址 新建一个 `app/Http/Common/Listener/SetRequestTrustedProxiesListener` 文件 ```php = 3.0.21 ::: 在记录用户操作记录的中间件中,会在每次请求时记录用户的访问记录。其中包括了用户 ip 地址。 而获取客户端 ip,是由 `Support` 组件中的 [ClientIpRequestTrait](https://github.com/mineadmin/components/blob/3.0/src/Support/Request/ClientIpRequestTrait.php) 封装而来的。此处则直接借鉴了 `symfony Request` 的实现方法。 而由于在生产环境中,一般来说还会会套一层或多层反向代理。这样如果不设置受信任的代理时。则会获取到真实的反向代理 ip。以下将简单介绍下如何在反向代理后的程序中获取正确的客户端 ip 而不是局域网地址 新建一个 `app/Http/Common/Listener/SetRequestTrustedProxiesListener` 文件 ```php UseHttp: 发起请求 UseHttp -> RequestInterceptor: 添加Authorization头 RequestInterceptor -> Backend: 发送HTTP请求 alt 请求成功 Backend -> ResponseInterceptor: 返回成功响应 ResponseInterceptor -> Frontend: 返回数据 else Token过期 (401) Backend -> ResponseInterceptor: 返回401错误 ResponseInterceptor -> TokenRefresh: 刷新Access Token alt Token刷新成功 TokenRefresh -> ResponseInterceptor: 返回新Token ResponseInterceptor -> Backend: 重新发送原请求 Backend -> ResponseInterceptor: 返回成功响应 ResponseInterceptor -> Frontend: 返回数据 else Token刷新失败 TokenRefresh -> ResponseInterceptor: 刷新失败 ResponseInterceptor -> Frontend: 跳转登录页面 end else 其他错误 Backend -> ResponseInterceptor: 返回错误响应 ResponseInterceptor -> Frontend: 显示错误信息 end @enduml ``` ## 内部请求 (useHttp) ### 基本用法 在项目的任意位置都可以直接使用 `useHttp()` 函数,无需手动导入: ```ts // 获取请求实例 const http = useHttp() // GET 请求 - 获取用户列表 const getUserList = async (params?: any) => { return await http.get('/admin/user/index', params) } // POST 请求 - 创建新用户 const createUser = async (userData: any) => { return await http.post('/admin/user/save', userData) } // PUT 请求 - 更新用户信息 const updateUser = async (id: number, userData: any) => { return await http.put(`/admin/user/update/${id}`, userData) } // DELETE 请求 - 删除用户 const deleteUser = async (id: number) => { return await http.delete(`/admin/user/destroy/${id}`) } ``` ### 高级配置 支持传入额外的 axios 配置参数: ```ts const http = useHttp() // 设置请求超时时间 const result = await http.get('/admin/user/index', {}, { timeout: 10000, // 10秒超时 headers: { 'X-Custom-Header': 'CustomValue' } }) // 上传文件请求 const uploadFile = async (file: File) => { const formData = new FormData() formData.append('file', file) return await http.post('/admin/upload/image', formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 60000 // 上传超时设为60秒 }) } // 下载文件 const downloadFile = async (fileId: string) => { return await http.get(`/admin/file/download/${fileId}`, {}, { responseType: 'blob' // 二进制数据 }) } ``` ### 实际使用示例 在组件中的完整使用示例: ```vue ``` ## Token 刷新机制 ### 自动刷新原理 MineAdmin 实现了基于双 Token 的无感刷新机制: 1. **Access Token** - 用于业务 API 认证,过期时间较短(默认1小时) 2. **Refresh Token** - 用于刷新 Access Token,过期时间较长(默认2小时) ```plantuml @startuml !define RECTANGLE class state "正常请求" as Normal state "Token验证" as Verify state "Token过期" as Expired state "刷新Token" as Refresh state "重新请求" as Retry state "登录页面" as Login [*] -> Normal: 发起API请求 Normal -> Verify: 后端验证Token Verify -> Normal: Token有效 Verify -> Expired: Token过期(401) Expired -> Refresh: 使用Refresh Token Refresh -> Retry: 刷新成功 Refresh -> Login: 刷新失败 Retry -> Normal: 使用新Token重试 Normal -> [*]: 返回结果 Login -> [*]: 用户重新登录 @enduml ``` ### 并发请求处理 当有多个并发请求时,系统会智能处理 Token 刷新: ```ts // 并发场景示例 const [users, roles, permissions] = await Promise.all([ http.get('/admin/user/index'), http.get('/admin/role/index'), http.get('/admin/permission/index') ]) // 如果 Token 过期,只会刷新一次,其他请求会等待 // 刷新完成后,所有请求会使用新 Token 重新发送 ``` 具体刷新机制详情可参考 [用户认证文档](/v3/backend/security/passport.md)。 ## 外部请求 ### 基本用法 用于请求第三方 API 或非 MineAdmin 后端服务: ```ts import request from '@/utils/http' const { createHttp } = request // 创建第三方API请求实例 const thirdPartyHttp = createHttp('https://api.example.com', { headers: { 'User-Agent': 'MineAdmin/1.0', 'X-API-Key': 'your-api-key' }, timeout: 15000 }) // 使用第三方API const getExternalData = async () => { try { const response = await thirdPartyHttp.get('/users') return response.data } catch (error) { console.error('第三方API请求失败:', error) throw error } } ``` ### 多个外部服务 可以为不同的外部服务创建多个请求实例: ```ts // 地图服务API const mapHttp = createHttp('https://api.map.com', { headers: { 'Authorization': 'Bearer map-token' } }) // 支付服务API const paymentHttp = createHttp('https://api.payment.com', { headers: { 'Authorization': 'Bearer payment-token' } }) // 短信服务API const smsHttp = createHttp('https://api.sms.com', { headers: { 'X-API-Key': 'sms-api-key' } }) // 使用示例 const sendSms = async (phone: string, message: string) => { return await smsHttp.post('/send', { phone, message }) } ``` ## 拦截器详解 ### 响应拦截器源码分析 MineAdmin 的响应拦截器位于 `src/utils/http.ts` 文件中,主要处理以下场景: 1. **成功响应处理** 2. **Token 过期自动刷新** 3. **错误状态码处理** 4. **文件下载响应处理** #### 核心拦截器逻辑 ```ts:line-numbers http.interceptors.response.use( async (response: AxiosResponse): Promise => { isLoading.value = false const userStore = useUserStore() await usePluginStore().callHooks('networkResponse', response) const config = response.config // 处理文件下载响应 if ((response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') && !/^application\/json/.test(response.headers['content-type']) && response.status === ResultCode.SUCCESS ) { return Promise.resolve(response.data) } // 处理成功响应 if (response?.data?.code === ResultCode.SUCCESS) { return Promise.resolve(response.data) } else { // 根据不同错误码进行处理 switch (response?.data?.code) { case ResultCode.UNAUTHORIZED: { // Token 过期处理逻辑 const logout = useDebounceFn( async () => { Message.error('登录状态已过期,需要重新登录', { zIndex: 9999 }) await useUserStore().logout() }, 3000, { maxWait: 5000 }, ) // 检查是否需要刷新 Token if (userStore.isLogin && !isRefreshToken.value) { isRefreshToken.value = true if (!cache.get('refresh_token')) { await logout() break } try { // 使用 Refresh Token 刷新 Access Token const refreshTokenResponse = await createHttp(null, { headers: { Authorization: `Bearer ${cache.get('refresh_token')}`, }, }).post('/admin/passport/refresh') if (refreshTokenResponse.data.code !== 200) { await logout() break } else { // 更新 Token 并重新发送请求 const { data } = refreshTokenResponse.data userStore.token = data.access_token cache.set('token', data.access_token) cache.set('expire', useDayjs().unix() + data.expire_at, { exp: data.expire_at }) cache.set('refresh_token', data.refresh_token) config.headers!.Authorization = `Bearer ${userStore.token}` requestList.value.map((cb: any) => cb()) requestList.value = [] return http(config) // 重新发送原请求 } } catch (e: any) { requestList.value.map((cb: any) => cb()) await logout() break } finally { requestList.value = [] isRefreshToken.value = false } } else { // 如果正在刷新 Token,将请求加入队列等待 return new Promise((resolve) => { requestList.value.push(() => { config.headers!.Authorization = `Bearer ${cache.get('token')}` resolve(http(config)) }) }) } } case ResultCode.NOT_FOUND: Message.error('服务器资源不存在', { zIndex: 9999 }) break case ResultCode.FORBIDDEN: Message.error('没有权限访问此接口', { zIndex: 9999 }) break case ResultCode.METHOD_NOT_ALLOWED: Message.error('请求方法不被允许', { zIndex: 9999 }) break case ResultCode.FAIL: Message.error('服务器内部错误', { zIndex: 9999 }) break default: Message.error(response?.data?.message ?? '未知错误', { zIndex: 9999 }) break } return Promise.reject(response.data ? response.data : null) } }, // 网络错误处理 async (error: any) => { isLoading.value = false const serverError = useDebounceFn(async () => { if (error && error.response && error.response.status === 500) { Message.error(error.message ?? '服务器内部错误', { zIndex: 9999 }) } }, 3000, { maxWait: 5000 }) await serverError() return Promise.reject(error) }, ) ``` ### 状态码处理机制 系统对不同的 HTTP 状态码和业务错误码进行了统一处理: | 状态码 | 说明 | 处理方式 | |--------|------|----------| | `200` (SUCCESS) | 请求成功 | 直接返回数据 | | `401` (UNAUTHORIZED) | Token 过期或无效 | 自动刷新 Token 或跳转登录 | | `403` (FORBIDDEN) | 权限不足 | 显示权限错误提示 | | `404` (NOT\_FOUND) | 资源不存在 | 显示资源不存在提示 | | `405` (METHOD\_NOT\_ALLOWED) | 请求方法不允许 | 显示方法错误提示 | | `500` (INTERNAL\_ERROR) | 服务器内部错误 | 显示服务器错误提示 | ### 自定义拦截器 如果需要为外部请求自定义拦截器,可以这样做: ```ts import request from '@/utils/http' const { createHttp } = request // 创建带自定义拦截器的请求实例 const customHttp = createHttp('https://api.custom.com') // 添加请求拦截器 customHttp.interceptors.request.use( (config) => { // 在发送请求之前做一些处理 config.headers['X-Timestamp'] = Date.now() console.log('发送请求:', config) return config }, (error) => { console.error('请求错误:', error) return Promise.reject(error) } ) // 添加响应拦截器 customHttp.interceptors.response.use( (response) => { // 处理响应数据 console.log('收到响应:', response) if (response.data.status === 'error') { throw new Error(response.data.message) } return response }, (error) => { // 处理响应错误 console.error('响应错误:', error) return Promise.reject(error) } ) ``` ## 最佳实践 ### 1. 错误处理 建议在组件中统一处理错误: ```ts // composables/useApi.ts export const useApi = () => { const http = useHttp() const handleError = (error: any, defaultMessage = '操作失败') => { const message = error?.message || error?.data?.message || defaultMessage ElMessage.error(message) console.error('API错误:', error) } const safeRequest = async (requestFn: () => Promise, errorMessage?: string): Promise => { try { return await requestFn() } catch (error) { handleError(error, errorMessage) return null } } return { http, handleError, safeRequest } } ``` ### 2. 类型定义 为 API 响应定义明确的类型: ```ts // types/api.ts export interface ApiResponse { code: number message: string data: T } export interface PaginatedResponse { items: T[] total: number page: number size: number } export interface User { id: number username: string email: string status: number } // 使用示例 const getUserList = async (): Promise>> => { return await http.get('/admin/user/index') } ``` ### 3. 请求封装 将常用的 API 请求封装成可复用的服务: ```ts // services/userService.ts export class UserService { private http = useHttp() async getList(params: any) { return await this.http.get('/admin/user/index', params) } async create(user: Partial) { return await this.http.post('/admin/user/save', user) } async update(id: number, user: Partial) { return await this.http.put(`/admin/user/update/${id}`, user) } async delete(id: number) { return await this.http.delete(`/admin/user/destroy/${id}`) } async batchDelete(ids: number[]) { return await this.http.post('/admin/user/destroy', { ids }) } } // 创建服务实例 export const userService = new UserService() ``` ### 4. 加载状态管理 合理使用加载状态提升用户体验: ```vue ``` ## 常见问题 ### Q: Token 刷新期间的并发请求如何处理? A: 系统会将所有需要 Token 的请求暂存在队列中,等待 Token 刷新完成后统一使用新 Token 重新发送。 ### Q: 如何处理文件上传进度? A: 可以使用 axios 的 `onUploadProgress` 配置: ```ts const uploadWithProgress = async (file: File, onProgress?: (progress: number) => void) => { const formData = new FormData() formData.append('file', file) return await http.post('/admin/upload', formData, { onUploadProgress: (progressEvent) => { if (progressEvent.total && onProgress) { const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100) onProgress(progress) } } }) } ``` ### Q: 如何取消正在进行的请求? A: 使用 axios 的取消令牌: ```ts import { ref, onUnmounted } from 'vue' const controller = ref() const fetchData = async () => { // 取消之前的请求 controller.value?.abort() // 创建新的控制器 controller.value = new AbortController() try { const response = await http.get('/admin/data', {}, { signal: controller.value.signal }) return response } catch (error) { if (error.name === 'AbortError') { console.log('请求已取消') } else { throw error } } } // 组件卸载时取消请求 onUnmounted(() => { controller.value?.abort() }) ``` --- --- url: /v3/guide/contributions.md --- # 贡献指南 :::tip 共建开源 开源需要大家一起来支持,支持的方式有很多种,比如使用、推荐、写教程、保护生态、贡献代码、回答问题、分享经验等;欢迎您加入我们! ::: ## 仓库地址 > 请不要贡献到 Gitee 仓库,Gitee提交的代码会被Github仓库覆盖、而且贡献人列表也不会出现您的名字 ### Github * [MineAdmin 后端源代码](https://github.com/mineadmin/mineadmin) * [MineAdmin 前端源代码](https://github.com/mineadmin/mineadmin-vue) * [MineAdmin 内核组件](https://github.com/mineadmin/components) * [MineAdmin 文档](https://github.com/mineadmin/doc-v3) ### Gitee * [MineAdmin 后端源代码](https://gitee.com/mineadmin/mineadmin) * [MineAdmin 前端源代码](https://gitee.com/mineadmin/mineadmin-vue) ## 你可以做什么 ### 关注 [issues](https://github.com/mineadmin/mineadmin/issues) 动态 * 我们会在 issues 中发布一些待开发的功能,如果你感兴趣,可以在 issues 中留言,我们会尽快回复。 * 评论回复帮助提出疑问的用户; * 根据[issues](https://github.com/mineadmin/mineadmin/issues)内容,提出合理的解决方案;去修复bug或者实现功能,并以 [pull request](https://github.com/mineadmin/mineadmin/pulls) 形式提交至 MineAdmin 仓库 * 关注自己提交 Pull Request 的进度和状态,以推动您的 Pull Request 尽快合入主仓库; * 对其他人提交的 Pull Request 进行 Code Review,并给出您的建议和看法。 * 根据他人或自己的需求,研发独立的功能组件; * 完善[文档](https://gitee.com/mineadmin/doc-v3),提供更好的使用说明。 ### Pull Request 指南 虽然我们会定期发布一些待开发的功能,但是我们更欢迎你自己提出你想要实现的功能。你可以在 [issues](https://github.com/mineadmin/mineadmin/issues) 中提出你的想法,我们会尽快回复是否接受。 在提交问题之前,请检查是否已经发布了类似的问题。 * fork 本仓库到你的 Github 账号下; * 提交信息的格式应为 \[File Name]: Info about commit. (例如) README.md: Fix xxx bug * 提交代码前,请先执行 `composer cs-fix` 进行代码格式化; * 提交代码前,请先执行 `composer an` 进行代码静态检查; * 提交代码前,请先执行 `composer test` 进行单元测试;单元测试不要在您的任何生产环境上运行,因为它会删除添加数据; * 确保将 PR 创建为你的功能分支, 而不是 master 分支上直接提交修改。 * 如果你的 PR 修复了 bug,请提供有关相关 bug 的描述。 --- --- url: /v3/backend/base/router.md --- # 路由与 API 文档 本文已迁移到 [Hyperf 路由与 API 文档](/backend/frameworks/hyperf/3.2/base/router)。 跨框架一致的路由和接口元数据约定,请阅读 [后台路由契约](/v3/backend/contracts/routing) 和 [接口元数据契约](/v3/backend/contracts/api-metadata)。 --- --- url: /backend/frameworks/hyperf/3.1/base/router.md --- # 路由与API文档系统 ## 目录 1. [概述与架构](#_1-概述与架构) 2. [快速开始](#_2-快速开始) 3. [HTTP规范与最佳实践](#_3-http规范与最佳实践) 4. [响应结构体系统](#_4-响应结构体系统) 5. [MineAdmin自定义注解](#_5-mineadmin自定义注解) 6. [实际应用示例](#_6-实际应用示例) 7. [常见问题与解决方案](#_10-常见问题与解决方案) *** ## 1. 概述与架构 ### 1.1 系统概述 MineAdmin 内置了完整的 API 文档生成系统,基于 [Swagger/OpenAPI 3.0](https://swagger.io) 规范,为开发者提供了强大的 API 文档自动生成和管理功能。 **访问方式**: 本地开发时访问 `http://localhost:9503/swagger` 查看完整的 API 文档 ### 1.2 架构层级 ::: tip 技术栈架构 MineAdmin 的 API 文档系统采用多层架构设计: * **[mineadmin/swagger](https://github.com/mineadmin/Swagger)** - MineAdmin 专用的 Swagger 注解封装层 * **[hyperf/swagger](https://github.com/hyperf/swagger)** - Hyperf 框架的 Swagger 集成组件 * **[zircote/swagger-php](https://github.com/zircote/swagger-php)** - PHP Swagger 注解处理核心 * **[OpenAPI 规范](https://github.com/OAI/OpenAPI-Specification)** - 业界标准的 API 文档规范 ::: ### 1.3 系统架构图 ```plantuml @startuml !define LIGHTYELLOW #fff3e0 !define LIGHTBLUE #e1f5fe !define LIGHTPURPLE #f3e5f5 node "MineAdmin Application" as A LIGHTBLUE node "Controller Layer" as B node "MineAdmin Swagger Annotations" as C LIGHTYELLOW node "Hyperf Swagger Component" as D node "Swagger-PHP Core" as E node "OpenAPI 3.0 Specification" as F node "Swagger UI Documentation" as G LIGHTPURPLE node "Request/Response Models" as H node "Validation Rules" as I node "Schema Definitions" as J A --> B B --> C C --> D D --> E E --> F F --> G H --> C I --> C J --> C @enduml ``` ### 1.4 核心优势 * **自动化文档生成**: 基于代码注解自动生成完整的 API 文档 * **类型安全**: 强类型支持,确保文档与实际代码一致 * **实时同步**: 代码变更时文档自动更新 * **交互式测试**: 内置的 Swagger UI 支持直接测试 API *** ## 2. 快速开始 ### 2.1 基础配置 确保你的项目已正确安装 MineAdmin Swagger 组件: ```bash composer require mineadmin/swagger ``` ### 2.2 第一个 API 接口 创建一个简单的 API 接口: ```php 1, "name" => "张三"]), title: "获取成功", description: "成功获取用户信息" )] public function getUserInfo(): Result { return $this->success([ 'id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com' ]); } } ``` ### 2.3 访问文档 启动服务后,访问 `http://localhost:9503/swagger` 查看生成的文档。 *** ## 3. HTTP规范与最佳实践 ### 3.1 RESTful API 设计原则 MineAdmin 推荐遵循 RESTful 架构风格,确保 API 接口的一致性和可预测性。 #### 3.1.1 HTTP 方法映射 ```plantuml @startuml !define LIGHTGREEN #e8f5e8 !define LIGHTORANGE #fff2e8 !define LIGHTBLUE #e8f2ff !define LIGHTRED #ffe8e8 !define LIGHTPURPLE #f5e8ff node "HTTP Methods" as A node "GET - 查询数据" as B LIGHTGREEN node "POST - 创建数据" as C LIGHTORANGE node "PUT - 更新数据" as D LIGHTBLUE node "DELETE - 删除数据" as E LIGHTRED node "PATCH - 部分更新" as F LIGHTPURPLE A --> B A --> C A --> D A --> E A --> F @enduml ``` #### 3.1.2 标准路由设计模式 以用户管理模块为例,展示标准的 RESTful API 设计: | HTTP方法 | 路由路径 | 功能描述 | 响应数据 | |---------|----------|---------|----------| | `GET` | `/admin/user/list` | 获取用户列表(分页) | 用户列表数据 | | `GET` | `/admin/user/{id}` | 获取单个用户详情 | 单个用户数据 | | `POST` | `/admin/user` | 创建新用户 | 创建的用户数据 | | `PUT` | `/admin/user/{id}` | 完整更新用户信息 | 更新后的用户数据 | | `PATCH` | `/admin/user/{id}` | 部分更新用户信息 | 更新后的用户数据 | | `DELETE` | `/admin/user/{id}` | 删除用户 | 删除确认信息 | #### 3.1.3 最佳实践建议 ::: tip 设计原则 1. **资源命名**: 使用名词而非动词,采用复数形式 ``` ✅ /admin/users ❌ /admin/getUsers ``` 2. **嵌套资源**: 体现资源间的层次关系 ``` ✅ /admin/users/{id}/roles ❌ /admin/user-roles?user_id={id} ``` 3. **状态码语义**: 正确使用 HTTP 状态码 ``` 200 - 请求成功 201 - 资源创建成功 400 - 请求参数错误 401 - 未授权访问 403 - 权限不足 404 - 资源不存在 500 - 服务器内部错误 ``` 4. **灵活性优先**: 规范是基础,业务需求是核心 * 遵循 RESTful 原则但不拘泥于严格规范 * 以业务的可持续迭代为主要考量 * 保持团队内部的一致性 ::: ### 3.2 URL 设计规范 #### 3.2.1 命名约定 ```php // 推荐的命名方式 GET /admin/users // 获取用户列表 GET /admin/users/{id} // 获取指定用户 POST /admin/users // 创建用户 PUT /admin/users/{id} // 更新用户 DELETE /admin/users/{id} // 删除用户 // 特殊操作的命名 POST /admin/users/{id}/enable // 启用用户 POST /admin/users/{id}/disable // 禁用用户 GET /admin/users/search // 搜索用户 ``` #### 3.2.2 参数传递规范 ```php // 查询参数 - 用于过滤、排序、分页 GET /admin/users?page=1&page_size=20&status=active&sort=created_at,desc // 路径参数 - 用于唯一标识资源 GET /admin/users/123 // 请求体参数 - 用于复杂数据传递 POST /admin/users Content-Type: application/json { "username": "zhangsan", "email": "zhangsan@example.com", "roles": [1, 2, 3] } ``` *** ## 4. 响应结构体系统 ### 4.1 统一响应格式 MineAdmin 采用统一的响应结构 `\App\Http\Common\Result`,确保所有 API 接口返回格式的一致性。 ### 4.2 Result 类架构 ```plantuml @startuml class Result { +ResultCode code +string message +mixed data +__construct(code, message, data) +toArray() : array } enum ResultCode { SUCCESS = 200 FAIL = 500 UNAUTHORIZED = 401 FORBIDDEN = 403 NOT_FOUND = 404 -- +getMessage(value) : string } class AbstractController { #success(data, message) : Result #error(message, data) : Result #json(code, data, message) : Result } Result --> ResultCode AbstractController --> Result @enduml ``` ### 4.3 核心实现代码 #### 4.3.1 Result 响应类 ::: code-group ```php [Result.php] message === null) { $this->message = ResultCode::getMessage($this->code->value); } } /** * 转换为数组格式 */ public function toArray(): array { return [ 'code' => $this->code->value, 'message' => $this->message, 'data' => $this->data, ]; } } ``` ```php [AbstractController.php] success([ 'list' => $list, 'total' => $total, 'page' => $page, 'page_size' => $pageSize, 'total_pages' => ceil($total / $pageSize) ]); } } ``` ```php [AdminController.php] getRequest()->input('page', 1); } /** * 获取每页大小 */ protected function getPageSize(int $default = 10, int $max = 100): int { $size = (int) $this->getRequest()->input('page_size', $default); return min($size, $max); // 限制最大页面大小 } /** * 获取请求实例 */ protected function getRequest(): RequestInterface { return ApplicationContext::getContainer()->get(RequestInterface::class); } /** * 获取排序参数 */ protected function getOrderBy(string $default = 'id'): array { $sort = $this->getRequest()->input('sort', $default); $order = $this->getRequest()->input('order', 'asc'); return [$sort, in_array(strtolower($order), ['asc', 'desc']) ? $order : 'asc']; } } ``` ::: ### 4.4 ResultCode 枚举类 MineAdmin 提供了一套完整的业务状态码枚举系统,用于标准化 API 响应的状态信息。 #### 4.4.1 核心实现 ```php userService->getList(); return $this->success($users, '获取用户列表成功'); } catch (ValidationException $e) { return $this->json(ResultCode::VALIDATION_ERROR, [], $e->getMessage()); } catch (\Exception $e) { return $this->error('系统异常,请稍后重试'); } } } ``` *** ## 5. MineAdmin自定义注解 MineAdmin 提供了三个核心的自定义 Swagger 注解,用于简化 API 文档的编写和维护。所有注解都位于 `Mine\Swagger\Attributes\` 命名空间下。 ### 5.1 注解架构概览 ```plantuml @startuml class SwaggerAnnotation class ResultResponse { +object|string instance +string title +array examples +string description +mixed example +array headers +int response } class PageResponse { +object|string instance +string title +array examples +string description +mixed example +array headers +int response } class FormRequest { +string schema +string title +string description +array required +array properties +array only } SwaggerAnnotation <|-- ResultResponse SwaggerAnnotation <|-- PageResponse SwaggerAnnotation <|-- FormRequest @enduml ``` ### 5.2 ResultResponse 注解 用于定义单个资源或操作的响应结构,自动生成标准的 API 响应文档。 #### 5.2.1 构造函数签名 ```php ResultResponse::__construct( object|string $instance, // 响应数据的类实例或类名 ?string $title = null, // 响应标题 ?array $examples = null, // 多个示例数组 ?string $description = null, // 响应描述 mixed $example = Generator::UNDEFINED, // 单个示例 ?array $headers = null, // 响应头信息 ?int $response = 200 // HTTP状态码 ) ``` #### 5.2.2 参数详解 | 参数 | 类型 | 必填 | 说明 | |-----|------|------|------| | `$instance` | `object\|string` | ✅ | 响应数据的类实例或类名,支持自动解析注解 | | `$title` | `string` | ❌ | 响应的标题,用于文档显示 | | `$examples` | `array` | ❌ | 多个响应示例,键值对形式 | | `$description` | `string` | ❌ | 详细的响应说明 | | `$example` | `mixed` | ❌ | 单个响应示例,JSON字符串或对象 | | `$headers` | `array` | ❌ | 自定义响应头信息 | | `$response` | `int` | ❌ | HTTP状态码,默认200 | #### 5.2.3 实际应用示例 基于用户登录接口的完整示例: ::: code-group ```php [登录控制器] validated(); $tokenData = $this->authService->login($credentials); return $this->success($tokenData, '登录成功'); } } ``` ```php [响应数据模型] ['type' => 'integer', 'description' => '用户ID'], 'username' => ['type' => 'string', 'description' => '用户名'], 'nickname' => ['type' => 'string', 'description' => '昵称'], ] )] public array $user_info; } ``` ::: #### 5.2.4 最佳实践 ::: warning 注意事项 1. **instance 参数**: 推荐使用具体的类实例而非类名,确保注解能正确解析 2. **示例数据**: 提供真实、完整的示例数据,便于前端开发者理解 3. **描述信息**: 详细说明响应的业务含义和使用场景 4. **状态码**: 根据实际业务情况设置合适的 HTTP 状态码 ::: ### 5.3 PageResponse 注解 专门用于分页数据的响应结构注解,自动生成包含分页信息的标准响应文档。 #### 5.3.1 构造函数签名 `PageResponse` 的构造函数与 `ResultResponse` 完全一致,但在语义上专门用于分页响应。 ```php PageResponse::__construct( object|string $instance, // 分页数据项的类实例或类名 ?string $title = null, // 响应标题 ?array $examples = null, // 多个示例数组 ?string $description = null, // 响应描述 mixed $example = Generator::UNDEFINED, // 单个示例 ?array $headers = null, // 响应头信息 ?int $response = 200 // HTTP状态码 ) ``` #### 5.3.2 分页响应结构 ```plantuml @startuml !define LIGHTGREEN #e8f5e8 !define LIGHTYELLOW #fff3e0 node "PageResponse" as A LIGHTGREEN node "Result Structure" as B node "code: 200" as C node "message: string" as D node "data: object" as E LIGHTYELLOW node "list: array" as F node "total: int" as G node "page: int" as H node "page_size: int" as I node "total_pages: int" as J A --> B B --> C B --> D B --> E E --> F E --> G E --> H E --> I E --> J @enduml ``` ### 5.4 FormRequest 注解 专门用于请求参数的结构化文档注解,基于现有的 Schema 类自动生成请求参数文档。 #### 5.4.1 构造函数签名 ```php FormRequest::__construct( ?string $schema = null, // 需要解析的 schema 类名 ?string $title = null, // 表单标题 ?string $description = null, // 表单描述 ?array $required = null, // 必填字段数组 ?array $properties = null, // 额外的属性定义 array $only = [] // 只显示指定的字段 ) ``` #### 5.4.2 参数详解 | 参数 | 类型 | 必填 | 说明 | |-----|------|------|------| | `$schema` | `string` | ❌ | 基础 Schema 类名,用于字段解析 | | `$title` | `string` | ❌ | 请求表单的标题 | | `$description` | `string` | ❌ | 请求表单的详细描述 | | `$required` | `array` | ❌ | 必填字段列表 | | `$properties` | `array` | ❌ | 额外的字段属性定义 | | `$only` | `array` | ❌ | 只显示指定的字段,用于字段过滤 | *** ## 6. 实际应用示例 ### 6.1 完整的CRUD接口示例 以文章管理为例,展示完整的CRUD接口实现: ```php getRequest()->all(); $page = $this->getCurrentPage(); $pageSize = $this->getPageSize(); $result = $this->articleService->paginate($filters, $page, $pageSize); return $this->paginate($result['list'], $result['total'], $page, $pageSize); } /** * 获取单篇文章 */ #[OA\Get( path: "/admin/articles/{id}", summary: "获取文章详情", description: "根据ID获取单篇文章的详细信息" )] #[OA\Parameter(name: "id", description: "文章ID", in: "path", required: true, schema: new OA\Schema(type: "integer"))] #[ResultResponse(instance: new Result(data: new ArticleSchema()), title: "文章详情")] public function show(int $id): Result { $article = $this->articleService->findById($id); return $this->success($article); } /** * 创建文章 */ #[OA\Post( path: "/admin/articles", summary: "创建文章", description: "创建新的文章" )] #[OA\RequestBody(content: new OA\JsonContent(ref: ArticleRequest::class))] #[ResultResponse(instance: new Result(data: new ArticleSchema()), title: "创建成功", response: 201)] public function store(ArticleRequest $request): Result { $data = $request->validated(); $article = $this->articleService->create($data); return $this->success($article, '文章创建成功'); } /** * 更新文章 */ #[OA\Put( path: "/admin/articles/{id}", summary: "更新文章", description: "更新指定文章的信息" )] #[OA\Parameter(name: "id", description: "文章ID", in: "path", required: true, schema: new OA\Schema(type: "integer"))] #[OA\RequestBody(content: new OA\JsonContent(ref: ArticleRequest::class))] #[ResultResponse(instance: new Result(data: new ArticleSchema()), title: "更新成功")] public function update(int $id, ArticleRequest $request): Result { $data = $request->validated(); $article = $this->articleService->update($id, $data); return $this->success($article, '文章更新成功'); } /** * 删除文章 */ #[OA\Delete( path: "/admin/articles/{id}", summary: "删除文章", description: "删除指定的文章" )] #[OA\Parameter(name: "id", description: "文章ID", in: "path", required: true, schema: new OA\Schema(type: "integer"))] #[ResultResponse(instance: new Result(), title: "删除成功")] public function destroy(int $id): Result { $this->articleService->delete($id); return $this->success([], '文章删除成功'); } } ``` ### 6.2 请求验证类示例 ```php 'required|string|max:200', 'content' => 'required|string', 'excerpt' => 'nullable|string|max:500', 'status' => 'required|integer|in:0,1', 'category_id' => 'nullable|integer|exists:categories,id', 'tags' => 'nullable|array', 'tags.*' => 'integer|exists:tags,id', ]; } public function attributes(): array { return [ 'title' => '文章标题', 'content' => '文章内容', 'excerpt' => '文章摘要', 'status' => '发布状态', 'category_id' => '分类ID', 'tags' => '标签列表', ]; } } ``` ### 6.3 数据模型Schema示例 ```php ["type" => "integer", "description" => "分类ID"], "name" => ["type" => "string", "description" => "分类名称"] ] )] public ?array $category; #[OA\Property(property: "tags", title: "文章标签", type: "array", items: new OA\Items(type: "object", properties: [ "id" => ["type" => "integer", "description" => "标签ID"], "name" => ["type" => "string", "description" => "标签名称"] ] ) )] public array $tags; #[OA\Property(property: "created_at", title: "创建时间", type: "string", format: "date-time")] public string $created_at; #[OA\Property(property: "updated_at", title: "更新时间", type: "string", format: "date-time")] public string $updated_at; } ``` *** ## 7. 性能优化与最佳实践 #### 7.1 选择性扫描 ```php // config/autoload/swagger.php // 只扫描必要的目录,避免全项目扫描 'scan' => [ 'paths' => [ Finder::create() ->in([BASE_PATH . '/app/Http', BASE_PATH . '/app/Schema']) ->name('*.php') ->getIterator() ], ], ``` ### 7.2 代码组织最佳实践 #### 7.2.1 目录结构建议 ``` app/ ├── Http/ │ ├── Admin/ │ │ ├── Controller/ # 管理后台控制器 │ │ └── Request/ # 请求验证类 │ └── Common/ │ ├── Result.php # 统一响应结构 │ └── ResultCode.php # 状态码枚举 ├── Schema/ # Swagger Schema 定义 │ ├── UserSchema.php │ └── ArticleSchema.php └── Service/ # 业务逻辑层 ├── UserService.php └── ArticleService.php ``` *** ## 8. 错误处理与调试 ### 8.1 常见错误类型 #### 8.1.1 注解解析错误 ```php // ❌ 错误示例 - 注解语法错误 #[ResultResponse( instance: UserSchema, // 缺少 ::class title = '用户信息' // 使用了 = 而不是 : )] // ✅ 正确示例 #[ResultResponse( instance: UserSchema::class, title: '用户信息' )] ``` #### 8.1.2 循环引用问题 ```php // ❌ 可能导致循环引用 class UserSchema { #[OA\Property(ref: 'GroupSchema')] public GroupSchema $group; } class GroupSchema { #[OA\Property(type: 'array', items: new OA\Items(ref: 'UserSchema'))] public array $users; } // ✅ 使用懒加载避免循环引用 class UserSchema { #[OA\Property(ref: '#/components/schemas/GroupSchema')] public array $group; } ``` ### 8.2 调试技巧 #### 8.2.1 启用详细错误日志 ```php // config/autoload/logger.php return [ 'swagger' => [ 'handler' => [ 'class' => Monolog\Handler\StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/swagger.log', 'level' => Monolog\Logger::DEBUG, ], ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, ], ], ]; ``` #### 8.2.2 文档相关命令 ```shell # 重新生成Swagger文档 php bin/hyperf.php gen:swagger # 根据指定的 model 生成 Swagger Schema php bin/hyperf.php gen:swagger-schema ``` *** ## 10. 常见问题与解决方案 ### 10.1 注解相关问题 #### 10.1.1 注解不生效 **问题描述**: 添加了注解但在Swagger文档中不显示 **可能原因**: 1. 注解语法错误 2. 类未被扫描到 3. 缓存问题 **解决方案**: ```php // 1. 检查注解语法 #[OA\Get(path: "/users", summary: "获取用户列表")] // ✅ 正确 // #[OA\Get(path = "/users", summary = "获取用户列表")] // ❌ 错误语法 // 2. 确保类在扫描范围内 // config/autoload/swagger.php 'scan' => [ 'paths' => [ Finder::create() ->in([BASE_PATH . '/app/Http', BASE_PATH . '/app/Schema']) ->name('*.php') ->getIterator() ], ], // 3. 清除缓存并重新生成文档 php bin/hyperf.php swagger:generate ``` #### 10.1.2 Schema引用问题 **问题描述**: Schema引用无法正确解析 **解决方案**: ```php // ❌ 错误的引用方式 #[OA\Property(ref: 'UserSchema')] // ✅ 正确的引用方式 #[OA\Property(ref: '#/components/schemas/UserSchema')] // 或者使用类引用 #[ResultResponse(instance: UserSchema::class)] ``` ### 10.2 最佳实践总结 #### 10.2.1 开发阶段 1. **渐进式添加**: 先为核心接口添加文档,再逐步完善 2. **模板复用**: 创建通用的注解模板,提高开发效率 3. **即时验证**: 开发过程中及时检查文档生成效果 #### 10.2.2 生产环境 1. **定期更新**: 建立文档更新机制,确保文档与代码同步 2. **访问控制**: 生产环境考虑对Swagger UI的访问控制 #### 10.2.3 团队协作 1. **规范制定**: 建立团队统一的注解编写规范 2. **Code Review**: 将API文档检查纳入代码审查流程 3. **自动化**: 通过CI/CD自动检查文档完整性 *** --- --- url: /backend/frameworks/hyperf/3.2/base/router.md --- # 路由与API文档系统 ## 目录 1. [概述与架构](#_1-概述与架构) 2. [快速开始](#_2-快速开始) 3. [HTTP规范与最佳实践](#_3-http规范与最佳实践) 4. [响应结构体系统](#_4-响应结构体系统) 5. [MineAdmin自定义注解](#_5-mineadmin自定义注解) 6. [实际应用示例](#_6-实际应用示例) 7. [常见问题与解决方案](#_10-常见问题与解决方案) *** ## 1. 概述与架构 ### 1.1 系统概述 MineAdmin 内置了完整的 API 文档生成系统,基于 [Swagger/OpenAPI 3.0](https://swagger.io) 规范,为开发者提供了强大的 API 文档自动生成和管理功能。 **访问方式**: 本地开发时访问 `http://localhost:9503/swagger` 查看完整的 API 文档 ### 1.2 架构层级 ::: tip 技术栈架构 MineAdmin 的 API 文档系统采用多层架构设计: * **[mineadmin/swagger](https://github.com/mineadmin/Swagger)** - MineAdmin 专用的 Swagger 注解封装层 * **[hyperf/swagger](https://github.com/hyperf/swagger)** - Hyperf 框架的 Swagger 集成组件 * **[zircote/swagger-php](https://github.com/zircote/swagger-php)** - PHP Swagger 注解处理核心 * **[OpenAPI 规范](https://github.com/OAI/OpenAPI-Specification)** - 业界标准的 API 文档规范 ::: ### 1.3 系统架构图 ```plantuml @startuml !define LIGHTYELLOW #fff3e0 !define LIGHTBLUE #e1f5fe !define LIGHTPURPLE #f3e5f5 node "MineAdmin Application" as A LIGHTBLUE node "Controller Layer" as B node "MineAdmin Swagger Annotations" as C LIGHTYELLOW node "Hyperf Swagger Component" as D node "Swagger-PHP Core" as E node "OpenAPI 3.0 Specification" as F node "Swagger UI Documentation" as G LIGHTPURPLE node "Request/Response Models" as H node "Validation Rules" as I node "Schema Definitions" as J A --> B B --> C C --> D D --> E E --> F F --> G H --> C I --> C J --> C @enduml ``` ### 1.4 核心优势 * **自动化文档生成**: 基于代码注解自动生成完整的 API 文档 * **类型安全**: 强类型支持,确保文档与实际代码一致 * **实时同步**: 代码变更时文档自动更新 * **交互式测试**: 内置的 Swagger UI 支持直接测试 API *** ## 2. 快速开始 ### 2.1 基础配置 确保你的项目已正确安装 MineAdmin Swagger 组件: ```bash composer require mineadmin/swagger ``` ### 2.2 第一个 API 接口 创建一个简单的 API 接口: ```php 1, "name" => "张三"]), title: "获取成功", description: "成功获取用户信息" )] public function getUserInfo(): Result { return $this->success([ 'id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com' ]); } } ``` ### 2.3 访问文档 启动服务后,访问 `http://localhost:9503/swagger` 查看生成的文档。 *** ## 3. HTTP规范与最佳实践 ### 3.1 RESTful API 设计原则 MineAdmin 推荐遵循 RESTful 架构风格,确保 API 接口的一致性和可预测性。 #### 3.1.1 HTTP 方法映射 ```plantuml @startuml !define LIGHTGREEN #e8f5e8 !define LIGHTORANGE #fff2e8 !define LIGHTBLUE #e8f2ff !define LIGHTRED #ffe8e8 !define LIGHTPURPLE #f5e8ff node "HTTP Methods" as A node "GET - 查询数据" as B LIGHTGREEN node "POST - 创建数据" as C LIGHTORANGE node "PUT - 更新数据" as D LIGHTBLUE node "DELETE - 删除数据" as E LIGHTRED node "PATCH - 部分更新" as F LIGHTPURPLE A --> B A --> C A --> D A --> E A --> F @enduml ``` #### 3.1.2 标准路由设计模式 以用户管理模块为例,展示标准的 RESTful API 设计: | HTTP方法 | 路由路径 | 功能描述 | 响应数据 | |---------|----------|---------|----------| | `GET` | `/admin/user/list` | 获取用户列表(分页) | 用户列表数据 | | `GET` | `/admin/user/{id}` | 获取单个用户详情 | 单个用户数据 | | `POST` | `/admin/user` | 创建新用户 | 创建的用户数据 | | `PUT` | `/admin/user/{id}` | 完整更新用户信息 | 更新后的用户数据 | | `PATCH` | `/admin/user/{id}` | 部分更新用户信息 | 更新后的用户数据 | | `DELETE` | `/admin/user/{id}` | 删除用户 | 删除确认信息 | #### 3.1.3 最佳实践建议 ::: tip 设计原则 1. **资源命名**: 使用名词而非动词,采用复数形式 ``` ✅ /admin/users ❌ /admin/getUsers ``` 2. **嵌套资源**: 体现资源间的层次关系 ``` ✅ /admin/users/{id}/roles ❌ /admin/user-roles?user_id={id} ``` 3. **状态码语义**: 正确使用 HTTP 状态码 ``` 200 - 请求成功 201 - 资源创建成功 400 - 请求参数错误 401 - 未授权访问 403 - 权限不足 404 - 资源不存在 500 - 服务器内部错误 ``` 4. **灵活性优先**: 规范是基础,业务需求是核心 * 遵循 RESTful 原则但不拘泥于严格规范 * 以业务的可持续迭代为主要考量 * 保持团队内部的一致性 ::: ### 3.2 URL 设计规范 #### 3.2.1 命名约定 ```php // 推荐的命名方式 GET /admin/users // 获取用户列表 GET /admin/users/{id} // 获取指定用户 POST /admin/users // 创建用户 PUT /admin/users/{id} // 更新用户 DELETE /admin/users/{id} // 删除用户 // 特殊操作的命名 POST /admin/users/{id}/enable // 启用用户 POST /admin/users/{id}/disable // 禁用用户 GET /admin/users/search // 搜索用户 ``` #### 3.2.2 参数传递规范 ```php // 查询参数 - 用于过滤、排序、分页 GET /admin/users?page=1&page_size=20&status=active&sort=created_at,desc // 路径参数 - 用于唯一标识资源 GET /admin/users/123 // 请求体参数 - 用于复杂数据传递 POST /admin/users Content-Type: application/json { "username": "zhangsan", "email": "zhangsan@example.com", "roles": [1, 2, 3] } ``` *** ## 4. 响应结构体系统 ### 4.1 统一响应格式 MineAdmin 采用统一的响应结构 `\App\Http\Common\Result`,确保所有 API 接口返回格式的一致性。 ### 4.2 Result 类架构 ```plantuml @startuml class Result { +ResultCode code +string message +mixed data +__construct(code, message, data) +toArray() : array } enum ResultCode { SUCCESS = 200 FAIL = 500 UNAUTHORIZED = 401 FORBIDDEN = 403 NOT_FOUND = 404 -- +getMessage(value) : string } class AbstractController { #success(data, message) : Result #error(message, data) : Result #json(code, data, message) : Result } Result --> ResultCode AbstractController --> Result @enduml ``` ### 4.3 核心实现代码 #### 4.3.1 Result 响应类 ::: code-group ```php [Result.php] message === null) { $this->message = ResultCode::getMessage($this->code->value); } } /** * 转换为数组格式 */ public function toArray(): array { return [ 'code' => $this->code->value, 'message' => $this->message, 'data' => $this->data, ]; } } ``` ```php [AbstractController.php] success([ 'list' => $list, 'total' => $total, 'page' => $page, 'page_size' => $pageSize, 'total_pages' => ceil($total / $pageSize) ]); } } ``` ```php [AdminController.php] getRequest()->input('page', 1); } /** * 获取每页大小 */ protected function getPageSize(int $default = 10, int $max = 100): int { $size = (int) $this->getRequest()->input('page_size', $default); return min($size, $max); // 限制最大页面大小 } /** * 获取请求实例 */ protected function getRequest(): RequestInterface { return ApplicationContext::getContainer()->get(RequestInterface::class); } /** * 获取排序参数 */ protected function getOrderBy(string $default = 'id'): array { $sort = $this->getRequest()->input('sort', $default); $order = $this->getRequest()->input('order', 'asc'); return [$sort, in_array(strtolower($order), ['asc', 'desc']) ? $order : 'asc']; } } ``` ::: ### 4.4 ResultCode 枚举类 MineAdmin 提供了一套完整的业务状态码枚举系统,用于标准化 API 响应的状态信息。 #### 4.4.1 核心实现 ```php userService->getList(); return $this->success($users, '获取用户列表成功'); } catch (ValidationException $e) { return $this->json(ResultCode::VALIDATION_ERROR, [], $e->getMessage()); } catch (\Exception $e) { return $this->error('系统异常,请稍后重试'); } } } ``` *** ## 5. MineAdmin自定义注解 MineAdmin 提供了三个核心的自定义 Swagger 注解,用于简化 API 文档的编写和维护。所有注解都位于 `Mine\Swagger\Attributes\` 命名空间下。 ### 5.1 注解架构概览 ```plantuml @startuml class SwaggerAnnotation class ResultResponse { +object|string instance +string title +array examples +string description +mixed example +array headers +int response } class PageResponse { +object|string instance +string title +array examples +string description +mixed example +array headers +int response } class FormRequest { +string schema +string title +string description +array required +array properties +array only } SwaggerAnnotation <|-- ResultResponse SwaggerAnnotation <|-- PageResponse SwaggerAnnotation <|-- FormRequest @enduml ``` ### 5.2 ResultResponse 注解 用于定义单个资源或操作的响应结构,自动生成标准的 API 响应文档。 #### 5.2.1 构造函数签名 ```php ResultResponse::__construct( object|string $instance, // 响应数据的类实例或类名 ?string $title = null, // 响应标题 ?array $examples = null, // 多个示例数组 ?string $description = null, // 响应描述 mixed $example = Generator::UNDEFINED, // 单个示例 ?array $headers = null, // 响应头信息 ?int $response = 200 // HTTP状态码 ) ``` #### 5.2.2 参数详解 | 参数 | 类型 | 必填 | 说明 | |-----|------|------|------| | `$instance` | `object\|string` | ✅ | 响应数据的类实例或类名,支持自动解析注解 | | `$title` | `string` | ❌ | 响应的标题,用于文档显示 | | `$examples` | `array` | ❌ | 多个响应示例,键值对形式 | | `$description` | `string` | ❌ | 详细的响应说明 | | `$example` | `mixed` | ❌ | 单个响应示例,JSON字符串或对象 | | `$headers` | `array` | ❌ | 自定义响应头信息 | | `$response` | `int` | ❌ | HTTP状态码,默认200 | #### 5.2.3 实际应用示例 基于用户登录接口的完整示例: ::: code-group ```php [登录控制器] validated(); $tokenData = $this->authService->login($credentials); return $this->success($tokenData, '登录成功'); } } ``` ```php [响应数据模型] ['type' => 'integer', 'description' => '用户ID'], 'username' => ['type' => 'string', 'description' => '用户名'], 'nickname' => ['type' => 'string', 'description' => '昵称'], ] )] public array $user_info; } ``` ::: #### 5.2.4 最佳实践 ::: warning 注意事项 1. **instance 参数**: 推荐使用具体的类实例而非类名,确保注解能正确解析 2. **示例数据**: 提供真实、完整的示例数据,便于前端开发者理解 3. **描述信息**: 详细说明响应的业务含义和使用场景 4. **状态码**: 根据实际业务情况设置合适的 HTTP 状态码 ::: ### 5.3 PageResponse 注解 专门用于分页数据的响应结构注解,自动生成包含分页信息的标准响应文档。 #### 5.3.1 构造函数签名 `PageResponse` 的构造函数与 `ResultResponse` 完全一致,但在语义上专门用于分页响应。 ```php PageResponse::__construct( object|string $instance, // 分页数据项的类实例或类名 ?string $title = null, // 响应标题 ?array $examples = null, // 多个示例数组 ?string $description = null, // 响应描述 mixed $example = Generator::UNDEFINED, // 单个示例 ?array $headers = null, // 响应头信息 ?int $response = 200 // HTTP状态码 ) ``` #### 5.3.2 分页响应结构 ```plantuml @startuml !define LIGHTGREEN #e8f5e8 !define LIGHTYELLOW #fff3e0 node "PageResponse" as A LIGHTGREEN node "Result Structure" as B node "code: 200" as C node "message: string" as D node "data: object" as E LIGHTYELLOW node "list: array" as F node "total: int" as G node "page: int" as H node "page_size: int" as I node "total_pages: int" as J A --> B B --> C B --> D B --> E E --> F E --> G E --> H E --> I E --> J @enduml ``` ### 5.4 FormRequest 注解 专门用于请求参数的结构化文档注解,基于现有的 Schema 类自动生成请求参数文档。 #### 5.4.1 构造函数签名 ```php FormRequest::__construct( ?string $schema = null, // 需要解析的 schema 类名 ?string $title = null, // 表单标题 ?string $description = null, // 表单描述 ?array $required = null, // 必填字段数组 ?array $properties = null, // 额外的属性定义 array $only = [] // 只显示指定的字段 ) ``` #### 5.4.2 参数详解 | 参数 | 类型 | 必填 | 说明 | |-----|------|------|------| | `$schema` | `string` | ❌ | 基础 Schema 类名,用于字段解析 | | `$title` | `string` | ❌ | 请求表单的标题 | | `$description` | `string` | ❌ | 请求表单的详细描述 | | `$required` | `array` | ❌ | 必填字段列表 | | `$properties` | `array` | ❌ | 额外的字段属性定义 | | `$only` | `array` | ❌ | 只显示指定的字段,用于字段过滤 | *** ## 6. 实际应用示例 ### 6.1 完整的CRUD接口示例 以文章管理为例,展示完整的CRUD接口实现: ```php getRequest()->all(); $page = $this->getCurrentPage(); $pageSize = $this->getPageSize(); $result = $this->articleService->paginate($filters, $page, $pageSize); return $this->paginate($result['list'], $result['total'], $page, $pageSize); } /** * 获取单篇文章 */ #[OA\Get( path: "/admin/articles/{id}", summary: "获取文章详情", description: "根据ID获取单篇文章的详细信息" )] #[OA\Parameter(name: "id", description: "文章ID", in: "path", required: true, schema: new OA\Schema(type: "integer"))] #[ResultResponse(instance: new Result(data: new ArticleSchema()), title: "文章详情")] public function show(int $id): Result { $article = $this->articleService->findById($id); return $this->success($article); } /** * 创建文章 */ #[OA\Post( path: "/admin/articles", summary: "创建文章", description: "创建新的文章" )] #[OA\RequestBody(content: new OA\JsonContent(ref: ArticleRequest::class))] #[ResultResponse(instance: new Result(data: new ArticleSchema()), title: "创建成功", response: 201)] public function store(ArticleRequest $request): Result { $data = $request->validated(); $article = $this->articleService->create($data); return $this->success($article, '文章创建成功'); } /** * 更新文章 */ #[OA\Put( path: "/admin/articles/{id}", summary: "更新文章", description: "更新指定文章的信息" )] #[OA\Parameter(name: "id", description: "文章ID", in: "path", required: true, schema: new OA\Schema(type: "integer"))] #[OA\RequestBody(content: new OA\JsonContent(ref: ArticleRequest::class))] #[ResultResponse(instance: new Result(data: new ArticleSchema()), title: "更新成功")] public function update(int $id, ArticleRequest $request): Result { $data = $request->validated(); $article = $this->articleService->update($id, $data); return $this->success($article, '文章更新成功'); } /** * 删除文章 */ #[OA\Delete( path: "/admin/articles/{id}", summary: "删除文章", description: "删除指定的文章" )] #[OA\Parameter(name: "id", description: "文章ID", in: "path", required: true, schema: new OA\Schema(type: "integer"))] #[ResultResponse(instance: new Result(), title: "删除成功")] public function destroy(int $id): Result { $this->articleService->delete($id); return $this->success([], '文章删除成功'); } } ``` ### 6.2 请求验证类示例 ```php 'required|string|max:200', 'content' => 'required|string', 'excerpt' => 'nullable|string|max:500', 'status' => 'required|integer|in:0,1', 'category_id' => 'nullable|integer|exists:categories,id', 'tags' => 'nullable|array', 'tags.*' => 'integer|exists:tags,id', ]; } public function attributes(): array { return [ 'title' => '文章标题', 'content' => '文章内容', 'excerpt' => '文章摘要', 'status' => '发布状态', 'category_id' => '分类ID', 'tags' => '标签列表', ]; } } ``` ### 6.3 数据模型Schema示例 ```php ["type" => "integer", "description" => "分类ID"], "name" => ["type" => "string", "description" => "分类名称"] ] )] public ?array $category; #[OA\Property(property: "tags", title: "文章标签", type: "array", items: new OA\Items(type: "object", properties: [ "id" => ["type" => "integer", "description" => "标签ID"], "name" => ["type" => "string", "description" => "标签名称"] ] ) )] public array $tags; #[OA\Property(property: "created_at", title: "创建时间", type: "string", format: "date-time")] public string $created_at; #[OA\Property(property: "updated_at", title: "更新时间", type: "string", format: "date-time")] public string $updated_at; } ``` *** ## 7. 性能优化与最佳实践 #### 7.1 选择性扫描 ```php // config/autoload/swagger.php // 只扫描必要的目录,避免全项目扫描 'scan' => [ 'paths' => [ Finder::create() ->in([BASE_PATH . '/app/Http', BASE_PATH . '/app/Schema']) ->name('*.php') ->getIterator() ], ], ``` ### 7.2 代码组织最佳实践 #### 7.2.1 目录结构建议 ``` app/ ├── Http/ │ ├── Admin/ │ │ ├── Controller/ # 管理后台控制器 │ │ └── Request/ # 请求验证类 │ └── Common/ │ ├── Result.php # 统一响应结构 │ └── ResultCode.php # 状态码枚举 ├── Schema/ # Swagger Schema 定义 │ ├── UserSchema.php │ └── ArticleSchema.php └── Service/ # 业务逻辑层 ├── UserService.php └── ArticleService.php ``` *** ## 8. 错误处理与调试 ### 8.1 常见错误类型 #### 8.1.1 注解解析错误 ```php // ❌ 错误示例 - 注解语法错误 #[ResultResponse( instance: UserSchema, // 缺少 ::class title = '用户信息' // 使用了 = 而不是 : )] // ✅ 正确示例 #[ResultResponse( instance: UserSchema::class, title: '用户信息' )] ``` #### 8.1.2 循环引用问题 ```php // ❌ 可能导致循环引用 class UserSchema { #[OA\Property(ref: 'GroupSchema')] public GroupSchema $group; } class GroupSchema { #[OA\Property(type: 'array', items: new OA\Items(ref: 'UserSchema'))] public array $users; } // ✅ 使用懒加载避免循环引用 class UserSchema { #[OA\Property(ref: '#/components/schemas/GroupSchema')] public array $group; } ``` ### 8.2 调试技巧 #### 8.2.1 启用详细错误日志 ```php // config/autoload/logger.php return [ 'swagger' => [ 'handler' => [ 'class' => Monolog\Handler\StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/swagger.log', 'level' => Monolog\Logger::DEBUG, ], ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, ], ], ]; ``` #### 8.2.2 文档相关命令 ```shell # 重新生成Swagger文档 php bin/hyperf.php gen:swagger # 根据指定的 model 生成 Swagger Schema php bin/hyperf.php gen:swagger-schema ``` *** ## 10. 常见问题与解决方案 ### 10.1 注解相关问题 #### 10.1.1 注解不生效 **问题描述**: 添加了注解但在Swagger文档中不显示 **可能原因**: 1. 注解语法错误 2. 类未被扫描到 3. 缓存问题 **解决方案**: ```php // 1. 检查注解语法 #[OA\Get(path: "/users", summary: "获取用户列表")] // ✅ 正确 // #[OA\Get(path = "/users", summary = "获取用户列表")] // ❌ 错误语法 // 2. 确保类在扫描范围内 // config/autoload/swagger.php 'scan' => [ 'paths' => [ Finder::create() ->in([BASE_PATH . '/app/Http', BASE_PATH . '/app/Schema']) ->name('*.php') ->getIterator() ], ], // 3. 清除缓存并重新生成文档 php bin/hyperf.php swagger:generate ``` #### 10.1.2 Schema引用问题 **问题描述**: Schema引用无法正确解析 **解决方案**: ```php // ❌ 错误的引用方式 #[OA\Property(ref: 'UserSchema')] // ✅ 正确的引用方式 #[OA\Property(ref: '#/components/schemas/UserSchema')] // 或者使用类引用 #[ResultResponse(instance: UserSchema::class)] ``` ### 10.2 最佳实践总结 #### 10.2.1 开发阶段 1. **渐进式添加**: 先为核心接口添加文档,再逐步完善 2. **模板复用**: 创建通用的注解模板,提高开发效率 3. **即时验证**: 开发过程中及时检查文档生成效果 #### 10.2.2 生产环境 1. **定期更新**: 建立文档更新机制,确保文档与代码同步 2. **访问控制**: 生产环境考虑对Swagger UI的访问控制 #### 10.2.3 团队协作 1. **规范制定**: 建立团队统一的注解编写规范 2. **Code Review**: 将API文档检查纳入代码审查流程 3. **自动化**: 通过CI/CD自动检查文档完整性 *** --- --- url: /v3/front/base/route-menu.md --- # 路由和菜单 MineAdmin 基于 `vue-router` 提供了一套完整的路由系统,支持**静态路由**和**动态路由**两种模式,为企业级权限管理提供强大支撑。 ## 系统架构概览 ```plantuml @startuml !theme plain start :用户登录; if (权限验证) then (通过) :加载静态路由; :请求动态菜单API; note right: /admin/permission/menus :合并路由数据; :生成菜单结构; :渲染界面; stop else (失败) :跳转登录页; stop endif @enduml ``` ## 路由类型选择指南 ### 📊 选择决策矩阵 | 场景 | 静态路由 | 动态路由 | 推荐理由 | |------|---------|----------|---------| | 公共页面(登录、404) | ✅ | ❌ | 无需权限验证,快速加载 | | 基础管理页面 | ❌ | ✅ | 需要权限控制 | | 多租户系统 | ❌ | ✅ | 不同租户菜单结构不同 | | 开发调试页面 | ✅ | ❌ | 仅开发环境使用 | | 高频访问页面 | ✅ | ❌ | 减少网络请求,提升性能 | ## 路由、菜单详细说明 ### 🔹 静态路由 静态路由在前端预先定义,应用启动时立即可用,适用于无需权限控制的页面。 **特点:** * 前端预定义,启动时可用 * 无需网络请求,加载快速 * 适合公共页面和基础功能 **配置位置:** `src/router/static-routes` 目录 **工作流程:** ```plantuml @startuml !theme plain [*] --> 应用启动 应用启动 --> 加载静态路由配置 加载静态路由配置 --> 注册到vue_router : 配置完成 注册到vue_router --> 立即可访问 立即可访问 --> [*] @enduml ``` ::: tip 💡 未来规划 系统考虑引入**文件路由**模式(文件即路由),但目前在 MineAdmin 场景中使用频率不高。 未来可能会根据社区需求添加此功能。 ::: ### 🔹 动态路由 动态路由基于用户权限动态生成,提供精细化的权限控制。 **生成流程:** 1. 用户登录验证通过 2. 请求 `/admin/permission/menus` 接口 3. 服务器返回用户权限菜单数据 4. 前端转换为路由配置 5. 动态注册到 vue-router 6. 生成对应菜单结构 ```plantuml @startuml !theme plain actor 用户 as U participant "前端应用" as F participant "权限API" as A participant "路由系统" as R participant "菜单组件" as M U -> F: 登录成功 F -> A: 请求菜单权限 activate A A --> F: 返回权限数据 deactivate A F -> F: 数据格式转换 F -> R: 动态注册路由 activate R deactivate R F -> M: 生成菜单结构 activate M M --> U: 显示个性化菜单 deactivate M @enduml ``` ### 🔹 菜单系统 菜单是路由的可视化表现,将路由配置转换为用户界面元素。 **菜单与路由关系:** * 一个路由可能对应一个或多个菜单项 * 菜单支持多层级嵌套结构 * 支持图标、徽章、国际化等丰富展示 ## 路由配置详解 ### 基础数据类型 系统在 `#/types/global.d.ts` 中定义了完整的路由类型: ::: details 📋 路由数据类型定义 ```typescript declare namespace MineRoute { interface routeRecord { name?: string // 路由名称,必须唯一 path?: string // 路由路径 redirect?: string // 重定向地址 expand?: boolean // 是否展开子菜单 component?: () => Promise // 异步组件 components?: () => Promise // 命名视图组件 meta?: RouteMeta // 路由元数据 children?: routeRecord[] // 子路由配置 } interface RouteMeta { // 基础信息 title?: string | (() => string) // 页面标题 i18n?: string | (() => string) // 国际化键名 icon?: string // 图标(支持iconify) badge?: () => string | number // 徽章内容 // 显示控制 hidden?: boolean // 是否隐藏菜单 subForceShow?: boolean // 强制显示子菜单 affix?: boolean // 是否固定标签页 // 功能配置 cache?: boolean // 是否缓存页面 copyright?: boolean // 是否显示版权信息 breadcrumbEnable?: boolean // 是否显示面包屑 // 路由类型 type?: 'M' | 'B' | 'I' | 'L' | string // M:菜单 B:按钮 I:iframe L:外链 link?: string // 外链/iframe地址 // 权限控制 auth?: string[] // 权限码数组 role?: string[] // 角色数组 user?: string[] // 用户ID数组 // 系统内部 activeName?: string // 激活菜单名称 breadcrumb?: routeRecord[] // 面包屑路径(自动生成) } } ``` ::: ### 完整配置示例 ```typescript // 标准菜单页面配置 const menuRoute: MineRoute.routeRecord = { name: 'system', path: '/system', redirect: '/system/user', meta: { title: '系统管理', i18n: 'menu.system', icon: 'icon-park-outline:setting-two', type: 'M' }, children: [ { name: 'system-user', path: '/system/user', component: () => import('~/modules/system/views/user/index.vue'), meta: { title: '用户管理', i18n: 'menu.system.user', icon: 'icon-park-outline:user', cache: true, auth: ['system:user:list'] } } ] } ``` ## META 配置详解 ### 🏷️ 基础显示配置 #### title - 页面标题 ```typescript meta: { title: '用户管理', // 直接指定标题 // 或 title: () => `用户管理(${count})` // 动态标题 } ``` **应用场景:** 菜单显示、标签页标题、浏览器标题 #### icon - 图标配置 ```typescript meta: { icon: 'icon-park-outline:user', // Iconify图标 icon: 'mdi:user', // Material Design图标 icon: '/custom-icon.svg' // 自定义SVG图标 } ``` **支持图标库:** Iconify、Material Design Icons、自定义SVG #### badge - 徽章配置 ```typescript meta: { badge: () => store.unreadCount, // 动态徽章 badge: () => 'NEW' // 固定徽章 } ``` ### 🎯 路由类型配置 #### type - 路由类型 ```typescript type RouteType = 'M' | 'B' | 'I' | 'L' // M: 菜单类型(默认) meta: { type: 'M' } // 显示在菜单中,可有子路由 // B: 按钮类型 meta: { type: 'B' } // 不显示菜单,无子路由,权限控制 // I: iframe类型 meta: { type: 'I', link: 'https://admin.example.com' } // L: 外链类型 meta: { type: 'L', link: 'https://docs.example.com' } ``` ### 🔐 权限控制配置 #### 多层级权限控制 ```typescript meta: { // 权限码控制(推荐) auth: ['system:user:list', 'system:user:create'], // 角色控制 role: ['admin', 'manager'], // 用户控制 user: ['1001', '1002'] } ``` **权限验证优先级:** `user > role > auth` ### 🚀 性能配置 #### cache - 页面缓存 ```typescript // 组件中配置 defineOptions({ name: 'SystemUser' // 必须与路由name一致 }) // 路由中启用 meta: { cache: true } ``` #### 懒加载配置 ```typescript // 基础懒加载 component: () => import('~/views/user/index.vue') // 分组懒加载(webpack魔法注释) component: () => import( /* webpackChunkName: "system" */ '~/modules/system/views/user/index.vue' ) ``` ## 实际应用案例 ### 📝 案例1: 标准CRUD模块 ```typescript // 用户管理完整配置 export const userManagementRoutes: MineRoute.routeRecord = { name: 'user-management', path: '/users', redirect: '/users/list', meta: { title: '用户管理', i18n: 'menu.users', icon: 'icon-park-outline:user', type: 'M' }, children: [ // 列表页面 { name: 'user-list', path: '/users/list', component: () => import('~/modules/user/views/list.vue'), meta: { title: '用户列表', cache: true, auth: ['user:list'] } }, // 详情页面(隐藏菜单) { name: 'user-detail', path: '/users/:id', component: () => import('~/modules/user/views/detail.vue'), meta: { title: '用户详情', hidden: true, cache: true, activeName: 'user-list', // 激活父菜单 auth: ['user:view'] } }, // 权限控制按钮 { name: 'user-delete', path: '/users/delete', meta: { type: 'B', // 按钮类型,不显示菜单 auth: ['user:delete'] } } ] } ``` ### 🌐 案例2: 外部集成 ```typescript // iframe和外链配置 export const externalRoutes: MineRoute.routeRecord = { name: 'external', path: '/external', meta: { title: '外部系统', icon: 'icon-park-outline:link' }, children: [ // iframe嵌入 { name: 'external-monitor', path: '/external/monitor', meta: { title: '监控中心', type: 'I', link: 'https://monitor.company.com', auth: ['system:monitor'] } }, // 外链跳转 { name: 'external-docs', path: '/external/docs', meta: { title: '接口文档', type: 'L', link: 'https://api-docs.company.com' } } ] } ``` ### 🏢 案例3: 复杂工作流 ```typescript // 多层级工作流配置 export const workflowRoutes: MineRoute.routeRecord = { name: 'workflow', path: '/workflow', meta: { title: '工作流程', icon: 'icon-park-outline:flow-chart', badge: () => store.pendingTasks }, children: [ { name: 'workflow-pending', path: '/workflow/pending', component: () => import('~/workflow/pending.vue'), meta: { title: '待办事项', affix: true, // 固定标签页 cache: true } }, { name: 'workflow-approval', path: '/workflow/approval', redirect: '/workflow/approval/my', meta: { title: '审批管理', role: ['manager', 'admin'] }, children: [ { name: 'my-approval', path: '/workflow/approval/my', component: () => import('~/workflow/my-approval.vue'), meta: { title: '我的审批', cache: true } } ] } ] } ``` ## 最佳实践 ### 📝 命名规范 **✅ 推荐做法:** ```typescript // 路由名称使用kebab-case name: 'system-user-list' // 路径使用小写+连字符 path: '/system/user-management' // 国际化键名分层级 i18n: 'menu.system.user.list' ``` **❌ 避免的做法:** ```typescript // 避免驼峰命名 name: 'SystemUserList' // 避免特殊字符 path: '/system/user_management' // 避免过深层级 i18n: 'menu.system.management.user.list.page' ``` ### 🏗️ 路由结构设计 **层级控制原则:** * 菜单层级不超过3层 * 每个层级子项数量不超过8个 * 相关功能模块归类组织 **权限粒度设计:** ```typescript // 功能级权限(推荐) auth: ['user:list', 'user:create', 'user:edit'] // 避免过细粒度 auth: ['user:list:name', 'user:list:email'] // ❌ // 避免过粗粒度 auth: ['user:all'] // ❌ ``` ### ⚡ 性能优化策略 #### 路由懒加载优化 ```typescript // 按模块分组加载 const UserRoutes = () => import( /* webpackChunkName: "user-module" */ '~/modules/user/routes' ) // 预加载关键路由 const Dashboard = () => import( /* webpackChunkName: "dashboard" */ /* webpackPreload: true */ '~/views/dashboard.vue' ) ``` #### 菜单渲染优化 ```typescript // 大量菜单项时使用虚拟滚动 meta: { virtualScroll: true // 启用虚拟滚动 } // 延迟加载非关键菜单 meta: { lazyLoad: true } ``` ## 问题排查指南 ### 🐛 常见问题及解决方案 #### 1. 路由无法访问 **症状:** 输入URL后显示404或空白页 **排查步骤:** ```typescript // 1. 检查路由是否正确注册 console.log('已注册路由:', router.getRoutes()) // 2. 验证路由配置 const route = { name: 'user-list', // ✅ 确保name唯一 path: '/users', // ✅ 确保路径正确 component: () => import('~/views/users.vue') // ✅ 组件路径存在 } // 3. 检查权限配置 const hasPermission = await checkAuth(['user:list']) ``` #### 2. 菜单不显示 **可能原因及解决:** ```typescript // 原因1: hidden设置为true meta: { hidden: false } // 确保未隐藏 // 原因2: 权限验证失败 meta: { auth: ['correct:permission'] } // 检查权限码 // 原因3: 路由类型错误 meta: { type: 'M' } // 确保是菜单类型 ``` #### 3. 页面缓存失效 **解决方案:** ```vue ``` ```typescript // 路由配置 meta: { cache: true, // 确保组件name与路由name一致 name: 'UserList' } ``` ### 🔍 调试工具 #### 路由调试助手 ```typescript // 路由调试函数 export const debugRoute = () => { const router = useRouter() const currentRoute = useRoute() console.group('路由调试信息') console.log('当前路由:', currentRoute.name) console.log('路由参数:', currentRoute.params) console.log('查询参数:', currentRoute.query) console.log('路由元数据:', currentRoute.meta) console.log('所有路由:', router.getRoutes()) console.groupEnd() } // 权限调试 export const debugPermission = async (route: RouteRecord) => { const { auth, role, user } = route.meta console.group('权限调试') console.log('所需权限:', auth) console.log('所需角色:', role) console.log('所需用户:', user) if (auth) { console.log('权限验证结果:', await checkAuth(auth)) } console.groupEnd() } ``` #### 菜单验证工具 ```typescript // 菜单结构验证 export const validateMenuStructure = (routes: MineRoute.routeRecord[]) => { const issues = [] const checkRoute = (route: MineRoute.routeRecord, depth = 0) => { // 检查层级深度 if (depth > 3) { issues.push(`路由 ${route.name} 层级过深 (${depth})`) } // 检查必要字段 if (!route.name) { issues.push(`路由缺少name字段: ${route.path}`) } // 递归检查子路由 route.children?.forEach(child => checkRoute(child, depth + 1) ) } routes.forEach(route => checkRoute(route)) return issues } ``` --- --- url: /v3/guide/start/deployment.md --- # 部署 本文将讲述如何在各种环境中部署 MineAdmin 的前后端应用程序,包括开发、测试、生产环境的最佳实践。 ## 部署架构概览 MineAdmin 采用前后端分离架构,基于以下技术栈: * **后端**: PHP 8.1+ + Hyperf 框架 + Swoole 扩展 * **前端**: Vue 3 + TypeScript + Vite * **数据库**: MySQL 5.7+ / PostgreSQL (可选) * **缓存**: Redis 6.0+ * **容器化**: Docker + Docker Compose ```plantuml @startuml MineAdmin 部署架构图 !theme plain skinparam rectangle { BackgroundColor LightBlue BorderColor Black } skinparam component { BackgroundColor LightGreen BorderColor Black } rectangle "负载均衡" as LB { component "Nginx/HAProxy" as nginx } rectangle "Web 层" as Web { component "前端静态资源\n(Vue 3 + Vite)" as frontend } rectangle "API 层" as API { component "MineAdmin 后端\n(PHP 8.1 + Hyperf)" as backend1 component "MineAdmin 后端\n(PHP 8.1 + Hyperf)" as backend2 } rectangle "数据层" as Data { component "MySQL 5.7+" as mysql component "Redis 6.0+" as redis } LB --> Web LB --> API API --> Data @enduml ``` ## 环境准备 ### PHP 扩展要求 基于 [`mineadmin/Dockerfile`](https://github.com/mineadmin/MineAdmin/blob/master/Dockerfile) 的配置: **必需扩展:** * cURL >= 7.68 * Fileinfo * OpenSSL >= 1.1 * PDO * Redis >= 5.3 * JSON * Tokenizer * SimpleXML * XMLWriter **可选扩展:** * PDO\_MYSQL (MySQL 支持) * PDO\_PGSQL (PostgreSQL 支持) * Swoole >= 5.1 (高性能模式) * Swow >= 1.5 * XlsWriter (Excel 文件支持) **PHP 配置优化:** ```ini # /etc/php/8.1/php.ini 或相应版本路径 upload_max_filesize = 128M post_max_size = 128M memory_limit = 1G max_execution_time = 300 max_input_vars = 3000 date.timezone = Asia/Shanghai ``` ## 后端部署 ### 1. 环境配置 #### 创建环境配置文件 复制并配置环境文件,参考 [`mineadmin/.env.example`](https://github.com/mineadmin/MineAdmin/blob/master/.env.example): ```shell cp .env.example .env ``` **开发环境配置 (.env)**: ```bash APP_NAME=MineAdmin APP_ENV=dev APP_DEBUG=true APP_URL=http://127.0.0.1:9501 # 数据库配置 DB_DRIVER=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=mineadmin DB_USERNAME=root DB_PASSWORD=your_password DB_CHARSET=utf8mb4 DB_COLLATION=utf8mb4_unicode_ci DB_PREFIX= # Redis 配置 REDIS_HOST=127.0.0.1 REDIS_AUTH= REDIS_PORT=6379 REDIS_DB=0 # JWT 密钥 (请生成新的密钥) JWT_SECRET=your_jwt_secret_key_here ``` **生产环境配置**: ```bash APP_NAME=MineAdmin APP_ENV=prod APP_DEBUG=false APP_URL=https://your-domain.com # 数据库配置 (使用内网 IP) DB_DRIVER=mysql DB_HOST=10.0.0.10 DB_PORT=3306 DB_DATABASE=mineadmin DB_USERNAME=mineadmin DB_PASSWORD=strong_password_here DB_CHARSET=utf8mb4 DB_COLLATION=utf8mb4_unicode_ci DB_PREFIX= # Redis 配置 (使用内网 IP,启用密码) REDIS_HOST=10.0.0.11 REDIS_AUTH=redis_password_here REDIS_PORT=6379 REDIS_DB=0 # JWT 密钥 (64 字符强密钥) JWT_SECRET=generated_64_character_jwt_secret_key_here ``` #### 生成 JWT 密钥 ```shell # 生成安全的 JWT 密钥 php -r "echo base64_encode(random_bytes(64)) . PHP_EOL;" ``` ### 2. 数据库初始化 #### 数据库迁移 执行数据库迁移,基于 [`mineadmin/databases/migrations/`](https://github.com/mineadmin/MineAdmin/tree/master/databases/migrations) 目录中的迁移文件: ```shell # 运行数据库迁移 php bin/hyperf.php migrate # 查看迁移状态 php bin/hyperf.php migrate:status ``` **主要数据表包括**: * `user` - 用户表 * `menu` - 菜单表 * `role` - 角色表 * `rules` - 权限规则表 * `attachment` - 附件表 * `user_login_log` - 用户登录日志 * `user_operation_log` - 用户操作日志 #### 数据填充(可选) ```shell # 执行数据填充 php bin/hyperf.php db:seed ``` ### 3. 直接服务器部署 #### 使用 Supervisord 进程管理 创建 Supervisor 配置文件 `/etc/supervisor/conf.d/mineadmin.conf`: ```ini [program:mineadmin] command=php /var/www/mineadmin/bin/hyperf.php start directory=/var/www/mineadmin autostart=true autorestart=true startretries=3 user=www-data redirect_stderr=true stdout_logfile=/var/log/mineadmin.log stdout_logfile_maxbytes=50MB stdout_logfile_backups=10 ``` 启动服务: ```shell # 重新加载配置 sudo supervisorctl reread sudo supervisorctl update # 启动 MineAdmin sudo supervisorctl start mineadmin # 查看状态 sudo supervisorctl status mineadmin ``` #### 使用 Systemd 服务管理 创建系统服务文件 `/etc/systemd/system/mineadmin.service`: ```ini [Unit] Description=MineAdmin Hyperf Service After=network.target mysql.service redis.service [Service] Type=forking User=www-data Group=www-data WorkingDirectory=/var/www/mineadmin ExecStart=/usr/bin/php /var/www/mineadmin/bin/hyperf.php start -d ExecStop=/bin/kill -TERM $MAINPID ExecReload=/bin/kill -USR1 $MAINPID Restart=always RestartSec=5 StandardOutput=journal StandardError=journal SyslogIdentifier=mineadmin [Install] WantedBy=multi-user.target ``` 管理服务: ```shell # 启用并启动服务 sudo systemctl enable mineadmin sudo systemctl start mineadmin # 查看服务状态 sudo systemctl status mineadmin # 查看日志 sudo journalctl -u mineadmin -f ``` ### 4. 容器化部署 (推荐) #### 单容器部署 基于项目根目录的 [`Dockerfile`](https://github.com/mineadmin/MineAdmin/blob/master/Dockerfile): ```shell # 构建镜像 docker build -t mineadmin:latest . # 运行容器 (开发环境) docker run -d \ --name mineadmin \ -p 9501:9501 \ -p 9503:9503 \ -v $(pwd)/.env:/opt/www/.env \ -v $(pwd)/storage:/opt/www/storage \ mineadmin:latest # 查看容器状态 docker ps -a docker logs mineadmin ``` #### Docker Compose 部署(完整环境) 使用项目提供的 [`docker-compose.yml`](https://github.com/mineadmin/MineAdmin/blob/master/docker-compose.yml) 配置: **开发环境 docker-compose.yml**: ```yaml name: mineadmin-dev volumes: mine_redis_data: mine_mysql_data: mine_uploads: networks: mineadmin: driver: bridge services: redis: image: redis:7.2-alpine container_name: mineadmin-redis ports: - "6379:6379" volumes: - mine_redis_data:/data command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-} environment: - TZ=Asia/Shanghai networks: - mineadmin healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 3 deploy: resources: limits: memory: 512M mysql: image: mysql:8.0 container_name: mineadmin-mysql volumes: - mine_mysql_data:/var/lib/mysql - ./docker/mysql/conf.d:/etc/mysql/conf.d ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-root} MYSQL_DATABASE: ${DB_DATABASE:-mineadmin} MYSQL_USER: ${DB_USERNAME:-mineadmin} MYSQL_PASSWORD: ${DB_PASSWORD:-root} MYSQL_CHARACTER_SET_SERVER: utf8mb4 MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci TZ: Asia/Shanghai networks: - mineadmin healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 deploy: resources: limits: memory: 1G app: build: context: . dockerfile: Dockerfile args: - timezone=Asia/Shanghai container_name: mineadmin-app volumes: - ./:/opt/www - mine_uploads:/opt/www/storage/uploads ports: - "9501:9501" - "9503:9503" environment: - TZ=Asia/Shanghai - APP_ENV=dev depends_on: mysql: condition: service_healthy redis: condition: service_healthy networks: - mineadmin healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9501/"] interval: 30s timeout: 10s retries: 3 ``` **生产环境部署**: ```shell # 创建生产环境配置 cp .env.example .env.prod # 启动服务 docker-compose --env-file .env.prod up -d # 查看服务状态 docker-compose ps # 查看日志 docker-compose logs -f app ``` #### Kubernetes 部署 **ConfigMap 配置**: ```yaml apiVersion: v1 kind: ConfigMap metadata: name: mineadmin-config namespace: mineadmin data: .env: | APP_NAME=MineAdmin APP_ENV=prod APP_DEBUG=false APP_URL=https://admin.yourdomain.com DB_DRIVER=mysql DB_HOST=mysql-service DB_PORT=3306 DB_DATABASE=mineadmin DB_USERNAME=mineadmin DB_PASSWORD=your_secure_password DB_CHARSET=utf8mb4 DB_COLLATION=utf8mb4_unicode_ci DB_PREFIX= REDIS_HOST=redis-service REDIS_AUTH=your_redis_password REDIS_PORT=6379 REDIS_DB=0 JWT_SECRET=your_64_character_jwt_secret ``` **Deployment 配置**: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: mineadmin-deployment namespace: mineadmin spec: replicas: 3 selector: matchLabels: app: mineadmin template: metadata: labels: app: mineadmin spec: containers: - name: mineadmin image: mineadmin:latest ports: - containerPort: 9501 - containerPort: 9503 env: - name: APP_ENV value: "prod" - name: TZ value: "Asia/Shanghai" volumeMounts: - name: config-volume mountPath: /opt/www/.env subPath: .env - name: storage-volume mountPath: /opt/www/storage resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" livenessProbe: httpGet: path: / port: 9501 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: / port: 9501 initialDelaySeconds: 10 periodSeconds: 5 volumes: - name: config-volume configMap: name: mineadmin-config - name: storage-volume persistentVolumeClaim: claimName: mineadmin-storage-pvc ``` **Service 配置**: ```yaml apiVersion: v1 kind: Service metadata: name: mineadmin-service namespace: mineadmin spec: selector: app: mineadmin ports: - name: http protocol: TCP port: 80 targetPort: 9501 type: ClusterIP ``` **Ingress 配置**: ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: mineadmin-ingress namespace: mineadmin annotations: nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-body-size: "128m" cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: tls: - hosts: - admin.yourdomain.com secretName: mineadmin-tls rules: - host: admin.yourdomain.com http: paths: - path: / pathType: Prefix backend: service: name: mineadmin-service port: number: 80 ``` ### 5. 反向代理与负载均衡 无论何时都不建议将应用程序直接暴露在公网环境,最好是通过反向代理进行流量转发 基于 [`mineadmin/config/autoload/server.php`](https://github.com/mineadmin/MineAdmin/blob/master/config/autoload/server.php) 的服务器配置,应用默认监听 9501 端口。 #### Nginx 反向代理 **生产环境 Nginx 配置** (`/etc/nginx/sites-available/mineadmin`): ```nginx # 上游服务器配置 (负载均衡) upstream mineadmin_backend { # 权重轮询 server 127.0.0.1:9501 weight=1 max_fails=3 fail_timeout=30s; server 127.0.0.1:9502 weight=1 max_fails=3 fail_timeout=30s backup; # 会话保持 ip_hash; # 健康检查 (需要 nginx_upstream_check_module) # check interval=3000 rise=2 fall=5 timeout=1000 type=http; } # HTTPS 重定向 server { listen 80; server_name admin.yourdomain.com; return 301 https://$server_name$request_uri; } # 主要配置 server { listen 443 ssl http2; server_name admin.yourdomain.com; # SSL 配置 ssl_certificate /etc/ssl/certs/mineadmin.crt; ssl_certificate_key /etc/ssl/private/mineadmin.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 5m; # 安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # 日志配置 access_log /var/log/nginx/mineadmin.access.log; error_log /var/log/nginx/mineadmin.error.log warn; # 客户端配置 client_max_body_size 128M; client_body_timeout 60s; client_header_timeout 60s; # Gzip 压缩 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # API 代理 location / { proxy_pass http://mineadmin_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; # 超时配置 proxy_connect_timeout 30s; proxy_send_timeout 60s; proxy_read_timeout 60s; # 缓冲区配置 proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; # 错误处理 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; proxy_next_upstream_tries 3; proxy_next_upstream_timeout 30s; } # 健康检查端点 location /health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } } ``` **启用站点**: ```shell # 创建软链接 sudo ln -s /etc/nginx/sites-available/mineadmin /etc/nginx/sites-enabled/ # 测试配置 sudo nginx -t # 重载配置 sudo systemctl reload nginx ``` #### HAProxy 负载均衡 **HAProxy 配置** (`/etc/haproxy/haproxy.cfg`): ```haproxy global daemon maxconn 4096 log stdout local0 defaults mode http log global option httplog option dontlognull option redispatch retries 3 timeout connect 5000ms timeout client 50000ms timeout server 50000ms # 前端配置 frontend mineadmin_frontend bind *:80 bind *:443 ssl crt /etc/ssl/certs/mineadmin.pem redirect scheme https if !{ ssl_fc } # 安全头 http-response set-header X-Frame-Options SAMEORIGIN http-response set-header X-XSS-Protection "1; mode=block" http-response set-header X-Content-Type-Options nosniff default_backend mineadmin_backend # 后端配置 backend mineadmin_backend balance roundrobin option httpchk GET /health http-check expect status 200 server app1 127.0.0.1:9501 check inter 2000ms rise 2 fall 3 server app2 127.0.0.1:9502 check inter 2000ms rise 2 fall 3 backup # 统计页面 stats enable stats uri /haproxy/stats stats refresh 30s stats hide-version stats auth admin:your_password_here ``` ### 6. 性能优化 #### Swoole 性能调优 基于 [`mineadmin/config/autoload/server.php`](https://github.com/mineadmin/MineAdmin/blob/master/config/autoload/server.php) 配置调整: ```php SWOOLE_PROCESS, 'servers' => [ [ 'name' => 'http', 'type' => Server::SERVER_HTTP, 'host' => '0.0.0.0', 'port' => 9501, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], ], ], ], 'settings' => [ // 性能优化配置 Constant::OPTION_WORKER_NUM => swoole_cpu_num() * 2, // 工作进程数 Constant::OPTION_MAX_COROUTINE => 100000, // 最大协程数 Constant::OPTION_OPEN_TCP_NODELAY => true, // TCP_NODELAY Constant::OPTION_MAX_REQUEST => 50000, // 工作进程最大请求数 Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024, // Socket 缓冲区大小 Constant::OPTION_PACKAGE_MAX_LENGTH => 10 * 1024 * 1024, // 最大包长度 // HTTP2 支持 Constant::OPTION_OPEN_HTTP2_PROTOCOL => true, // 进程管理 Constant::OPTION_PID_FILE => BASE_PATH . '/runtime/hyperf.pid', Constant::OPTION_LOG_FILE => BASE_PATH . '/runtime/logs/swoole.log', Constant::OPTION_LOG_LEVEL => SWOOLE_LOG_INFO, // 内存优化 Constant::OPTION_BACKLOG => 128, Constant::OPTION_HEARTBEAT_CHECK_INTERVAL => 30, Constant::OPTION_HEARTBEAT_IDLE_TIME => 60, ], ]; ``` #### 数据库连接池优化 基于 [`mineadmin/config/autoload/databases.php`](https://github.com/mineadmin/MineAdmin/blob/master/config/autoload/databases.php) 配置: ```php [ 'driver' => env('DB_DRIVER', 'mysql'), 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', 3306), 'database' => env('DB_DATABASE', 'mineadmin'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD'), 'charset' => env('DB_CHARSET', 'utf8mb4'), 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'prefix' => env('DB_PREFIX', ''), // 连接池配置 (生产环境优化) 'pool' => [ 'min_connections' => 10, // 最小连接数 'max_connections' => 100, // 最大连接数 'connect_timeout' => 10.0, // 连接超时时间 'wait_timeout' => 3.0, // 等待超时时间 'heartbeat' => -1, // 心跳间隔 'max_idle_time' => 60, // 最大空闲时间 ], // 查询缓存配置 'cache' => [ 'handler' => RedisHandler::class, 'cache_key' => 'MineAdmin:%s:m:%s:%s:%s', 'prefix' => 'model-cache', 'ttl' => 86400 * 7, // 缓存时间 'empty_model_ttl' => 60, // 空模型缓存时间 'load_script' => true, 'use_default_value' => false, ], ], ]; ``` #### Redis 缓存优化 ```redis # /etc/redis/redis.conf 生产环境配置 # 基础配置 bind 127.0.0.1 port 6379 timeout 0 keepalive 300 requirepass your_redis_password # 内存优化 maxmemory 2gb maxmemory-policy allkeys-lru maxmemory-samples 10 # 持久化配置 save 900 1 save 300 10 save 60 10000 rdbcompression yes rdbchecksum yes dbfilename dump.rdb dir /var/lib/redis # AOF 配置 appendonly yes appendfilename "appendonly.aof" appendfsync everysec no-appendfsync-on-rewrite no auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 客户端连接 maxclients 10000 tcp-keepalive 60 tcp-backlog 511 # 日志配置 loglevel notice logfile /var/log/redis/redis-server.log ``` ### 7. 安全配置 #### 生产环境安全配置 基于 [`mineadmin/config/config.php`](https://github.com/mineadmin/MineAdmin/blob/master/config/config.php) 的调试配置: ```php env('APP_NAME', 'MineAdmin'), // 生产环境必须关闭调试模式 'scan_cacheable' => true, // 启用扫描缓存 'debug' => false, // 关闭调试模式 // 日志级别配置 (生产环境) StdoutLoggerInterface::class => [ 'log_level' => [ LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::EMERGENCY, LogLevel::ERROR, LogLevel::WARNING, // LogLevel::INFO, // 生产环境可以关闭 INFO 日志 // LogLevel::DEBUG, // 生产环境必须关闭 DEBUG 日志 ], ], ]; ``` #### 防火墙配置 **UFW 防火墙规则**: ```shell # 重置防火墙规则 sudo ufw --force reset # 默认拒绝所有连接 sudo ufw default deny incoming sudo ufw default allow outgoing # 允许 SSH (修改为非默认端口) sudo ufw allow 22022/tcp # 允许 HTTP/HTTPS sudo ufw allow 80/tcp sudo ufw allow 443/tcp # 允许内网数据库连接 sudo ufw allow from 10.0.0.0/8 to any port 3306 sudo ufw allow from 10.0.0.0/8 to any port 6379 # 启用防火墙 sudo ufw enable # 查看状态 sudo ufw status verbose ``` **iptables 防火墙规则**: ```shell #!/bin/bash # /etc/iptables/rules.sh # 清空现有规则 iptables -F iptables -X iptables -t nat -F iptables -t nat -X # 设置默认策略 iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT ACCEPT # 允许本地回环 iptables -A INPUT -i lo -j ACCEPT # 允许已建立的连接 iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # 允许 SSH (非默认端口) iptables -A INPUT -p tcp --dport 22022 -j ACCEPT # 允许 HTTP/HTTPS iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT # 防 DDoS 攻击 iptables -A INPUT -p tcp --dport 80 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT # 保存规则 iptables-save > /etc/iptables/rules.v4 ``` ### 8. 监控与日志 #### 系统监控配置 **Prometheus + Grafana 监控配置**: ```yaml # docker-compose.monitoring.yml version: '3.8' services: prometheus: image: prom/prometheus:latest container_name: prometheus ports: - "9090:9090" volumes: - ./monitoring/prometheus:/etc/prometheus - prometheus_data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.console.libraries=/etc/prometheus/console_libraries' - '--web.console.templates=/etc/prometheus/consoles' grafana: image: grafana/grafana:latest container_name: grafana ports: - "3000:3000" volumes: - grafana_data:/var/lib/grafana - ./monitoring/grafana:/etc/grafana/provisioning environment: - GF_SECURITY_ADMIN_PASSWORD=your_password node-exporter: image: prom/node-exporter:latest container_name: node-exporter ports: - "9100:9100" volumes: prometheus_data: grafana_data: ``` #### 日志管理 **结构化日志配置**: ```php [ 'handlers' => [ [ 'class' => RotatingFileHandler::class, 'constructor' => [ 'filename' => BASE_PATH . '/runtime/logs/mineadmin.log', 'maxFiles' => 14, 'level' => Logger::DEBUG, ], 'formatter' => [ 'class' => JsonFormatter::class, 'constructor' => [], ], ], [ 'class' => StreamHandler::class, 'constructor' => [ 'stream' => 'php://stdout', 'level' => env('APP_DEBUG') ? Logger::DEBUG : Logger::INFO, ], 'formatter' => [ 'class' => JsonFormatter::class, 'constructor' => [], ], ], ], ], ]; ``` **ELK Stack 日志收集**: ```yaml # docker-compose.logging.yml version: '3.8' services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 container_name: elasticsearch environment: - discovery.type=single-node - "ES_JAVA_OPTS=-Xms1g -Xmx1g" ports: - "9200:9200" volumes: - elasticsearch_data:/usr/share/elasticsearch/data logstash: image: docker.elastic.co/logstash/logstash:7.17.0 container_name: logstash volumes: - ./logging/logstash/config:/usr/share/logstash/config - ./logging/logstash/pipeline:/usr/share/logstash/pipeline ports: - "5044:5044" - "5000:5000/tcp" - "5000:5000/udp" - "9600:9600" depends_on: - elasticsearch kibana: image: docker.elastic.co/kibana/kibana:7.17.0 container_name: kibana ports: - "5601:5601" environment: ELASTICSEARCH_URL: http://elasticsearch:9200 ELASTICSEARCH_HOSTS: http://elasticsearch:9200 depends_on: - elasticsearch volumes: elasticsearch_data: ``` ### 9. 调试模式与环境切换 在 [`mineadmin/config/config.php`](https://github.com/mineadmin/MineAdmin/blob/master/config/config.php) 配置文件中,调试选项决定了错误信息的展示级别,默认遵循环境变量 `APP_DEBUG` 的值。 **环境配置说明**: | 环境 | APP\_ENV | APP\_DEBUG | 说明 | |------|---------|-----------|------| | 开发 | dev | true | 显示详细错误信息,启用热重载 | | 测试 | test | false | 模拟生产环境,记录详细日志 | | 生产 | prod | false | 隐藏错误信息,优化性能 | ::: danger 安全警告 在生产环境中,`APP_DEBUG` 必须设置为 `false`。如果设置为 `true`,将可能暴露敏感信息(数据库配置、API 密钥等)给最终用户,存在严重安全风险。 ::: **环境切换命令**: ```shell # 切换到生产环境 export APP_ENV=prod export APP_DEBUG=false # 重启服务 supervisorctl restart mineadmin ``` ## 前端部署 MineAdmin 前端基于 Vue 3 + TypeScript + Vite 构建,支持现代化的 SPA 单页应用部署。 ### 1. 构建准备 #### 环境要求 基于 [`mineadmin/web/package.json`](https://github.com/mineadmin/MineAdmin/blob/master/web/package.json) 配置: * **Node.js**: >= 18.16.0 * **包管理器**: pnpm (推荐) >= 8.0.0 * **构建工具**: Vite >= 4.0.0 #### 依赖安装 ```shell # 进入前端目录 cd web # 安装依赖 (推荐使用 pnpm) pnpm install # 或使用 npm npm install ``` ### 2. 构建配置 #### 环境变量配置 创建不同环境的配置文件: **开发环境** (`.env.development`): ```bash # API 基础地址 VITE_API_BASE_URL=http://127.0.0.1:9501 VITE_API_PREFIX=/api # 应用配置 VITE_APP_NAME=MineAdmin VITE_APP_VERSION=3.0.0 # 开发配置 VITE_DEV_MOCK=false VITE_DEV_PROXY=true ``` **生产环境** (`.env.production`): ```bash # API 基础地址 VITE_API_BASE_URL=https://api.yourdomain.com VITE_API_PREFIX=/api # 应用配置 VITE_APP_NAME=MineAdmin VITE_APP_VERSION=3.0.0 # 生产配置 VITE_BUILD_COMPRESS=gzip VITE_BUILD_ANALYZE=false VITE_BUILD_DROP_CONSOLE=true ``` #### Vite 构建配置优化 ```typescript // vite.config.ts 生产环境优化配置 import { defineConfig } from 'vite' import { resolve } from 'path' export default defineConfig(({ command, mode }) => { const isProduction = mode === 'production' return { base: '/', // 根路径部署 // 构建配置 build: { outDir: 'dist', assetsDir: 'assets', sourcemap: !isProduction, // 生产环境禁用 sourcemap minify: 'terser', // 代码分割 rollupOptions: { output: { // 手动分包 manualChunks: { 'vue-vendor': ['vue', 'vue-router', 'pinia'], 'element-plus': ['element-plus'], 'lodash': ['lodash-es'], 'utils': ['axios', 'dayjs'] }, // 静态资源命名 chunkFileNames: 'js/[name]-[hash].js', entryFileNames: 'js/[name]-[hash].js', assetFileNames: '[ext]/[name]-[hash].[ext]' } }, // 压缩配置 terserOptions: { compress: { drop_console: isProduction, // 生产环境移除 console drop_debugger: isProduction } }, // 大文件警告阈值 chunkSizeWarningLimit: 1500 }, // 开发服务器配置 server: { port: 3000, host: '0.0.0.0', proxy: { '/api': { target: 'http://127.0.0.1:9501', changeOrigin: true, secure: false } } } } }) ``` ### 3. 直接服务器部署 #### 构建静态资源 ```shell # 开发环境构建 pnpm build --mode development # 生产环境构建 pnpm build --mode production # 构建并分析包大小 pnpm build --mode production && pnpm analyze ``` #### Nginx 静态文件配置 **完整的 Nginx 前端配置** (`/etc/nginx/sites-available/mineadmin-frontend`): ```nginx server { listen 80; server_name www.yourdomain.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name www.yourdomain.com; # SSL 配置 ssl_certificate /etc/ssl/certs/yourdomain.crt; ssl_certificate_key /etc/ssl/private/yourdomain.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 5m; # 安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:;" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # 文档根目录 root /var/www/mineadmin/web/dist; index index.html; # 日志配置 access_log /var/log/nginx/mineadmin-frontend.access.log; error_log /var/log/nginx/mineadmin-frontend.error.log warn; # Gzip 压缩 gzip on; gzip_vary on; gzip_min_length 1024; gzip_comp_level 6; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; # Brotli 压缩 (如果已安装) # brotli on; # brotli_comp_level 6; # brotli_types text/plain text/css application/json application/javascript text/xml application/xml; # 静态资源缓存策略 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|avif)$ { expires 1y; add_header Cache-Control "public, no-transform, immutable"; add_header Vary "Accept-Encoding"; access_log off; # 预压缩文件支持 location ~* \.(js|css)$ { gzip_static on; } } # 字体文件缓存 location ~* \.(woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, no-transform, immutable"; add_header Access-Control-Allow-Origin "*"; access_log off; } # HTML 文件不缓存 location ~* \.html$ { expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate"; add_header Pragma "no-cache"; } # API 代理到后端服务 location ^~ /api/ { proxy_pass http://127.0.0.1:9501; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; # CORS 支持 add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always; if ($request_method = 'OPTIONS') { add_header Access-Control-Allow-Origin "*"; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; add_header Access-Control-Max-Age 1728000; add_header Content-Type 'text/plain; charset=utf-8'; add_header Content-Length 0; return 204; } } # 单页应用路由支持 location / { try_files $uri $uri/ /index.html; # 预加载关键资源 add_header Link "; rel=preload; as=style" always; add_header Link "; rel=preload; as=script" always; } # 健康检查 location = /health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } # 安全配置 location ~ /\. { deny all; access_log off; log_not_found off; } location ~* \.(env|log|ini)$ { deny all; access_log off; log_not_found off; } } ``` ### 4. 容器化部署(推荐) #### 多阶段构建 Dockerfile 基于项目提供的 [`mineadmin/web/Dockerfile`](https://github.com/mineadmin/MineAdmin/blob/master/web/Dockerfile) 优化: ```dockerfile # 第一阶段:构建应用 FROM node:20-alpine3.20 AS builder # 安装构建依赖 RUN apk add --no-cache git python3 make g++ # 设置工作目录 WORKDIR /app # 复制 package 文件 COPY package.json pnpm-lock.yaml ./ # 安装 pnpm 和依赖 RUN npm install -g pnpm@latest && \ pnpm config set registry https://registry.npmmirror.com && \ pnpm install --frozen-lockfile # 复制源代码 COPY . . # 构建参数 ARG NODE_ENV=production ARG API_BASE_URL=https://api.yourdomain.com # 设置环境变量 ENV NODE_ENV=$NODE_ENV ENV VITE_API_BASE_URL=$API_BASE_URL # 构建应用 RUN pnpm build --mode $NODE_ENV # 第二阶段:生产运行环境 FROM nginx:1.25-alpine AS production # 安装必要工具 RUN apk add --no-cache tzdata && \ ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ echo "Asia/Shanghai" > /etc/timezone # 创建 nginx 用户和目录 RUN addgroup -g 1001 -S nginx-app && \ adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx-app -g nginx-app nginx-app # 复制构建产物 COPY --from=builder /app/dist /usr/share/nginx/html # 复制 Nginx 配置 COPY --from=builder /app/docker/nginx.conf /etc/nginx/nginx.conf COPY --from=builder /app/docker/default.conf /etc/nginx/conf.d/default.conf # 设置权限 RUN chown -R nginx-app:nginx-app /usr/share/nginx/html && \ chown -R nginx-app:nginx-app /var/cache/nginx && \ chown -R nginx-app:nginx-app /var/log/nginx && \ chmod -R 755 /usr/share/nginx/html # 健康检查 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost/health || exit 1 # 暴露端口 EXPOSE 80 # 使用非 root 用户运行 USER nginx-app # 启动 Nginx CMD ["nginx", "-g", "daemon off;"] ``` #### Docker Compose 前端部署 ```yaml # docker-compose.frontend.yml version: '3.8' services: frontend: build: context: ./web dockerfile: Dockerfile args: - NODE_ENV=production - API_BASE_URL=https://api.yourdomain.com container_name: mineadmin-frontend ports: - "80:80" - "443:443" volumes: - ./docker/nginx/ssl:/etc/nginx/ssl:ro - ./docker/nginx/conf.d:/etc/nginx/conf.d:ro - frontend_logs:/var/log/nginx environment: - TZ=Asia/Shanghai restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "traefik.enable=true" - "traefik.http.routers.frontend.rule=Host(`www.yourdomain.com`)" - "traefik.http.routers.frontend.tls=true" - "traefik.http.routers.frontend.tls.certresolver=letsencrypt" deploy: resources: limits: memory: 256M cpus: '0.5' reservations: memory: 128M cpus: '0.25' volumes: frontend_logs: ``` **部署命令**: ```shell # 构建并启动 docker-compose -f docker-compose.frontend.yml up -d --build # 查看日志 docker-compose -f docker-compose.frontend.yml logs -f # 更新部署 docker-compose -f docker-compose.frontend.yml down docker-compose -f docker-compose.frontend.yml up -d --build ``` ### 5. CDN 与静态资源优化 #### CDN 配置 ```typescript // vite.config.ts CDN 配置 export default defineConfig({ build: { rollupOptions: { external: ['vue', 'vue-router', 'element-plus'], output: { globals: { 'vue': 'Vue', 'vue-router': 'VueRouter', 'element-plus': 'ElementPlus' } } } }, plugins: [ // CDN 插件配置 cdn({ modules: [ { name: 'vue', global: 'Vue', spare: 'https://unpkg.com/vue@3/dist/vue.global.prod.js' }, { name: 'vue-router', global: 'VueRouter', spare: 'https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js' }, { name: 'element-plus', global: 'ElementPlus', spare: 'https://unpkg.com/element-plus/dist/index.full.min.js', css: 'https://unpkg.com/element-plus/dist/index.css' } ] }) ] }) ``` #### 阿里云 OSS 静态资源上传 ```shell #!/bin/bash # 自动化部署脚本 # 构建项目 pnpm build --mode production # 上传到 OSS ossutil cp -r dist/ oss://your-bucket/mineadmin/ --update # 刷新 CDN 缓存 aliyun cdn RefreshObjectCaches --ObjectPath "https://cdn.yourdomain.com/*" echo "部署完成!" ``` ### 6. 性能优化 #### Webpack Bundle 分析 ```shell # 安装分析工具 pnpm add -D rollup-plugin-visualizer # 生成分析报告 pnpm build --mode production ``` #### 预加载优化 ```html MineAdmin
``` ### 7. 故障排除 #### 常见部署问题 **1. 路由 404 问题** ```nginx # Nginx 配置确保包含 location / { try_files $uri $uri/ /index.html; } ``` **2. API 跨域问题** ```typescript // vite.config.ts 开发环境代理 export default defineConfig({ server: { proxy: { '/api': { target: process.env.VITE_API_BASE_URL, changeOrigin: true, secure: false } } } }) ``` **3. 静态资源加载失败** ```nginx # 检查 Nginx 文件权限 location ~* \.(js|css|png|jpg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public"; # 确保文件存在 try_files $uri =404; } ``` **4. 内存不足构建失败** ```shell # 增加 Node.js 内存限制 export NODE_OPTIONS="--max-old-space-size=4096" pnpm build ``` #### 日志调试 ```shell # 查看 Nginx 错误日志 tail -f /var/log/nginx/error.log # 查看 Docker 容器日志 docker logs mineadmin-frontend -f # 浏览器网络调试 # F12 -> Network -> 查看失败的请求 ``` **配置文件**: * \[x] `.env` 文件已正确配置 * \[x] 数据库连接参数已测试 * \[x] Redis 连接参数已测试 * \[x] JWT 密钥已生成并配置 **安全设置**: * \[x] `APP_DEBUG` 已设置为 `false`(生产环境) * \[x] 数据库密码符合强密码策略 * \[x] Redis 已设置认证密码 * \[x] 防火墙规则已配置 **性能基准测试**: ```bash # 使用 Apache Bench 进行压力测试 ab -n 1000 -c 10 http://localhost:9501/api/system/info # 使用 wrk 进行更详细的性能测试 wrk -t4 -c100 -d30s --timeout 10s http://localhost:9501/api/system/info ``` ## 常见问题排查 ### 后端常见问题 \*\* 内存不足导致进程崩溃\*\* ```bash # 症状:日志中出现 "Cannot allocate memory" # 解决方案:调整 PHP 内存限制 echo "memory_limit = 1G" >> /etc/php/8.1/php.ini # 或调整 Swoole 工作进程数 # config/autoload/server.php Constant::OPTION_WORKER_NUM => min(swoole_cpu_num(), 4) ``` **数据库连接超时** ```bash # 症状:大量 "Connection timed out" 错误 # 解决方案:调整数据库连接池配置 # config/autoload/databases.php 'pool' => [ 'min_connections' => 1, 'max_connections' => 20, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'max_idle_time' => 60, ] ``` **Redis 连接失败** ```bash # 症状:Redis 相关操作报错 # 检查 Redis 服务状态 systemctl status redis-server # 检查 Redis 配置 redis-cli ping # 检查防火墙 sudo ufw status | grep 6379 ``` \*\* 文件上传权限问题\*\* ```bash # 症状:文件上传失败,403 错误 # 解决方案:设置正确的目录权限 chown -R www-data:www-data storage/ chmod -R 755 storage/ chmod -R 777 storage/uploads/ ``` ### 前端常见问题 \*\* 打包构建失败\*\* ```bash # 症状:pnpm build 失败,内存不足 # 解决方案:增加 Node.js 内存限制 export NODE_OPTIONS="--max-old-space-size=8192" pnpm build # 或使用增量构建 pnpm build --mode development ``` **2. 路由 404 问题** ```nginx # 症状:刷新页面出现 404 # 解决方案:确保 Nginx 配置包含 SPA 路由支持 location / { try_files $uri $uri/ /index.html; } ``` **3. API 跨域问题** ```bash # 症状:浏览器控制台出现 CORS 错误 # 解决方案:检查 Nginx 代理配置 location ^~ /api/ { proxy_pass http://127.0.0.1:9501; # 添加 CORS 头 add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always; } ``` **4. 静态资源加载失败** ```nginx # 症状:CSS/JS 文件 404 # 检查 Nginx 配置中的资源路径 location ~* \.(js|css|png|jpg|gif|ico|svg)$ { root /var/www/mineadmin/web/dist; expires 1y; try_files $uri =404; } ``` ### 容器部署问题 **1. Docker 构建失败** ```bash # 症状:Docker build 过程中网络超时 # 解决方案:使用国内镜像源 docker build --build-arg NPM_REGISTRY=https://registry.npmmirror.com . # 或使用多阶段构建缓存 docker build --target builder -t mineadmin:builder . docker build --cache-from mineadmin:builder -t mineadmin:latest . ``` **2. 容器启动失败** ```bash # 症状:容器启动后立即退出 # 查看容器日志 docker logs mineadmin-app # 进入容器调试 docker exec -it mineadmin-app /bin/sh # 检查容器健康状态 docker inspect --format='{{.State.Health.Status}}' mineadmin-app ``` **3. 容器间网络通信问题** ```bash # 症状:应用无法连接到数据库容器 # 检查 Docker 网络 docker network ls docker network inspect mineadmin_default # 使用服务名进行连接 # .env 文件中使用容器服务名 DB_HOST=mysql REDIS_HOST=redis ``` ### 性能优化问题 **1. 响应时间过慢** ```bash # 检查系统负载 top iostat -x 1 # 检查 Swoole 进程状态 ps aux | grep hyperf # 分析慢查询 # 在 MySQL 配置中启用慢查询日志 slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 0.5 ``` **2. 内存使用率过高** ```bash # 检查内存使用 free -h ps aux --sort=-%mem | head # 调整 Swoole 配置 # config/autoload/server.php Constant::OPTION_MAX_REQUEST => 10000 # 减少最大请求数 Constant::OPTION_WORKER_NUM => 2 # 减少工作进程数 ``` ## 🔗 相关资源 * **源码仓库**: [github.com/mineadmin/mineadmin](https://github.com/mineadmin/mineadmin) * **Hyperf 官方文档**: [hyperf.wiki](https://hyperf.wiki) * **技术支持**: [GitHub Issues](https://github.com/mineadmin/mineadmin/issues) --- --- url: /backend/frameworks/hyperf/3.1/base/error-handler.md --- # 错误处理 ## 目录 * [默认异常处理机制](#默认异常处理机制) * [异常处理流程](#异常处理流程) * [核心异常处理器](#核心异常处理器) * [业务异常处理](#业务异常处理) * [自定义异常处理器](#自定义异常处理器) * [调试模式特性](#调试模式特性) * [最佳实践](#最佳实践) * [常见问题](#常见问题) ## 默认异常处理机制 ::: tip 前置知识 要理解 MineAdmin 的异常处理,首先需要你对 [Hyperf](https://hyperf.io) 的错误处理有一定的了解。 本文不讲述基础性的说明,请先了解 Hyperf 异常处理的基本概念。 ::: MineAdmin 基于 Hyperf 框架实现了完善的异常处理机制。在 `config/autoload/exceptions.php` 中配置了多个异常处理器,采用责任链模式按顺序处理不同类型的异常。 ## 异常处理流程 ```plantuml @startuml start :异常抛出; :BusinessExceptionHandler; if (是否为BusinessException?) then (是) :返回业务错误响应; stop else (否) :UnauthorizedExceptionHandler; if (是否为UnauthorizedException?) then (是) :返回401未授权响应; stop else (否) :ValidationExceptionHandler; if (是否为ValidationException?) then (是) :返回参数验证错误响应; stop else (否) :JwtExceptionHandler; if (是否为JWT异常?) then (是) :返回JWT错误响应; stop else (否) :AppExceptionHandler; :返回通用错误响应; stop endif endif endif endif @enduml ``` ## 核心异常处理器 ### 异常处理器配置 ::: code-group ```php [exceptions.php] [ 'http' => [ // 处理业务异常 - 优先级最高 BusinessExceptionHandler::class, // 处理未授权异常 UnauthorizedExceptionHandler::class, // 处理验证器异常 ValidationExceptionHandler::class, // 处理JWT异常 JwtExceptionHandler::class, // 处理应用异常 - 最后兜底处理 AppExceptionHandler::class, ], ], ]; ``` ::: ::: warning 注意事项 * 异常处理器的顺序很重要,越靠前的处理器优先级越高 * `AppExceptionHandler` 作为兜底处理器,应该始终放在最后 * 不要随意修改处理器的顺序,除非你完全理解其影响 ::: ### 基础异常处理器类 所有的异常处理器都继承自 `AbstractHandler`,提供了统一的处理逻辑: ::: code-group ```php [AbstractHandler.php] report($throwable); return value(function (ResponsePlusInterface $responsePlus) use ($throwable) { // 如果是 debug 模式,自动处理跨域并记录详细错误信息 if ($this->isDebug()) { $responsePlus ->setHeader('Access-Control-Allow-Origin', '*') ->setHeader('Access-Control-Allow-Credentials', 'true') ->setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS') ->setHeader('Access-Control-Allow-Headers', 'DNT,Keep-Alive,User-Agent,Cache-Control,Content-Type,Authorization'); // 记录详细的异常信息到上下文 Context::set(self::class . '.throwable', [ 'message' => $throwable->getMessage(), 'file' => $throwable->getFile(), 'line' => $throwable->getLine(), 'trace' => $throwable->getTrace(), ]); } return $responsePlus; }, $this->handlerRequestId( $this->handlerResult( $response, $this->handleResponse($throwable) ) )); } /** * 上报异常日志,包括控制台输出和文件记录 */ public function report(\Throwable $throwable) { // 如果是debug模式,在控制台打印格式化的错误信息 if ($this->isDebug()) { $this->container->get(StdoutLoggerInterface::class)->error( $this->container->get(FormatterInterface::class)->format($throwable) ); } // 记录异常到错误日志文件 $this->loggerFactory ->get('error') ->error($throwable->getMessage(), ['exception' => $throwable]); } /** * 将结果包装到响应体中 */ protected function handlerResult(ResponsePlusInterface $responsePlus, Result $result): ResponsePlusInterface { $responsePlus->setHeader('Content-Type', 'application/json; charset=utf-8'); // 调试模式下返回详细的异常信息 if ($this->isDebug()) { $result = $result->toArray(); $result['throwable'] = Context::get(self::class . '.throwable'); return $responsePlus ->setBody(new SwooleStream(Json::encode($result))); } return $responsePlus ->setBody(new SwooleStream(Json::encode($result))); } /** * 为响应添加请求ID头部,便于问题追踪 */ private function handlerRequestId(ResponsePlusInterface $responsePlus): ResponsePlusInterface { return $responsePlus->setHeader('Request-Id', UuidRequestIdProcessor::getUuid()); } } ``` ```php [AppExceptionHandler.php] stopPropagation(); return new Result( ResultCode::FAIL, $throwable->getMessage() ?: '系统异常,请稍后重试' ); } /** * 该处理器处理所有类型的异常 */ public function isValid(\Throwable $throwable): bool { return true; } } ``` ::: ## Result 和 ResultCode 核心类 ### Result 统一响应类 `Result` 类是 MineAdmin 中所有接口响应的标准格式,实现了 `Arrayable` 接口并支持 OpenAPI 文档注解: ::: code-group ```php [Result.php] message === null) { $this->message = ResultCode::getMessage($this->code->value); } } public function toArray(): array { return [ 'code' => $this->code->value, 'message' => $this->message, 'data' => $this->data, ]; } } ``` ::: #### 使用示例 ::: code-group ```php [成功响应] // 成功响应 - 使用默认成功码 $result = new Result(); // 成功响应 - 带数据 $result = new Result(data: ['id' => 1, 'name' => '张三']); // 成功响应 - 自定义消息 $result = new Result(message: '操作成功完成'); ``` ```php [失败响应] // 失败响应 - 使用默认失败码 $result = new Result(ResultCode::FAIL, '操作失败'); // 失败响应 - 使用具体状态码 $result = new Result(ResultCode::UNAUTHORIZED, '用户未登录'); // 失败响应 - 带错误数据 $result = new Result( ResultCode::UNPROCESSABLE_ENTITY, '参数验证失败', ['errors' => ['email' => ['邮箱格式错误']]] ); ``` ::: ### ResultCode 状态码枚举 `ResultCode` 是一个基于 PHP 8.1 枚举的状态码定义,使用 Hyperf 的 Constants 特性支持国际化消息: ::: code-group ```php [ResultCode.php] '操作成功', 'fail' => '操作失败', 'unauthorized' => '用户未授权', 'forbidden' => '禁止访问', 'not_found' => '资源不存在', 'method_not_allowed' => '方法不允许', 'not_acceptable' => '不可接受的请求', 'conflict' => '参数验证失败', 'disabled' => '资源已禁用', ]; ``` ```php [获取国际化消息] // 通过 ResultCode 获取消息 $message = ResultCode::getMessage(ResultCode::SUCCESS->value); // 输出:'操作成功' // Result 构造时自动获取消息 $result = new Result(ResultCode::NOT_FOUND); // $result->message 自动为 '资源不存在' ``` ::: ## 业务异常处理 ### BusinessException 业务异常类 推荐使用 `BusinessException` 来抛出业务相关的异常,而不是直接使用 `throw new Exception`: ::: code-group ```php [BusinessException.php] response = new Result($code, $message, $data); parent::__construct($message ?? ResultCode::getMessage($code->value)); } /** * 获取结构化的响应对象 */ public function getResponse(): Result { return $this->response; } } ``` ```php [BusinessExceptionHandler.php] stopPropagation(); if ($throwable instanceof BusinessException) { return $throwable->getResponse(); } // 兜底处理 return new Result( ResultCode::FAIL, $throwable->getMessage() ); } /** * 只处理 BusinessException 类型的异常 */ public function isValid(\Throwable $throwable): bool { return $throwable instanceof BusinessException; } } ``` ::: ### 实际使用示例 ::: code-group ```php [UserService.php] '用户名']) ); } // 查找用户 $user = $this->findUserByUsername($username); if (!$user) { throw new BusinessException( ResultCode::NOT_FOUND, trans('auth.user_not_found') ); } // 验证密码 if (!$this->verifyPassword($password, $user['password'])) { throw new BusinessException( ResultCode::UNAUTHORIZED, trans('auth.invalid_credentials') ); } // 检查用户状态 if ($user['status'] !== 'active') { throw new BusinessException( ResultCode::DISABLED, trans('auth.user_disabled'), ['reason' => $user['disable_reason'] ?? '未知原因'] ); } return $user; } /** * 更新用户资料 */ public function updateProfile(int $userId, array $data): bool { $user = $this->findUserById($userId); if (!$user) { throw new BusinessException( ResultCode::NOT_FOUND, '用户不存在' ); } // 检查邮箱是否已被使用 if (isset($data['email']) && $this->isEmailExists($data['email'], $userId)) { throw new BusinessException( ResultCode::UNPROCESSABLE_ENTITY, '邮箱已被其他用户使用' ); } return $this->updateUser($userId, $data); } } ``` ```php [UserController.php] request->input('username'); $password = $this->request->input('password'); // 如果 UserService 抛出 BusinessException, // 会被自动捕获并返回相应的错误响应 $user = $this->userService->login($username, $password); // 生成令牌等后续逻辑... $token = $this->generateToken($user); return $this->success([ 'token' => $token, 'user' => $user ]); } /** * 更新用户资料 */ public function updateProfile(): Result { $userId = $this->getUserId(); $data = $this->request->all(); $this->userService->updateProfile($userId, $data); return $this->success(message: '资料更新成功'); } } ``` ::: ## 自定义异常处理器 当需要处理特定类型的异常时,可以创建自定义的异常处理器。 ### 创建自定义异常类 ::: code-group ```php [PaymentException.php] paymentMethod; } public function getTransactionId(): string { return $this->transactionId; } } ``` ::: ### 创建自定义异常处理器 ::: code-group ```php [PaymentExceptionHandler.php] stopPropagation(); if ($throwable instanceof PaymentException) { // 记录支付异常的详细信息 $this->loggerFactory ->get('payment') ->error('支付异常', [ 'payment_method' => $throwable->getPaymentMethod(), 'transaction_id' => $throwable->getTransactionId(), 'message' => $throwable->getMessage(), 'trace' => $throwable->getTraceAsString(), ]); return new Result( ResultCode::FAIL, '支付处理失败,请稍后重试或联系客服', [ 'transaction_id' => $throwable->getTransactionId(), 'support_contact' => config('payment.support_contact'), ] ); } return new Result( ResultCode::FAIL, $throwable->getMessage() ); } public function isValid(\Throwable $throwable): bool { return $throwable instanceof PaymentException; } } ``` ::: ### 注册自定义异常处理器 在 `config/autoload/exceptions.php` 中注册你的自定义处理器: ::: code-group ```php [exceptions.php] [ 'http' => [ // 业务异常处理器 BusinessExceptionHandler::class, // 自定义的支付异常处理器 PaymentExceptionHandler::class, // 其他处理器... UnauthorizedExceptionHandler::class, ValidationExceptionHandler::class, JwtExceptionHandler::class, AppExceptionHandler::class, ], ], ]; ``` ::: ### 使用自定义异常 ::: code-group ```php [PaymentService.php] callPaymentGateway($method, $amount, $transactionId); if (!$result['success']) { throw new PaymentException( paymentMethod: $method, transactionId: $transactionId, message: "支付失败:{$result['error_msg']}" ); } return true; } catch (\Throwable $e) { // 将所有支付相关的异常包装为 PaymentException throw new PaymentException( paymentMethod: $method, transactionId: $transactionId, message: "支付处理异常:{$e->getMessage()}", previous: $e ); } } } ``` ::: ## 调试模式特性 ### 开启调试模式 在 `.env` 文件中设置: ```env APP_DEBUG=true ``` ### 调试模式功能 当 `APP_DEBUG=true` 时,异常处理器会提供以下额外功能: 1. **详细的异常信息**:响应中包含异常的文件、行号和调用栈 2. **控制台输出**:异常信息会输出到命令行控制台 3. **CORS 头部**:自动添加跨域请求头,方便前端调试 4. **Request-Id**:每个响应都包含唯一的请求ID,便于日志追踪 ### 调试响应格式 调试模式下的响应示例: ::: code-group ```json [调试模式响应] { "code": 500, "message": "用户不存在", "data": null, "throwable": { "message": "用户不存在", "file": "/app/Service/UserService.php", "line": 45, "trace": [ { "file": "/app/Controller/UserController.php", "line": 23, "function": "findUser", "class": "App\\Service\\UserService", "type": "->" } ] } } ``` ```json [生产模式响应] { "code": 500, "message": "用户不存在", "data": null } ``` ::: ::: warning 安全提醒 生产环境中务必关闭调试模式(`APP_DEBUG=false`),避免泄露敏感的系统信息。 ::: ## 最佳实践 ### 1. 异常分层处理 ```php // 推荐:使用语义化的异常类型 throw new BusinessException(ResultCode::NOT_FOUND, trans('user.not_found')); // 不推荐:直接抛出通用异常 throw new \Exception('用户不存在'); ``` ### 2. 合理使用 ResultCode ```php // 推荐:使用语义化的结果码 throw new BusinessException( ResultCode::UNPROCESSABLE_ENTITY, trans('validation.email_format') ); // 不推荐:使用通用的失败码 throw new BusinessException( ResultCode::FAIL, '邮箱格式错误' ); ``` ### 3. 异常信息国际化 ```php // 推荐:使用多语言支持 throw new BusinessException( ResultCode::NOT_FOUND, trans('auth.user_not_found') ); // 可接受:在特定情况下使用固定文本 throw new BusinessException( ResultCode::FAIL, '系统维护中,请稍后重试' ); ``` ### 4. 记录详细的上下文信息 ```php public function processOrder(int $orderId): bool { try { // 业务逻辑... return true; } catch (\Throwable $e) { // 记录详细的上下文信息 logger('order')->error('订单处理失败', [ 'order_id' => $orderId, 'user_id' => $this->getCurrentUserId(), 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw new BusinessException( ResultCode::FAIL, '订单处理失败,请稍后重试' ); } } ``` ## 常见问题 ### Q1: 异常没有被正确捕获? **可能原因:** * 异常处理器的 `isValid` 方法返回了 `false` * 异常处理器没有正确注册 * 异常处理器的顺序不正确 **解决方案:** 1. 检查异常处理器的 `isValid` 方法逻辑 2. 确认异常处理器已在 `exceptions.php` 中注册 3. 调整异常处理器的顺序,确保更具体的处理器在前面 ### Q2: 调试信息在生产环境中被泄露? **解决方案:** * 确保生产环境的 `.env` 文件中设置了 `APP_DEBUG=false` * 使用环境变量或配置管理工具确保不同环境的配置隔离 ### Q3: 异常处理器执行顺序混乱? **解决方案:** * 在 `exceptions.php` 中,将更具体的异常处理器放在前面 * 确保 `AppExceptionHandler` 始终在最后作为兜底处理器 ### Q4: 同一个异常被多个处理器记录导致日志重复? **解决方案:** * 在具体的异常处理器中调用 `$this->stopPropagation()` 阻止异常继续传播 * 只在最终处理器中进行日志记录 ### Q5: 如何处理异步任务中的异常? **解决方案:** ```php // 在异步任务中使用 try-catch 包装业务逻辑 use Hyperf\AsyncQueue\Job; class SendEmailJob extends Job { public function handle() { try { // 发送邮件的业务逻辑 $this->sendEmail(); } catch (BusinessException $e) { // 记录业务异常 logger('job')->warning('邮件发送业务异常', [ 'job_id' => $this->getJobId(), 'message' => $e->getMessage(), ]); } catch (\Throwable $e) { // 记录系统异常并重新抛出,让队列系统处理重试 logger('job')->error('邮件发送系统异常', [ 'job_id' => $this->getJobId(), 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw $e; } } } ``` 通过以上的异常处理机制,MineAdmin 提供了完善、可扩展的错误处理能力,帮助开发者构建稳定可靠的应用系统。 --- --- url: /backend/frameworks/hyperf/3.2/base/error-handler.md --- # 错误处理 ## 目录 * [默认异常处理机制](#默认异常处理机制) * [异常处理流程](#异常处理流程) * [核心异常处理器](#核心异常处理器) * [业务异常处理](#业务异常处理) * [自定义异常处理器](#自定义异常处理器) * [调试模式特性](#调试模式特性) * [最佳实践](#最佳实践) * [常见问题](#常见问题) ## 默认异常处理机制 ::: tip 前置知识 要理解 MineAdmin 的异常处理,首先需要你对 [Hyperf](https://hyperf.io) 的错误处理有一定的了解。 本文不讲述基础性的说明,请先了解 Hyperf 异常处理的基本概念。 ::: MineAdmin 基于 Hyperf 框架实现了完善的异常处理机制。在 `config/autoload/exceptions.php` 中配置了多个异常处理器,采用责任链模式按顺序处理不同类型的异常。 ## 异常处理流程 ```plantuml @startuml start :异常抛出; :BusinessExceptionHandler; if (是否为BusinessException?) then (是) :返回业务错误响应; stop else (否) :UnauthorizedExceptionHandler; if (是否为UnauthorizedException?) then (是) :返回401未授权响应; stop else (否) :ValidationExceptionHandler; if (是否为ValidationException?) then (是) :返回参数验证错误响应; stop else (否) :JwtExceptionHandler; if (是否为JWT异常?) then (是) :返回JWT错误响应; stop else (否) :AppExceptionHandler; :返回通用错误响应; stop endif endif endif endif @enduml ``` ## 核心异常处理器 ### 异常处理器配置 ::: code-group ```php [exceptions.php] [ 'http' => [ // 处理业务异常 - 优先级最高 BusinessExceptionHandler::class, // 处理未授权异常 UnauthorizedExceptionHandler::class, // 处理验证器异常 ValidationExceptionHandler::class, // 处理JWT异常 JwtExceptionHandler::class, // 处理应用异常 - 最后兜底处理 AppExceptionHandler::class, ], ], ]; ``` ::: ::: warning 注意事项 * 异常处理器的顺序很重要,越靠前的处理器优先级越高 * `AppExceptionHandler` 作为兜底处理器,应该始终放在最后 * 不要随意修改处理器的顺序,除非你完全理解其影响 ::: ### 基础异常处理器类 所有的异常处理器都继承自 `AbstractHandler`,提供了统一的处理逻辑: ::: code-group ```php [AbstractHandler.php] report($throwable); return value(function (ResponsePlusInterface $responsePlus) use ($throwable) { // 如果是 debug 模式,自动处理跨域并记录详细错误信息 if ($this->isDebug()) { $responsePlus ->setHeader('Access-Control-Allow-Origin', '*') ->setHeader('Access-Control-Allow-Credentials', 'true') ->setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS') ->setHeader('Access-Control-Allow-Headers', 'DNT,Keep-Alive,User-Agent,Cache-Control,Content-Type,Authorization'); // 记录详细的异常信息到上下文 Context::set(self::class . '.throwable', [ 'message' => $throwable->getMessage(), 'file' => $throwable->getFile(), 'line' => $throwable->getLine(), 'trace' => $throwable->getTrace(), ]); } return $responsePlus; }, $this->handlerRequestId( $this->handlerResult( $response, $this->handleResponse($throwable) ) )); } /** * 上报异常日志,包括控制台输出和文件记录 */ public function report(\Throwable $throwable) { // 如果是debug模式,在控制台打印格式化的错误信息 if ($this->isDebug()) { $this->container->get(StdoutLoggerInterface::class)->error( $this->container->get(FormatterInterface::class)->format($throwable) ); } // 记录异常到错误日志文件 $this->loggerFactory ->get('error') ->error($throwable->getMessage(), ['exception' => $throwable]); } /** * 将结果包装到响应体中 */ protected function handlerResult(ResponsePlusInterface $responsePlus, Result $result): ResponsePlusInterface { $responsePlus->setHeader('Content-Type', 'application/json; charset=utf-8'); // 调试模式下返回详细的异常信息 if ($this->isDebug()) { $result = $result->toArray(); $result['throwable'] = Context::get(self::class . '.throwable'); return $responsePlus ->setBody(new SwooleStream(Json::encode($result))); } return $responsePlus ->setBody(new SwooleStream(Json::encode($result))); } /** * 为响应添加请求ID头部,便于问题追踪 */ private function handlerRequestId(ResponsePlusInterface $responsePlus): ResponsePlusInterface { return $responsePlus->setHeader('Request-Id', UuidRequestIdProcessor::getUuid()); } } ``` ```php [AppExceptionHandler.php] stopPropagation(); return new Result( ResultCode::FAIL, $throwable->getMessage() ?: '系统异常,请稍后重试' ); } /** * 该处理器处理所有类型的异常 */ public function isValid(\Throwable $throwable): bool { return true; } } ``` ::: ## Result 和 ResultCode 核心类 ### Result 统一响应类 `Result` 类是 MineAdmin 中所有接口响应的标准格式,实现了 `Arrayable` 接口并支持 OpenAPI 文档注解: ::: code-group ```php [Result.php] message === null) { $this->message = ResultCode::getMessage($this->code->value); } } public function toArray(): array { return [ 'code' => $this->code->value, 'message' => $this->message, 'data' => $this->data, ]; } } ``` ::: #### 使用示例 ::: code-group ```php [成功响应] // 成功响应 - 使用默认成功码 $result = new Result(); // 成功响应 - 带数据 $result = new Result(data: ['id' => 1, 'name' => '张三']); // 成功响应 - 自定义消息 $result = new Result(message: '操作成功完成'); ``` ```php [失败响应] // 失败响应 - 使用默认失败码 $result = new Result(ResultCode::FAIL, '操作失败'); // 失败响应 - 使用具体状态码 $result = new Result(ResultCode::UNAUTHORIZED, '用户未登录'); // 失败响应 - 带错误数据 $result = new Result( ResultCode::UNPROCESSABLE_ENTITY, '参数验证失败', ['errors' => ['email' => ['邮箱格式错误']]] ); ``` ::: ### ResultCode 状态码枚举 `ResultCode` 是一个基于 PHP 8.1 枚举的状态码定义,使用 Hyperf 的 Constants 特性支持国际化消息: ::: code-group ```php [ResultCode.php] '操作成功', 'fail' => '操作失败', 'unauthorized' => '用户未授权', 'forbidden' => '禁止访问', 'not_found' => '资源不存在', 'method_not_allowed' => '方法不允许', 'not_acceptable' => '不可接受的请求', 'conflict' => '参数验证失败', 'disabled' => '资源已禁用', ]; ``` ```php [获取国际化消息] // 通过 ResultCode 获取消息 $message = ResultCode::getMessage(ResultCode::SUCCESS->value); // 输出:'操作成功' // Result 构造时自动获取消息 $result = new Result(ResultCode::NOT_FOUND); // $result->message 自动为 '资源不存在' ``` ::: ## 业务异常处理 ### BusinessException 业务异常类 推荐使用 `BusinessException` 来抛出业务相关的异常,而不是直接使用 `throw new Exception`: ::: code-group ```php [BusinessException.php] response = new Result($code, $message, $data); parent::__construct($message ?? ResultCode::getMessage($code->value)); } /** * 获取结构化的响应对象 */ public function getResponse(): Result { return $this->response; } } ``` ```php [BusinessExceptionHandler.php] stopPropagation(); if ($throwable instanceof BusinessException) { return $throwable->getResponse(); } // 兜底处理 return new Result( ResultCode::FAIL, $throwable->getMessage() ); } /** * 只处理 BusinessException 类型的异常 */ public function isValid(\Throwable $throwable): bool { return $throwable instanceof BusinessException; } } ``` ::: ### 实际使用示例 ::: code-group ```php [UserService.php] '用户名']) ); } // 查找用户 $user = $this->findUserByUsername($username); if (!$user) { throw new BusinessException( ResultCode::NOT_FOUND, trans('auth.user_not_found') ); } // 验证密码 if (!$this->verifyPassword($password, $user['password'])) { throw new BusinessException( ResultCode::UNAUTHORIZED, trans('auth.invalid_credentials') ); } // 检查用户状态 if ($user['status'] !== 'active') { throw new BusinessException( ResultCode::DISABLED, trans('auth.user_disabled'), ['reason' => $user['disable_reason'] ?? '未知原因'] ); } return $user; } /** * 更新用户资料 */ public function updateProfile(int $userId, array $data): bool { $user = $this->findUserById($userId); if (!$user) { throw new BusinessException( ResultCode::NOT_FOUND, '用户不存在' ); } // 检查邮箱是否已被使用 if (isset($data['email']) && $this->isEmailExists($data['email'], $userId)) { throw new BusinessException( ResultCode::UNPROCESSABLE_ENTITY, '邮箱已被其他用户使用' ); } return $this->updateUser($userId, $data); } } ``` ```php [UserController.php] request->input('username'); $password = $this->request->input('password'); // 如果 UserService 抛出 BusinessException, // 会被自动捕获并返回相应的错误响应 $user = $this->userService->login($username, $password); // 生成令牌等后续逻辑... $token = $this->generateToken($user); return $this->success([ 'token' => $token, 'user' => $user ]); } /** * 更新用户资料 */ public function updateProfile(): Result { $userId = $this->getUserId(); $data = $this->request->all(); $this->userService->updateProfile($userId, $data); return $this->success(message: '资料更新成功'); } } ``` ::: ## 自定义异常处理器 当需要处理特定类型的异常时,可以创建自定义的异常处理器。 ### 创建自定义异常类 ::: code-group ```php [PaymentException.php] paymentMethod; } public function getTransactionId(): string { return $this->transactionId; } } ``` ::: ### 创建自定义异常处理器 ::: code-group ```php [PaymentExceptionHandler.php] stopPropagation(); if ($throwable instanceof PaymentException) { // 记录支付异常的详细信息 $this->loggerFactory ->get('payment') ->error('支付异常', [ 'payment_method' => $throwable->getPaymentMethod(), 'transaction_id' => $throwable->getTransactionId(), 'message' => $throwable->getMessage(), 'trace' => $throwable->getTraceAsString(), ]); return new Result( ResultCode::FAIL, '支付处理失败,请稍后重试或联系客服', [ 'transaction_id' => $throwable->getTransactionId(), 'support_contact' => config('payment.support_contact'), ] ); } return new Result( ResultCode::FAIL, $throwable->getMessage() ); } public function isValid(\Throwable $throwable): bool { return $throwable instanceof PaymentException; } } ``` ::: ### 注册自定义异常处理器 在 `config/autoload/exceptions.php` 中注册你的自定义处理器: ::: code-group ```php [exceptions.php] [ 'http' => [ // 业务异常处理器 BusinessExceptionHandler::class, // 自定义的支付异常处理器 PaymentExceptionHandler::class, // 其他处理器... UnauthorizedExceptionHandler::class, ValidationExceptionHandler::class, JwtExceptionHandler::class, AppExceptionHandler::class, ], ], ]; ``` ::: ### 使用自定义异常 ::: code-group ```php [PaymentService.php] callPaymentGateway($method, $amount, $transactionId); if (!$result['success']) { throw new PaymentException( paymentMethod: $method, transactionId: $transactionId, message: "支付失败:{$result['error_msg']}" ); } return true; } catch (\Throwable $e) { // 将所有支付相关的异常包装为 PaymentException throw new PaymentException( paymentMethod: $method, transactionId: $transactionId, message: "支付处理异常:{$e->getMessage()}", previous: $e ); } } } ``` ::: ## 调试模式特性 ### 开启调试模式 在 `.env` 文件中设置: ```env APP_DEBUG=true ``` ### 调试模式功能 当 `APP_DEBUG=true` 时,异常处理器会提供以下额外功能: 1. **详细的异常信息**:响应中包含异常的文件、行号和调用栈 2. **控制台输出**:异常信息会输出到命令行控制台 3. **CORS 头部**:自动添加跨域请求头,方便前端调试 4. **Request-Id**:每个响应都包含唯一的请求ID,便于日志追踪 ### 调试响应格式 调试模式下的响应示例: ::: code-group ```json [调试模式响应] { "code": 500, "message": "用户不存在", "data": null, "throwable": { "message": "用户不存在", "file": "/app/Service/UserService.php", "line": 45, "trace": [ { "file": "/app/Controller/UserController.php", "line": 23, "function": "findUser", "class": "App\\Service\\UserService", "type": "->" } ] } } ``` ```json [生产模式响应] { "code": 500, "message": "用户不存在", "data": null } ``` ::: ::: warning 安全提醒 生产环境中务必关闭调试模式(`APP_DEBUG=false`),避免泄露敏感的系统信息。 ::: ## 最佳实践 ### 1. 异常分层处理 ```php // 推荐:使用语义化的异常类型 throw new BusinessException(ResultCode::NOT_FOUND, trans('user.not_found')); // 不推荐:直接抛出通用异常 throw new \Exception('用户不存在'); ``` ### 2. 合理使用 ResultCode ```php // 推荐:使用语义化的结果码 throw new BusinessException( ResultCode::UNPROCESSABLE_ENTITY, trans('validation.email_format') ); // 不推荐:使用通用的失败码 throw new BusinessException( ResultCode::FAIL, '邮箱格式错误' ); ``` ### 3. 异常信息国际化 ```php // 推荐:使用多语言支持 throw new BusinessException( ResultCode::NOT_FOUND, trans('auth.user_not_found') ); // 可接受:在特定情况下使用固定文本 throw new BusinessException( ResultCode::FAIL, '系统维护中,请稍后重试' ); ``` ### 4. 记录详细的上下文信息 ```php public function processOrder(int $orderId): bool { try { // 业务逻辑... return true; } catch (\Throwable $e) { // 记录详细的上下文信息 logger('order')->error('订单处理失败', [ 'order_id' => $orderId, 'user_id' => $this->getCurrentUserId(), 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw new BusinessException( ResultCode::FAIL, '订单处理失败,请稍后重试' ); } } ``` ## 常见问题 ### Q1: 异常没有被正确捕获? **可能原因:** * 异常处理器的 `isValid` 方法返回了 `false` * 异常处理器没有正确注册 * 异常处理器的顺序不正确 **解决方案:** 1. 检查异常处理器的 `isValid` 方法逻辑 2. 确认异常处理器已在 `exceptions.php` 中注册 3. 调整异常处理器的顺序,确保更具体的处理器在前面 ### Q2: 调试信息在生产环境中被泄露? **解决方案:** * 确保生产环境的 `.env` 文件中设置了 `APP_DEBUG=false` * 使用环境变量或配置管理工具确保不同环境的配置隔离 ### Q3: 异常处理器执行顺序混乱? **解决方案:** * 在 `exceptions.php` 中,将更具体的异常处理器放在前面 * 确保 `AppExceptionHandler` 始终在最后作为兜底处理器 ### Q4: 同一个异常被多个处理器记录导致日志重复? **解决方案:** * 在具体的异常处理器中调用 `$this->stopPropagation()` 阻止异常继续传播 * 只在最终处理器中进行日志记录 ### Q5: 如何处理异步任务中的异常? **解决方案:** ```php // 在异步任务中使用 try-catch 包装业务逻辑 use Hyperf\AsyncQueue\Job; class SendEmailJob extends Job { public function handle() { try { // 发送邮件的业务逻辑 $this->sendEmail(); } catch (BusinessException $e) { // 记录业务异常 logger('job')->warning('邮件发送业务异常', [ 'job_id' => $this->getJobId(), 'message' => $e->getMessage(), ]); } catch (\Throwable $e) { // 记录系统异常并重新抛出,让队列系统处理重试 logger('job')->error('邮件发送系统异常', [ 'job_id' => $this->getJobId(), 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw $e; } } } ``` 通过以上的异常处理机制,MineAdmin 提供了完善、可扩展的错误处理能力,帮助开发者构建稳定可靠的应用系统。 --- --- url: /v3/backend/base/error-handler.md --- # 错误处理 本文已迁移到 [Hyperf 错误处理](/backend/frameworks/hyperf/3.2/base/error-handler)。 跨框架一致的响应格式,请阅读 [响应结构契约](/v3/backend/contracts/response)。 --- --- url: /backend/frameworks/hyperf/3.1/base/structure.md --- # 项目目录结构 MineAdmin 采用现代化的分层架构设计,提供清晰的代码组织结构和最佳实践。本文档将详细介绍项目的目录结构、设计理念以及开发规范。 ## 概述 MineAdmin 的项目结构参考了 [Laravel](https://laravel.com/) 框架的设计理念,同时结合了现代化的分层架构模式。如果你熟悉 Laravel 开发,那么理解 MineAdmin 的结构将会非常容易。 ### 架构理念 MineAdmin 采用以下核心设计原则: * **分层架构**:Controller → Service → Repository → Model 的清晰分层 * **职责分离**:每个目录都有明确的职责边界 * **可扩展性**:支持插件化开发和模块化扩展 * **标准化**:遵循 PSR 规范和最佳实践 ## 项目根目录结构 ```plantuml @startmindmap * MineAdmin ** app/ *** 核心业务代码 *** Controllers、Services、Models ** config/ *** 配置文件 *** 数据库、缓存、队列配置 ** database/ *** 数据库迁移 *** 数据填充 *** 模型工厂 ** storage/ *** 日志文件 *** 上传文件 *** 临时文件 ** tests/ *** 单元测试 *** 功能测试 ** web/ *** 前端代码 *** 静态资源 ** plugin/ *** 插件目录 *** 第三方扩展 @endmindmap ``` ### 目录详细说明 #### `/app` - 应用核心目录 应用程序的核心业务逻辑所在地,包含控制器、服务层、数据层等核心组件。 **主要特点:** * 包含 99% 的业务代码 * 遵循 MVC 分层架构 * 支持模块化开发 #### `/config` - 配置目录 存放所有应用程序配置文件,提供灵活的环境配置管理。 **典型配置文件:** * `database.php` - 数据库配置 * `cache.php` - 缓存配置 * `queue.php` - 队列配置 #### `/database` - 数据库目录 管理数据库相关的所有文件,包括结构变更和测试数据。 **目录结构:** ``` database/ ├── migrations/ # 数据库迁移文件 ├── seeders/ # 数据填充文件 ``` #### `/storage` - 存储目录 存放应用程序运行时产生的文件和数据。 **目录用途:** * `uploads/` - 用户上传文件 * `swagger/` - API 文档文件 #### `/tests` - 测试目录 包含自动化测试套件,确保代码质量和功能正确性。 **测试类型:** * **单元测试** - 测试单个类或方法 * **功能测试** - 测试完整的业务流程 * **API 测试** - 测试 API 接口 #### `/web` - 前端目录 存放前端应用代码和静态资源文件。 #### `/plugin` - 插件目录 存放从插件市场下载的插件包,支持系统功能扩展。 ## App 目录深度解析 `app` 目录是整个应用的核心,采用严格的分层架构设计。 ```plantuml @startmindmap * app/ ** Http/ *** Controller/ (控制器层) *** Middleware/ (中间件) *** Request/ (请求验证) ** Service/ *** 业务逻辑层 *** 业务编排 ** Repository/ *** 数据访问层 *** 数据组装 ** Model/ *** 数据模型层 *** ORM 映射 ** Exceptions/ *** 异常处理 *** 错误管理 ** Schema/ *** API 文档 *** Swagger 定义 @endmindmap ``` ### Http 目录 - 请求处理层 负责处理所有 HTTP 请求的入口层,包含控制器、中间件和请求验证。 #### 目录结构 ``` Http/ ├── Admin/ # 后台管理模块 │ ├── Controller/ # 后台控制器 │ ├── Middleware/ # 后台中间件 │ ├── Request/ # 后台请求验证类 │ ├── Subscriber/ # 事件订阅者 │ └── Vo/ # 值对象类 ├── Api/ # API 接口模块 │ ├── Controller/ # API 控制器 │ │ └── V1/ # API 版本控制 │ ├── Middleware/ # API 中间件 │ └── Request/ # API 请求验证类 │ └── V1/ # API 版本请求类 ├── Common/ # 通用模块 │ ├── Controller/ # 通用控制器 │ ├── Event/ # 事件类 │ ├── Middleware/ # 通用中间件 │ ├── Request/ # 通用请求类 │ ├── Result.php # 响应结果类 │ ├── ResultCode.php # 结果状态码 │ └── Swagger/ # API 文档配置 └── CurrentUser.php # 当前用户上下文 ``` #### 模块化架构说明 **Admin 模块** - 后台管理功能 * 包含权限管理、用户管理、菜单管理等后台功能 * 采用完整的 MVC 结构,包含事件订阅者和值对象 **Api 模块** - 对外 API 接口 * 支持版本控制(V1, V2 等) * 独立的认证中间件和请求验证 * RESTful API 设计规范 **Common 模块** - 通用组件 * 提供跨模块共享的基础功能 * 统一的响应格式和状态码管理 * API 文档自动生成配置 ### Service 目录 - 业务逻辑层 Service 层是核心业务逻辑的实现场所,负责业务规则的编排和执行。 #### 设计原则 1. **单一职责** - 每个 Service 类只处理一个业务域 2. **依赖注入** - 通过构造函数注入依赖 3. **事务管理** - 确保业务操作的原子性 4. **异常处理** - 统一的异常处理机制 #### Service 层职责 **核心功能:** * 业务逻辑编排和执行 * 事务管理和数据一致性 * 调用 Repository 层进行数据操作 * 业务规则验证和处理 ### Repository 目录 - 数据访问层 Repository 模式提供了数据访问的抽象层,封装了数据查询和操作逻辑。 #### 设计特点 * **数据源抽象** - 可以轻松切换数据源(MySQL、Redis、ES等) * **查询复用** - 公共查询逻辑的复用 * **缓存集成** - 透明的缓存层集成 * **性能优化** - 查询优化和批量操作 #### Repository 层特点 **主要职责:** * 数据访问抽象层 * 复杂查询逻辑封装 * 缓存策略实现 * 数据源切换和优化 ### Model 目录 - 数据模型层 Model 层基于 Hyperf 的 Eloquent ORM,提供数据库表的对象关系映射。 #### 模型特性 * **关联关系** - 定义表之间的关联 * **访问器/修改器** - 数据格式化 * **事件监听** - 模型生命周期事件 * **软删除** - 逻辑删除支持 #### Model 层特性 **核心功能:** * 数据表映射和关系定义 * 属性访问器和修改器 * 模型事件和观察者 * 数据类型转换和验证 ### Exceptions 目录 - 异常处理 统一的异常处理机制,提供友好的错误信息和日志记录。 ### Schema 目录 - API 文档 包含 Swagger/OpenAPI 文档定义,用于 API 文档生成。 ::: danger 重要提醒 Schema 类严格禁止参与业务逻辑调度,仅用于 API 文档生成。 ::: ## 开发最佳实践 ### 代码组织规范 1. **命名规范** * 类名使用 `PascalCase` * 方法名使用 `camelCase` * 常量使用 `UPPER_SNAKE_CASE` 2. **文件组织** * 一个文件一个类 * 文件名与类名保持一致 * 合理使用命名空间 3. **依赖注入** * 优先使用构造函数注入 * 避免使用静态调用 * 面向接口编程 ### 架构模式建议 ```plantuml @startuml skinparam monochrome true skinparam shadowing false skinparam defaultFontName "Microsoft YaHei" skinparam defaultFontSize 14 top to bottom direction rectangle "Controller 控制器" as A rectangle "Service 业务层" as B rectangle "Repository 数据层" as C rectangle "Model 模型层" as D rectangle "外部服务" as E rectangle "Middleware 中间件" as F rectangle "Cache 缓存" as G rectangle "Database 数据库" as H A --> B : 调用 B --> C : 数据访问 C --> D : 模型映射 B --> E : 集成 A --> F : 请求处理 C --> G : 缓存读写 C --> H : 持久化存储 @enduml ``` ### 错误处理策略 1. **异常分类** * 业务异常 - 可预期的错误 * 系统异常 - 不可预期的错误 * 验证异常 - 数据格式错误 2. **日志记录** * 关键操作记录 * 异常信息记录 * 性能监控记录 ## 相关资源 ### 参考文档 * [Laravel 官方文档](https://laravel.com/docs/11.x) * [Laravel 中文文档](https://learnku.com/docs/laravel/10.x) * [Hyperf 协程框架](https://hyperf.wiki/3.1/#/en/) ::: warning ORM 差异说明 MineAdmin 使用的是由 [Hyperf](https://github.com/hyperf/hyperf) 维护的协程版 Eloquent ORM,在用法上与 Laravel 官方版本存在一定差异。在开发时请注意协程环境下的特殊用法。 ::: --- --- url: /backend/frameworks/hyperf/3.2/base/structure.md --- # 项目目录结构 MineAdmin 采用现代化的分层架构设计,提供清晰的代码组织结构和最佳实践。本文档将详细介绍项目的目录结构、设计理念以及开发规范。 ## 概述 MineAdmin 的项目结构参考了 [Laravel](https://laravel.com/) 框架的设计理念,同时结合了现代化的分层架构模式。如果你熟悉 Laravel 开发,那么理解 MineAdmin 的结构将会非常容易。 ### 架构理念 MineAdmin 采用以下核心设计原则: * **分层架构**:Controller → Service → Repository → Model 的清晰分层 * **职责分离**:每个目录都有明确的职责边界 * **可扩展性**:支持插件化开发和模块化扩展 * **标准化**:遵循 PSR 规范和最佳实践 ## 项目根目录结构 ```plantuml @startmindmap * MineAdmin ** app/ *** 核心业务代码 *** Controllers、Services、Models ** config/ *** 配置文件 *** 数据库、缓存、队列配置 ** database/ *** 数据库迁移 *** 数据填充 *** 模型工厂 ** storage/ *** 日志文件 *** 上传文件 *** 临时文件 ** tests/ *** 单元测试 *** 功能测试 ** web/ *** 前端代码 *** 静态资源 ** plugin/ *** 插件目录 *** 第三方扩展 @endmindmap ``` ### 目录详细说明 #### `/app` - 应用核心目录 应用程序的核心业务逻辑所在地,包含控制器、服务层、数据层等核心组件。 **主要特点:** * 包含 99% 的业务代码 * 遵循 MVC 分层架构 * 支持模块化开发 #### `/config` - 配置目录 存放所有应用程序配置文件,提供灵活的环境配置管理。 **典型配置文件:** * `database.php` - 数据库配置 * `cache.php` - 缓存配置 * `queue.php` - 队列配置 #### `/database` - 数据库目录 管理数据库相关的所有文件,包括结构变更和测试数据。 **目录结构:** ``` database/ ├── migrations/ # 数据库迁移文件 ├── seeders/ # 数据填充文件 ``` #### `/storage` - 存储目录 存放应用程序运行时产生的文件和数据。 **目录用途:** * `uploads/` - 用户上传文件 * `swagger/` - API 文档文件 #### `/tests` - 测试目录 包含自动化测试套件,确保代码质量和功能正确性。 **测试类型:** * **单元测试** - 测试单个类或方法 * **功能测试** - 测试完整的业务流程 * **API 测试** - 测试 API 接口 #### `/web` - 前端目录 存放前端应用代码和静态资源文件。 #### `/plugin` - 插件目录 存放从插件市场下载的插件包,支持系统功能扩展。 ## App 目录深度解析 `app` 目录是整个应用的核心,采用严格的分层架构设计。 ```plantuml @startmindmap * app/ ** Http/ *** Controller/ (控制器层) *** Middleware/ (中间件) *** Request/ (请求验证) ** Service/ *** 业务逻辑层 *** 业务编排 ** Repository/ *** 数据访问层 *** 数据组装 ** Model/ *** 数据模型层 *** ORM 映射 ** Exceptions/ *** 异常处理 *** 错误管理 ** Schema/ *** API 文档 *** Swagger 定义 @endmindmap ``` ### Http 目录 - 请求处理层 负责处理所有 HTTP 请求的入口层,包含控制器、中间件和请求验证。 #### 目录结构 ``` Http/ ├── Admin/ # 后台管理模块 │ ├── Controller/ # 后台控制器 │ ├── Middleware/ # 后台中间件 │ ├── Request/ # 后台请求验证类 │ ├── Subscriber/ # 事件订阅者 │ └── Vo/ # 值对象类 ├── Api/ # API 接口模块 │ ├── Controller/ # API 控制器 │ │ └── V1/ # API 版本控制 │ ├── Middleware/ # API 中间件 │ └── Request/ # API 请求验证类 │ └── V1/ # API 版本请求类 ├── Common/ # 通用模块 │ ├── Controller/ # 通用控制器 │ ├── Event/ # 事件类 │ ├── Middleware/ # 通用中间件 │ ├── Request/ # 通用请求类 │ ├── Result.php # 响应结果类 │ ├── ResultCode.php # 结果状态码 │ └── Swagger/ # API 文档配置 └── CurrentUser.php # 当前用户上下文 ``` #### 模块化架构说明 **Admin 模块** - 后台管理功能 * 包含权限管理、用户管理、菜单管理等后台功能 * 采用完整的 MVC 结构,包含事件订阅者和值对象 **Api 模块** - 对外 API 接口 * 支持版本控制(V1, V2 等) * 独立的认证中间件和请求验证 * RESTful API 设计规范 **Common 模块** - 通用组件 * 提供跨模块共享的基础功能 * 统一的响应格式和状态码管理 * API 文档自动生成配置 ### Service 目录 - 业务逻辑层 Service 层是核心业务逻辑的实现场所,负责业务规则的编排和执行。 #### 设计原则 1. **单一职责** - 每个 Service 类只处理一个业务域 2. **依赖注入** - 通过构造函数注入依赖 3. **事务管理** - 确保业务操作的原子性 4. **异常处理** - 统一的异常处理机制 #### Service 层职责 **核心功能:** * 业务逻辑编排和执行 * 事务管理和数据一致性 * 调用 Repository 层进行数据操作 * 业务规则验证和处理 ### Repository 目录 - 数据访问层 Repository 模式提供了数据访问的抽象层,封装了数据查询和操作逻辑。 #### 设计特点 * **数据源抽象** - 可以轻松切换数据源(MySQL、Redis、ES等) * **查询复用** - 公共查询逻辑的复用 * **缓存集成** - 透明的缓存层集成 * **性能优化** - 查询优化和批量操作 #### Repository 层特点 **主要职责:** * 数据访问抽象层 * 复杂查询逻辑封装 * 缓存策略实现 * 数据源切换和优化 ### Model 目录 - 数据模型层 Model 层基于 Hyperf 的 Eloquent ORM,提供数据库表的对象关系映射。 #### 模型特性 * **关联关系** - 定义表之间的关联 * **访问器/修改器** - 数据格式化 * **事件监听** - 模型生命周期事件 * **软删除** - 逻辑删除支持 #### Model 层特性 **核心功能:** * 数据表映射和关系定义 * 属性访问器和修改器 * 模型事件和观察者 * 数据类型转换和验证 ### Exceptions 目录 - 异常处理 统一的异常处理机制,提供友好的错误信息和日志记录。 ### Schema 目录 - API 文档 包含 Swagger/OpenAPI 文档定义,用于 API 文档生成。 ::: danger 重要提醒 Schema 类严格禁止参与业务逻辑调度,仅用于 API 文档生成。 ::: ## 开发最佳实践 ### 代码组织规范 1. **命名规范** * 类名使用 `PascalCase` * 方法名使用 `camelCase` * 常量使用 `UPPER_SNAKE_CASE` 2. **文件组织** * 一个文件一个类 * 文件名与类名保持一致 * 合理使用命名空间 3. **依赖注入** * 优先使用构造函数注入 * 避免使用静态调用 * 面向接口编程 ### 架构模式建议 ```plantuml @startuml skinparam monochrome true skinparam shadowing false skinparam defaultFontName "Microsoft YaHei" skinparam defaultFontSize 14 top to bottom direction rectangle "Controller 控制器" as A rectangle "Service 业务层" as B rectangle "Repository 数据层" as C rectangle "Model 模型层" as D rectangle "外部服务" as E rectangle "Middleware 中间件" as F rectangle "Cache 缓存" as G rectangle "Database 数据库" as H A --> B : 调用 B --> C : 数据访问 C --> D : 模型映射 B --> E : 集成 A --> F : 请求处理 C --> G : 缓存读写 C --> H : 持久化存储 @enduml ``` ### 错误处理策略 1. **异常分类** * 业务异常 - 可预期的错误 * 系统异常 - 不可预期的错误 * 验证异常 - 数据格式错误 2. **日志记录** * 关键操作记录 * 异常信息记录 * 性能监控记录 ## 相关资源 ### 参考文档 * [Laravel 官方文档](https://laravel.com/docs/11.x) * [Laravel 中文文档](https://learnku.com/docs/laravel/10.x) * [Hyperf 协程框架](https://hyperf.wiki/3.1/#/en/) ::: warning ORM 差异说明 MineAdmin 使用的是由 [Hyperf](https://github.com/hyperf/hyperf) 维护的协程版 Eloquent ORM,在用法上与 Laravel 官方版本存在一定差异。在开发时请注意协程环境下的特殊用法。 ::: --- --- url: /v3/guide/introduce/thank.md --- # 鸣谢 **感谢他们提供好用的产品,让我们站在巨人的肩膀上** > 以下排名不分先后 [Hyperf 一款高性能企业级协程框架](https://hyperf.io/) [Element Plus 一款优秀的前端组件库](https://element-plus.org/zh-CN/) [Swoole PHP协程框架](https://www.swoole.com) [Vue 前端框架](https://vuejs.org/) [Vite 前端运行及打包工具](https://vitejs.cn/) [Pinia 下一代的Vuex](https://github.com/vuejs/pinia) [Jetbrains 生产力工具](https://www.jetbrains.com/)