--- url: /start/overview.md --- # 🏡 CatchAdmin 专业版介绍 ## 核心技术栈 * **Laravel 11**(3.x 版本)/**Laravel12**(4.x 版本) - 现代化 PHP 后台管理框架 * **Vue3** - 渐进式前端 JavaScript 框架,提供响应式用户界面 * **ElementPlus** - 基于 Vue3 的企业级 UI 组件库 * **MySQL** - 高性能关系型数据库管理系统 * **Nginx** - 高性能 Web 服务器和反向代理 * **Redis** - 内存数据库,提供缓存和会话管理 ## 关于 CatchAdmin 专业版 首先感谢一直以来对 `CatchAdmin` 开源项目的支持和使用。作为一名开源工作者,我一直致力于开发出功能强大且易于使用的**企业级后台管理系统**,以帮助您简化业务流程和提升工作效率。为了能够继续开发和维护这个项目,我将推出一款专业版后台管理系统,以确保我能够持续为您提供高质量的服务和技术支持。 **专业版特性:** * **无缝升级**:专业版本不会在开源版本做破坏性变更,从开源版本切换到专业版本无任何开发负担 * **功能增强**:提供更多企业级功能模块和高级组件来配合您的工作 * **持续更新**:定期功能更新和安全补丁,满足不断变化的业务需求 我深信,CatchAdmin 专业版后台管理系统将为您的企业带来更多的价值和便利,显著提升开发效率和业务管理水平。 ## 购买专业版授权 ### 购买流程 请首先在[CatchAdmin 专业版官网](https://catchadmin.vip)进行账户注册,注册完成之后登录授权管理后台,在[Dashboard 页面](https://catchadmin.vip/dashboard)选择合适的授权版本进行购买。查看详细的[版本价格方案](https://catchadmin.vip/pricing)。 :::warning 支付方式 目前主要支持支付宝支付,如需其他支付方式请通过右下方客服窗口直接联系我们 ::: 购买授权完成之后,您可以在[Dashboard 页面](https://catchadmin.vip/dashboard)立即下载获取专业版源代码和相关技术文档。 ### 在线体验 在购买前,您可以通过[专业版在线演示](https://pro.catchadmin.com)全面体验 CatchAdmin 专业版后台管理系统的功能特性。 ### 技术支持与讨论 [CatchAdmin 技术讨论区](https://catchadmin.vip/forum) - 获取技术支持、功能建议和用户交流 ## 专业版独特优势 * **丰富的功能模块**:提供商城管理、CMS 内容管理、会员系统等企业级功能模块 * **优先技术支持**:作为付费用户,享有优先的技术支持和问题解决服务 * **高级组件库**:提供更多高级组件(如 `catch-table` 高级表格组件等) * **持续更新维护**:定期更新和改进后台管理系统,满足不断变化的业务需求和技术趋势 ## 目录结构 `Catchadmin` 专业版本服务端和前端(web 目录)放在一个项目中,这样会更方便开发。 ```php ├─app ├─bootstrap ├─config(配置目录) ├─database(migration和seed存放目录) ├─lang(多语言目录) ├─public(运行目录 ├─modules(业务功能模块目录) | ├──User (后台用户管理 - 管理员账户和权限) | ├──Permissions (权限控制模块 - RBAC权限管理系统) | ├──Common (公共服务模块 - 基础功能和工具类) | ├──Pay (支付集成模块 - 多渠道支付解决方案) | ├──System (系统管理模块 - 系统配置和监控) | ├──Cms (内容管理系统 - 文章和页面管理) | ├──Shop (电商管理模块 - 商品、订单、库存管理) | ├──Develop (开发工具模块 - 代码生成器和开发辅助) | ├──Wechat (微信生态模块 - 公众号和小程序管理) | ├──Openapi (开放API模块 - 第三方系统集成接口) | ├──Domain (域名管理模块 - 多站点域名配置) | └──Member (会员系统模块 - 用户注册和会员管理) ├─web │ ├─src (前端目录) │ │ ├─assets | | ├─compoents (组件) | | ├─enum (枚举) | | ├─layout | | ├─router | | ├─store (pinia目录) | | ├─styles (样式目录) | | ├─support (助手方法) | | ├─types (类型目录) | | ├─views | | | App.vue | | | app.ts | | | env.d.ts │ | package.json | | postcss.config.js │ | tailwind.config.js │ | tsconfig.json │ | tsconfig.node.json │ | vite.config.js (Vue项目配置) ├─routes ├─lang (多语言目录) ├─storage ├─tests │ .env-example(env配置示例) │ .gitattributes │ .gitignore │ .travis.yml │ composer.json │ .php-cs-fixer.dist.php │ phpunit.xml └─ artisan(命令行入口文件) ``` 通过以上目录结构,您可以快速了解 CatchAdmin 专业版的整体架构。在后续的文档中,我们将详细介绍各个模块的具体功能、开发方法和配置说明,帮助您更好地使用这套企业级后台管理系统。 --- --- url: /start/install.md --- # CatchAdmin 专业版安装指南 :::info 本章节介绍 CatchAdmin 专业版本地开发环境的安装配置。生产环境部署请查看[部署章节](./deploy.md) ::: ## 环境要求 | # | 环境 | 版本 | | :-- | :---- | :------ | | 1 | PHP | >= 8.2+ | | 2 | Nginx | latest | | 3 | Mysql | >= 5.7 | | 4 | Node | >= 22 | :::warning CatchAdmin 专业版 3.x 版本需要 Node.js 20,4.0+ 版本需要 Node.js 22。请根据项目版本选择对应的 Node.js 运行环境。 ::: ## 开发工具准备 在安装 CatchAdmin 专业版项目之前,您需要准备以下必要的开发工具和运行环境: | # | 必须 | 官网 | | :-- | :---------------------- | :-------------------------------- | | 1 | Composer PHP 依赖管理器 | https://getcomposer.org/download/ | | 2 | Node.js (>= 22) | https://nodejs.org/zh-cn/ | | 3 | Yarn 前端包管理器 | https://yarn.bootcss.com/ | | 4 | Vite 前端构建工具 | https://cn.vitejs.dev/ | :::warning 请确保上述开发工具已正确安装并可在命令行中使用,否则 CatchAdmin 专业版项目安装将会失败。 ::: ## 下载代码 购买 CatchAdmin 专业版授权后,可以从授权仓库下载项目源码 ![授权仓库](https://image.catchadmin.com/202506091548559.png) 在授权仓库进行下载。这里分四种方式,分别解释下。 * `版本`: 阶段性稳定功能的版本,可以在更新里面看到 [版本更新日志](./upgrade.md) * `分支`: 分支功能都是优先于版本的,都是直接提交的,你可以当作开发分支来看,不保证稳定性。 * `commit`: commit 用于修复一些紧急 bug,你可以看作一个一个的即时的小补丁 * `补丁`:版本之间的 diff 差异,例如你目前正在使用 `v3.5.0`,需要升级到 `v4.0.0`,就需要下载 `v4.0.0` 的补丁。[查看如何更新补丁](../server/consoles.md#补丁更新)。`需要注意的是补丁不会跨版本`。 请根据实际需求选择合适的版本下载源码包,所有代码包均为 `zip` 压缩格式,下载解压后即可进行 CatchAdmin 专业版项目的安装配置。 进行开发时建议阅读[最佳开发实践](./develop.md),了解项目结构和版本升级流程 ## CatchAdmin 专业版项目安装 :::warning 请确保已完成[开发工具准备](#开发工具准备)中的所有环境配置,缺少必要工具将导致 CatchAdmin 专业版安装失败。 ::: ### 命令行认证 :::info 推荐使用该方式安装 ::: CatchAdmin 专业版提供便捷的授权认证脚本,自动完成用户验证和项目依赖安装。进入项目根目录,执行以下认证命令 首先获取授权码,在[授权码页面](https://catchadmin.vip/user/license)获取,点击生成`授权码`之后。记得保存,页面不做**二次展示** ![授权码页面](https://image.catchadmin.com/202505041734786.png) 进入到源码解压之后的根目录,然后使用下面的命令 ```shell php auth 邮箱 这里替换成生成的授权码 # 假设你的邮箱是 catch@pro.com,授权码是 123456 , 那么需要执行如下的命令 php auth catch@pro.com 123456 ``` :::warning 系统会对每个账户下载来源统计,所以请不要泄露账户。如果出现不寻常的情况,我们会对账户进行一些限制。如果有误,烦请联系管理员 ::: 系统将自动通过 Composer 安装 PHP 项目依赖包。依赖安装完成后,需要初始化 CatchAdmin 专业版项目配置和数据库结构,执行以下命令 ```shell # 安装后台, 按照提示输入对应信息即可 php artisan catch:install ``` [更多其他的 CatchAdmin 开发相关的内置命令](../server/consoles.md) :::tip 生产环境部署时,请使用 `php artisan catch:install --prod` 命令进行项目初始化配置。 ::: ## Vue 前端项目配置 :::info 以下 Vue 前端项目配置步骤通常由 `catch:install` 命令自动完成。如需手动配置,请按照下面的步骤执行。 ::: Vue 前端项目源码位于 CatchAdmin 专业版根目录下的 `web` 目录中 ```shell cd web # 配置 npm 镜像源(提升下载速度,如使用代理可忽略) yarn config set registry https://registry.npmmirror.com # 安装 Vue 项目 npm 依赖包 yarn install ``` 进入 `web` 目录,创建 Vue 项目的环境配置文件 `.env`,配置内容如下 :::tip `catch:install` 命令已自动生成此配置文件,如需自定义 API 地址或项目名称,可按以下格式修改。 ::: ```shell # 记住一定要加上 api/ 前缀 VITE_BASE_URL=http://127.0.0.1:8000/api/ # 项目名称 VITE_APP_NAME=xxx项目 ``` ### 启动命令 使用以下命令启动 Vue 前端项目的 Vite 开发服务器 ```shell yarn dev ``` ::: warning CatchAdmin 专业版采用前后端分离架构,不能直接访问 PHP 后端项目(会产生异常)。需要启动 Vue 前端项目通过 API 接口与后端通信。 初次使用 Vue 开发者建议阅读 [Vue.js 官方文档](https://cn.vuejs.org/)。CatchAdmin 专业版前端 UI 组件基于 [Element Plus](https://element-plus.org) 构建。 ::: ## 开发环境启动 > CatchAdmin 专业版 4.0.0+ 版本提供了一键启动开发环境的便捷命令,无需分别执行 `php artisan serve` 和 `yarn dev` ```shell composer run dev ``` :::warning 如果本地开发环境已配置了集成环境(如 XAMPP、WAMP Laragon 等)或使用 Nginx/Apache 等 Web 服务器,请不要使用 `composer run dev` 命令。 此种情况下请单独启动前端项目:进入 `web` 目录后执行 `yarn dev` ::: ### 如何实现 以下为技术实现说明,便于故障排查: * 此命令启动的 PHP 内置服务器默认监听 `8000` 端口,如需修改请编辑 `composer.json` 中的启动脚本 * 依赖 `concurrently` 包实现并发启动,通常已预装。如缺失可全局安装:`npm install -g concurrently` ## 最佳开发实践 [开发实践, 如何升级](./develop.md) ## 补丁安装 请从授权仓库下载对应版本的升级补丁包 ![补丁安装](https://image.catchadmin.com/202505251010655.png) 下载补丁包后,将其放置到 CatchAdmin 专业版项目根目录。假设补丁文件名为 `xxxxx.zip`,使用以下命令执行版本升级 ```shell php artisan catch:upgrade xxxx.zip ``` --- --- url: /server/module.md --- # CatchAdmin 模块化开发指南 在开始使用 CatchAdmin 做后台项目开发前,首先需要深入了解 CatchAdmin 框架的一个最核心功能 - `模块化架构`。CatchAdmin 采用了先进的模块化设计理念,将后台系统中的所有业务功能都拆分为独立的、可重用的功能模块。这种组件化架构使得开发者之间可以方便地进行模块共享和协作开发。 ## CatchAdmin 模块化架构如何工作 CatchAdmin 框架的模块化组件通常存放在 **modules** 目录下,采用类似 Laravel Package 的组织结构。通过下面的 Artisan 命令可以方便地进行模块安装管理 ```shell php artisan catch:module:install ``` 根据 Artisan 命令行控制台显示的交互式提示,选择相应的 CatchAdmin 模块组件进行安装 CatchAdmin 模块安装命令运行完成后,系统会在 `storage/app` 目录下自动生成 `module.json` 模块配置文件,该 JSON 文件包含了所有已安装模块的详细信息和状态数据。 ![Laravel Admin - catchadmin模块架构图](https://image.catchadmin.com/202409081829416.png) 为了深入了解 CatchAdmin 模块化开发的具体工作流程,下面通过一个完整的新模块开发示例来详细说明 ## CatchAdmin 模块化组件开发实战 CatchAdmin 模块化开发可以通过可视化的模块创建工具开始 ![Laravel Admin - catchadmin 创建模块](https://image.catchadmin.com/202409081830987.png) 在 CatchAdmin 后台管理系统中,点击开发工具的模块管理菜单,然后选择新建模块功能。按照模块化开发规范填入模块的相关信息(模块名、描述、依赖关系等)后,点击创建即可。自动生成模块化组件的标准目录结构和文件。以下以创建一个 **test** 模块为例 ![Laravel Admin - catchadmin 管理模块](https://image.catchadmin.com/202409081831594.png) ![Laravel Admin - catchadmin 管理模块](https://image.catchadmin.com/202409081830144.png) 如图可以看到已经生成了**Test**模块 * `Http` 目录:CatchAdmin 模块的主要开发区域,包含控制器和中间件 * `Models` 数据模型存放目录,遵循 Laravel Eloquent ORM 规范 * `database` 数据库相关目录,包含 **migration** 数据迁移和 **seed** 数据填充脚本 * `Providers` 模块服务提供者,这是模块化架构的核心,路由注册、Artisan 命令、配置加载都需要通过服务提供者进行 * `route.php` 模块路由配置文件。这个也可以当作模块路由的入口文件。后期如果拆分路由,例如你拆开一个 `api.php`,只需要在 `route.php` 种 `require api.php` 即可加载 api 路由 * `Installer.php` 模块安装器,管理模块的安装和卸载逻辑 下面深入解析 CatchAdmin 模块化架构中最关键的 `ServiceProvider` 组件 ```php namespace Modules\Test\Providers; use Catch\CatchAdmin; use Catch\Providers\CatchModuleServiceProvider; class TestServiceProvider extends CatchModuleServiceProvider { public function moduleName(): string|array { return 'common'; } } ``` 在 CatchAdmin 模块化架构中,`ServiceProvider` 默认情况下会自动进行路由注册和加载。除了基本的路由管理,还额外提供了多个高级功能 * **事件管理** 在 CatchAdmin 模块中集成 Laravel 事件系统,如果需要为模块注册特定的事件监听器,这个功能与 Laravel 框架提供的默认事件机制保持一致。ServiceProvider 提供了一个标准化的 `$events` 数组配置 ```php class TestServiceProvider extends CatchModuleServiceProvider { protected $events = []; } ``` * **HTTP 中间件** 如果需要为 CatchAdmin 模块注册自定义的 HTTP 中间件组件,需要在 ServiceProvider 中实现 `middlewares` 方法 ```php class TestServiceProvider extends CatchModuleServiceProvider { protected function middlewares(): array { return []; } } ``` :::warning CatchAdmin 中间件作用域说明 在 ServiceProvider 中注册的 middlewares 是全局生效的,会影响 CatchAdmin 系统中的所有模块路由,因此需要谨慎使用。 如果只需要中间件作用于当前模块,建议直接在模块的路由配置文件中设置即可。 ::: ## CatchAdmin 模块自动加载机制 CatchAdmin 模块化架构的依赖管理和启动机制依赖于一个核心的配置文件 `storage/app/module.json`。这个 JSON 配置文件管理着所有模块的状态控制,包括模块的启用、禁用、依赖关系等。 为了提升开发灵活性,`CatchAdmin` 框架提供了一个可配置的模块自动加载机制, ::: code-group ```js [.env] // 在 .env 文件中修改 CATCH_MODULE_AUTOLOAD = true ``` ```php [config/catch.php] // 在模块配置中修改 [ 'module' => [ 'autoload' => env('CATCH_MODULE_AUTOLOAD', false), ], ] ``` ::: ## CatchAdmin 模块依赖管理与组件关系 在模块化开发中,某些复杂的业务模块可能需要依赖其他基础模块的功能组件,这时需要在安装时同步检查和安装依赖模块。CatchAdmin 的模块安装器提供了优雅的依赖管理机制。以商城模块的实际开发场景为例 ```php namespace Modules\Shop; use Catch\Support\Module\Installer as ModuleInstaller; use Modules\Shop\Providers\ShopServiceProvider; class Installer extends ModuleInstaller { // 添加模块依赖 protected function dependencies(): array { return ['member']; } } ``` ## CatchAdmin 模块安装与管理命令 ```shell php artisan catch:module:install ``` 使用 Artisan 命令行工具进行 CatchAdmin 模块安装,根据控制台显示的可用模块列表选择安装即可 ### 批量安装所有 CatchAdmin 模块 为了提升开发效率,CatchAdmin 支持一次性批量安装所有可用模块组件 ```shell php artisan catch:module:install --all ``` ## CatchAdmin 模块安装器与组件分享 :::info CatchAdmin 模块分享说明 如果只是为了个人或内部团队开发 CatchAdmin 模块,不需要发布到开源社区的话,可以跳过下面的模块分享配置步骤。 ::: 如果希望将自己开发的 CatchAdmin 模块贡献给社区,供其他 PHP 开发者使用,需要为模块创建一个标准化的安装器组件。按照 CatchAdmin 模块化规范,安装器通常约定存放在模块的根目录下。可以参考权限管理模块的安装器实现案例 ```php namespace Modules\Permissions; use Catch\Support\Module\Installer as ModuleInstaller; class Installer extends ModuleInstaller { protected function info(): array { // TODO: Implement info() method. return [ 'title' => '权限管理', 'name' => 'permissions', 'path' => 'permissions', 'keywords' => '权限, 角色, 部门', 'description' => '权限管理模块', 'provider' => PermissionsServiceProvider::class ]; } protected function requirePackages(): void { // TODO: Implement requirePackages() method. } protected function removePackages(): void { // TODO: Implement removePackages() method. } } ``` CatchAdmin 模块安装器需要实现数个关键方法来提供模块元数据信息。主要通过 `info` 方法返回模块的基本信息,如果模块需要额外的 Composer 包依赖,则需要在 `requirePackages` 方法中实现依赖包的自动安装逻辑 ```php protected function requirePackages(): void { // TODO: Implement requirePackages() method. $this->composer()->require('package/name') } ``` 对于采用动态菜单管理的 CatchAdmin 模块,还需要将模块的后台管理菜单信息导出为可分享的数据格式。CatchAdmin 提供了专用的 Artisan 命令来处理菜单导出 ```shell php artisan catch:export:menu ``` * `table` 参数为可选项,默认指向 CatchAdmin 的 `permissions` 数据表 对指定的 CatchAdmin 模块进行菜单数据导出,并自动生成相应的 Laravel Seeder 数据填充文件。这个功能主要用于模块打包发布阶段,对于不需要与其他开发者共享的内部模块,可以忽略此步骤 ```php php artisan catch:export:menu permissions ``` 完成以上步骤后,就可以将自己开发的 CatchAdmin 模块组件分享给全球的 PHP 开发者使用了!👏 请积极参与 CatchAdmin 开源社区建设,共同推动 PHP 模块化开发的发展。 --- --- url: /server/model.md --- # CatchModel 模型介绍 在后台项目开发中,大部分业务逻辑都与数据模型操作和数据库交互相关。为了简化开发流程并提供统一的数据操作接口,`CatchAdmin` 框架提供了功能强大的模型基类 `CatchModel`。所有业务模型都继承自 `CatchModel`,从而获得丰富的数据操作功能和内置特性。 ::: tip 重要提示 建议仔细阅读本文档,全面掌握 CatchModel 的数据操作功能将显著提升开发效率 ::: ## CatchModel 核心类 `CatchModel` 是 `CatchAdmin` 框架的核心模型基类,基于 Laravel Eloquent ORM 进行扩展,继承自 Laravel 的 `Model` 类,并集成了多个高级功能特性: ```php abstract class CatchModel extends Model { use BaseOperate, Trans, SoftDeletes, ScopeTrait; /** * 使用 Unix 时间戳格式 * @var string */ protected $dateFormat = 'U'; /** * 默认分页数量 */ protected $perPage = 10; /** * 关闭 Laravel 自动时间戳管理 * @var bool */ public $timestamps = false; /** * 默认类型转换配置 * @var array */ protected array $defaultCasts = [ 'created_at' => 'datetime:Y-m-d H:i:s', 'updated_at' => 'datetime:Y-m-d H:i:s', ]; /** * 默认隐藏字段 * @var array */ protected array $defaultHidden = ['deleted_at']; public function __construct(array $attributes = []) { parent::__construct($attributes); $this->init(); } /** * 初始化模型配置 */ protected function init() { $this->makeHidden($this->defaultHidden); $this->mergeCasts($this->defaultCasts); } /** * 自定义软删除启动方法 */ public static function bootSoftDeletes(): void { static::addGlobalScope(new SoftDelete()); } /** * 覆盖恢复方法,使用自定义的软删除逻辑 */ public function restore(): bool { if ($this->fireModelEvent('restoring') === false) { return false; } $this->{$this->getDeletedAtColumn()} = 0; $this->exists = true; $result = $this->save(); $this->fireModelEvent('restored', false); return $result; } } ``` ### 时间戳处理机制 `CatchAdmin` 框架中所有数据表的 `created_at` 和 `updated_at` 时间字段都采用 Unix 时间戳(`int` 类型)进行存储,这种设计提升了数据库查询效率。为了在 API 返回给前端时提供用户友好的日期格式,框架内置了数据类型转换配置: ```php protected array $defaultCasts = [ 'created_at' => 'datetime:Y-m-d H:i:s', 'updated_at' => 'datetime:Y-m-d H:i:s', ]; ``` ::: info 为什么 CatchModel 使用 `defaultCasts` 而不是 `casts`? 由于所有业务模型都继承自 `CatchModel`,而日期类型转换是每个数据模型都需要的基础功能。如果直接使用 Laravel 的 `casts` 属性,在子类中定义自己的 `casts` 时可能会覆盖基类的配置。`CatchModel` 使用 `defaultCasts` 可以确保基础转换不被意外覆盖。`defaultHidden` 属性也采用了同样的设计理念。 ::: ### 软删除机制 `CatchModel` 采用了自定义的软删除机制,与 Laravel Eloquent 默认的 `null` 值不同,使用 `0` 数值作为数据未删除状态。这种设计更符合数据库索引优化的需求,能够提升大数据量场景下的查询性能。 ::: tip `CatchModel` 的软删除使用方式与 Laravel 原生 SoftDelete 功能保持一致,无需额外配置。 ::: ```php class SoftDelete extends SoftDeletingScope { public function apply(Builder $builder, Model $model) { $builder->where($model->getQualifiedDeletedAtColumn(), '=', 0); } } ``` ## 模型属性配置 `CatchModel` 提供了丰富的模型属性配置选项,用于精细控制数据模型的 CRUD 操作行为、查询逻辑和数据处理特性: ### 数据结构相关属性 ```php // 树形结构父级字段,建议使用默认值 protected string $parentIdColumn = 'parent_id'; // 排序字段配置 protected string $sortField = 'sort'; protected bool $sortDesc = true; // 默认倒序 // 列表数据返回格式 protected bool $asTree = false; // 是否以树形结构返回 protected bool $isPaginate = true; // 是否启用分页 ``` ### 查询和表单相关属性 ```php // 列表查询默认字段 protected array $fields = []; // 表单提交字段配置 protected array $form = []; // 关联关系处理(如用户与角色的多对多关系) protected array $formRelations = []; ``` ### 权限控制属性 ```php // 数据权限控制 protected bool $dataRange = false; // 字段权限控制 protected bool $columnAccess = false; // 创建人自动填充 protected bool $isFillCreatorId = true; ``` ### 数据处理属性 ```php // 空值自动转换 protected bool $autoNull2EmptyString = true; // 动态排序参数 protected string $dynamicQuerySortField = 'sortField'; protected string $dynamicQuerySortOrder = 'order'; ``` ## 模型方法详解 ### 数据查询方法 #### 获取列表数据 ```php public function getList(): mixed ``` 该方法是 `CatchModel` 的核心数据查询方法,集成了多种高级数据检索功能: * 字段权限过滤 * 创建人信息关联 * 快速搜索支持 * 数据权限控制 * 自定义排序规则 * 动态排序支持 * 分页/树形结构返回 #### 列表查询自定义扩展 通过 `CatchModel` 的 `setBeforeGetList` 方法可以在数据查询执行前添加自定义逻辑: ::: code-group ```php [排序示例] // 添加自定义排序规则 $model->setBeforeGetList(function ($query) { return $query->orderByDesc('sort'); })->getList(); ``` ```php [联表查询] // 添加表连接 $model->setBeforeGetList(function ($query) { return $query->join('some_table', 'table.id', '=', 'some_table.table_id'); })->getList(); ``` ```php [关联加载] // 添加关联关系加载 $model->setBeforeGetList(function ($query) { return $query->with('someRelations'); })->getList(); ``` ::: ### 数据保存方法 #### 保存数据(单条记录) ```php public function storeBy(array $data): mixed ``` `CatchModel` 的数据保存方法,用于处理单条数据记录的数据库存储,支持 Laravel 关联关系的智能处理。操作成功返回主键 ID,失败返回 `false`。 #### 创建数据(批量操作) ```php public function createBy(array $data): mixed ``` ::: tip `createBy` 方法适用于数据批量创建的业务场景,而 `storeBy` 更适合单条数据 CRUD 操作。 ::: #### 更新数据 ```php public function updateBy($id, array $data): mixed ``` `CatchModel` 的数据更新方法,根据主键 ID 更新数据记录,支持 Laravel 关联关系的同步更新。 #### 批量更新 ```php public function batchUpdate(string $field, array $condition, array $data): bool ``` **参数说明:** | 参数 | 类型 | 说明 | | ------------ | -------- | ---------------------------- | | `$field` | `string` | 更新条件字段(如 `id`) | | `$condition` | `array` | 条件值数组(如 `[1, 2, 3]`) | | `$data` | `array` | 更新数据键值对 | ::: warning CatchModel 批量更新注意事项 * 该方法不会触发 Laravel 模型事件 * 数据条件数量必须与更新数据数量一致 ::: **使用示例:** ```php $model = new SomeModel(); $model->batchUpdate('id', [1, 2, 3], [ 'name' => ['小明', '小隋', '小书'], 'age' => [12, 13, 14] ]); ``` ### 数据查询方法 #### 单条数据查询 ```php public function firstBy($value, $field = null, array $columns = ['*']): ?Model ``` `CatchModel` 的单条数据查询方法,根据指定数据库字段查询单条记录,默认使用主键 `id` 字段。支持数据权限和字段权限过滤。 ### 数据删除方法 #### 删除数据 ```php public function deleteBy($id, bool $force = false, bool $softForce = false): ?bool ``` `CatchModel` 的数据删除方法,根据主键 ID 删除数据记录,默认采用安全的软删除机制。当 `$force` 参数设置为 `true` 时进行永久物理删除。 #### 删除软删除数据 ```php public function deleteTrash($id): mixed ``` 彻底删除已被 `CatchModel` 软删除标记的数据记录,执行永久性数据库删除操作。 #### 批量删除 ```php public function deletesBy(array|string $ids, bool $force = false, ?Closure $callback = null): bool ``` `CatchModel` 的批量删除方法,支持同时处理正常数据和软删除数据的批量删除操作。`$ids` 参数支持灵活的数据格式,可以是 PHP 数组或逗号分隔的字符串(如 `"1,2,3,4,5"`)。 `$callback` 参数可用于处理删除后的回调逻辑: ```php $ids = [1, 2, 3]; $this->deletesBy($ids, false, function($ids) { // $ids [1, 2, 3] // 这里处理删除后的逻辑 }); ``` #### 恢复数据 ```php public function restoreBy(array|string $ids): true ``` 配合 CatchAdmin 后台管理系统的数据回收站功能,用于恢复 `CatchModel` 软删除的数据记录。 ### 辅助功能方法 #### 排序功能 通过设置 `sortField` 属性可以指定默认排序字段: ```php protected string $sortField = 'sort'; ``` 默认使用倒序排序,如需正序排序可设置: ```php protected bool $sortDesc = false; ``` #### 状态切换 ```php public function toggleBy($id, string $field = 'status'): bool ``` 通过主键 ID 进行状态字段的切换,默认操作 `status` 字段。 #### 处理树状数据的子级更新 ```php public function updateChildren(mixed $parentId, string $field, mixed $value): void ``` 递归更新树形结构中指定父级下所有子级的字段值。 #### 字段别名处理 ```php public function aliasField(string|array $fields): string|array ``` 为字段添加表名前缀,避免联表查询时的字段冲突。 #### 创建人相关方法 ```php // 设置创建人 ID public function setCreatorId() // 获取创建人信息(Scope 查询) public function scopeCreator() ``` 使用创建人查询 Scope: ```php Model::select('*')->creator()->get(); ``` ::: info 使用条件 使用该查询需要数据表包含 `creator_id` 字段,否则无效果。 ::: #### 模糊查询 ```php public function whereLike($field, $value) ``` 提供便捷的模糊查询方法。 #### 快速搜索 ```php public function quickSearch(array $params = []) ``` 该方法通过模型的 `searchable` 属性配合请求参数进行动态搜索: ```php protected array $searchable = [ 'status' => '=', 'nickname' => 'like' ]; ``` 在 `CatchModel` 中配置 `searchable` 属性后,框架会自动生成相应的数据库查询条件: ```php Model::select('*') ->where('status', $request->get('status')) ->whereLike('nickname', $request->get('nickname')) ->get(); ``` `CatchModel` 的快速搜索支持数据库别名字段,特别适用于多表 `JOIN` 联查询场景。例如当 A 和 B 两张表都包含 `status` 字段时,可以精确指定查询目标 ```php // 在 CatchModel 中可以指定使用 A 表的 status 字段进行精确查询 protected array $searchable = [ 'A.status' => '=', ]; ``` ### CatchModel 数据库事务处理 `CatchModel` 在模型类中内置集成了 Laravel 数据库事务处理方法,无需额外引入 `DB` Facade: ```php // 开启事务 $this->beginTransaction(); // 提交事务 $this->commit(); // 回滚事务 $this->rollback(); // 事务闭包 $this->transaction(function() { // 事务内的操作 }); ``` ::: tip CatchModel 事务处理便捷性 相比传统的 `DB::beginTransaction()` 方式,`CatchModel` 直接在模型中调用事务方法更加便捷,提升了 PHP 开发效率。 ::: --- --- url: /front/catch-table.md --- # CatchTable 表格组件 - 高效动态数据表格解决方案 `CatchTable` 是一个专为 CatchAdmin Vue 应用设计的高性能表格组件,致力于简化后台管理系统中表格开发的复杂度。通过动态配置和模块化设计,CatchTable 能显著提升开发效率,提供完整的数据表格管理功能。 ## CatchTable 基础用法与快速上手 创建一个功能完整的数据表格只需简单配置即可实现。以下以用户管理页面为实例,详细演示 CatchTable 组件的基础使用方法 ![CatchAdmin 表格基础用法-laravel admin](https://image.catchadmin.com/202406130849406.png) 代码如下 ```javascript ``` ## CatchTable 按钮权限控制管理 CatchTable 提供完整的按钮权限管理功能。通过 `permission` 属性实现细粒度的权限控制,属性值格式为 `module.controller`(模块名.控制器名),请注意使用小写格式。以下是用户模块权限配置示例 ```javascript ``` ### CatchTable 内置操作方法 (Built-in Actions) CatchTable 预定义了完整的 CRUD 操作方法,每个操作都对应后端控制器的特定方法: * **export** - 数据导出功能,调用控制器的 `export` 方法 * **import** - 数据导入功能,调用控制器的 `import` 方法 * **store** - 数据新增功能,调用控制器的 `store` 方法 * **update** - 数据编辑功能,调用控制器的 `update` 方法 * **destroy** - 数据删除功能,调用控制器的 `destroy` 方法 * **restore** - 数据恢复功能,调用控制器的 `restore` 方法 ## CatchTable 高级搜索功能配置 ![CatchAdmin 表格搜索功能-laravel admin](https://image.catchadmin.com/202406130849989.png) CatchTable 提供强大的搜索功能,通过配置 `search-form` 属性即可实现多条件组合查询。支持多种表单控件类型,满足各种业务场景的搜索需求 ```javascript ``` ok,这样一个完整的表格页面就创建完成了。 ### search 组件支持以下表单组件 * `input` * `select` * `input-number` * `date` * `datetime` * `range` * `tree` * `remote-api-select` * `remote-select` #### Tree 组件特殊配置说明 `tree` 组件是基于下拉选择器的树形结构控件,专门用于处理具有层级关系的数据选择。由于 tree 组件通常需要从后端 API 动态获取数据,因此必须配置响应式数据源。以下是 tree 组件的标准配置方法: ```ts // 首先在 setup 中设置 search 相关组件 // 在 catch table 种设置 search 即可 ``` #### 远程下拉 使用远程接口下拉组件,可以获取后台任何带有分页接口数据。如果没有分页,请使用 `select` 组件 ```ts ``` #### 范围搜索 这里使用时间范围选择,接口会使用 `start_at` 和 `end_at` 两个搜索参数 ```ts ``` ## 获取当前列表的搜索参数数据 ```ts import { useQueryStore } from '@/components/catchTable/useQueryStore' const queryStore = useQueryStore() // 这样你就可以获取当前列表的搜索参数数据了 console.log(queryStore.getTableQuery) ``` ## CatchTable 数据新增功能实现 CatchTable 提供完整的 CRUD(增删改查)操作支持。数据新增功能通过 Vue 插槽(slot)机制实现,允许开发者自定义新增表单的界面和逻辑。以下演示如何在 CatchTable 中集成数据新增功能: ```javascript ``` 这里需要注意两点的是,一般情况下 Create 组件都是由代码自动生成功能生成的 * `Create` 组件是自带 `primary` props 的,用于更新 * `Create` 组件是自带 `api` props 的,api 主要用于接口提交 ### 弹窗关闭保持搜索条件 目前 CatchTable 内置了弹窗关闭保持搜索条件的功能,有时候翻页,例如翻页到第 10 页,修改数据,保存之后。会重置分页数据。如果需要保持搜索条件,可以 在 Create 组件中找到 `close` 方法。 ```javascript const closeDialog = inject('closeDialog') as Function onMounted(() => { close(() => closeDialog(false)) // 设置为 false,就不会重置分页数据了 }) ``` ## CatchTable 回收站功能配置 CatchTable 内置数据回收站功能,支持软删除和数据恢复机制。启用回收站功能只需简单的配置即可: ```javascript ``` ### 数据恢复 恢复功能需要添加一条路由才可以正常运行,例如用户管理(`UserController`)添加一条 `restore` 路由 ```php // 回收站恢复 Route::put('users/restore/{id}', [UserController::class, 'restore']); ``` :::warning 回收站数据的删除是强制删除,删除后数据将不可恢复 ::: ## 隐藏分页 一般列表都是需要分页的,但是某种场景下,需要隐藏分页的话,可以使用下面的代码 ```javascript ``` ## 树形表格 要使用树形表格,在 `catch-table` 中也是非常简单的,只需要 ```javascript ``` > {info} > 注意在 `catchtable` 中,树形表格都是自动隐藏分页的 ## 空数据显示的文本 如果表格没有数据,需要友好的提示的话,那么可以使用下面的代码,默认使用`暂无数据` ```javascript ``` ## 隐藏操作 表格默认一个新增操作,如果不需要的话,可以使用 ```javascript ``` ## 隐藏表头 ```javascript ``` ## 隐藏工具栏 在表格右上角,有三个默认工具栏操作,分别是 `刷新`,`表格栏目`, `搜索`, 如果不需要的话,可以使用 ```javascript ``` ## 隐藏多选删除 ```javascript ``` ## 默认参数 有这么一个场景,例如后台的字典管理,每个字典都需要管理字典值。而每个字典值列表则需要字典的 ID。这个时候 每个请求列表的 api 都是需要默认参数 `字典ID`。这个时候就需要添加默认参数 ```javascript ``` ## 搜索设置默认参数 如果需要给搜索框设置某一个默认值,可以使用 `default` 属性,这样每次进入列表页面,都可以自带一个默认搜索参数了 ```javascript ``` ## 默认选中 有时候表格需要默认选中一些数据,我们可以使用 ```javascript ``` :::tip 目前使用表格数据的主键数据作为选中依据 ::: ## Table 曝露方法 有一些需求可能需要直接操作表格的方法。目前表格对外有几个可以直接调用的方法,调用方法之前需要先设置 `table ref`,在获取整个`catchtable`对象 ref 之后,才可以使用 ```vue // js 代码 // ⚠️如果你对 vue 不熟悉的话,注意 ref="catchadmin" 这里 ref 的名称需要和 const [catchtable] 相同 ``` ### 搜索 在某些操作之后,需要搜索刷新列表 ```vue ``` ### 重置 在某些操作之后,需要重置列表,也可以叫做刷新吧 ```vue ``` ### 打开弹出层 ```vue ``` ### 关闭弹出层 ```vue ``` ### 删除 某些场景需要访问删除接口时候,就可以使用它 ```vue ``` ### 设置默认搜索参数 这个方法在某些特定场景下会有用到,比如一个表格列表的访问他的子列表,子列表需要父列表的某个条件才能访问到。这个时候就需要给子列表设置一个默认参数。`字典管理`列表就是一个很好的例子 ```vue ``` ### 获取表格多选 ID 目前 `catchadmin` 已经内置了多选删除。如果需要做其他多选操作的时候,可以使用它获取多选数据 ```vue ``` ## 表格插槽 为了让表格更加灵活点,`catchtable` 内置了几个插槽,来让用户自定义操作 ### 表格操作插槽 `catchtable` 默认只有新增操作,如果你需要添加其他的操作,那么你可以使用以下代码,新增表格的操作 ```vue ``` ![CatchAdmin 表格操作插槽-laravel admin](https://image.catchadmin.com/202406130851032.png) ### 搜索和表格之间插槽 `catchtable` 提供了搜索和表格之间的插槽,可以用于自定义搜索和表格之间的内容 ```vue ``` ### 批量操作插槽 目前表格内置了`批量删除`操作。当表格需要额外的批量操作时,可以使用该插槽。 ```vue ``` 光是这样的是不够的,还要获取批量选择 ID,请查看[获取表格多选 ID](#获取表格多选id),获取到多选 ID 之后进行操作 ![CatchAdmin 批量操作插槽-laravel admin](https://image.catchadmin.com/202406130852219.png) ### 栏目操作插槽 表格栏目支持`更新`和`删除`操作,如果还需要额外的操作,那么可以使用 ```vue // 通过 scope 你可以获取行数据 ``` ![CatchAdmin 栏目操作插槽-laravel admin](https://image.catchadmin.com/202406130852588.png) ### 弹窗插槽 弹窗插槽是每个表格都需要的,目前只服务于表单数据。 ```vue ``` ## 表格栏目 对于表格栏目,可以通过表格类型窥探一二。看下表格栏目是如何定义的 ```js export type columnType = 'expand' | 'selection' | 'index' | 'operate' export type fixed = 'fiexed' | 'right' | 'left' export interface Column { type?: columnType // 类型 expand select index label?: string prop?: string 'min-width'?: string | number width?: number | string slot?: 'string' header: 'string' // 表头插槽名称 align?: string fixed?: fixed sortable?: boolean | string 'sort-method'?: Function 'sort-by'?: Function resizable?: boolean 'header-align'?: string 'class-name'?: string selectable?: Function // function(row, index) show: boolean index?: number | Function // 如果设置了 type=index,可以通过传递 index 属性来自定义索引 children?: Array // 多级表头 filter?:Function, ellipsis?:boolean|number, // 当文字太多时,可以使用省略文字 switch: false, // swith 字段状态切换 // 操作 update?: boolean, // 编辑操作 destroy?: boolean // 删除操作 } ``` ### 栏目类型 `type` 字段 * `expand` 展开类型,树形结构的表格,规定哪个栏目展开 * `selection` 多选类型,一般用于表格多选操作。一般都是用于主键字段 * `index` 可以自定义索引 * `operate` 最后一行操作栏目 ### 栏目固定 `fiexed` 字段 * fixed 默认固定 * right 固定在右侧 * left 固定在左侧 ### 插槽 如果栏目是需要自定义,那么肯定是需要用插槽这个功能。只需要设置 `slot` 字段,例如插槽名称设置为 ```javascript { label: '你好', slot: 'hello' } ``` 那么此时只需要在`catchtable`组件如下设置 ```vue ``` ### 格式化字段 有时候并不需要插槽,例如当后台的接口中的性别字段(gender)返回 1, 2。其中 1 代表男 2 代表女,这个时候需要实现格式化方法即可 ```js { label: '性别', prop: 'gender', filter: (value) => { return value === 1 ? '男' : '女' } } ``` ### 自定义索引 当栏目的 type 设置成 `index` 时,则需要自定义索引,一般通过 `index` 来设置 ```js { type: 'index', prop: 'gender', index: () => {} } ``` ### 多级表头 当然,catchtable 也支持多集表头,只要一个简单的配置即可 ```js { prop: 'job_name', label: '岗位名称', children: [ { prop: 'coding', label: '岗位编码' }, { label: '状态', prop: 'status', switch: true, align: 'center' } ] } ``` ![CatchAdmin 多级表头-laravel admin](https://image.catchadmin.com/202406130854407.png) ### 字段太长,省略号 ```js { prop: 'description', label: '岗位描述', ellipsis: true // 添加该字段 }, ``` ### 字段状态切换 某些场景下,业务中只需要在表格中做某些字段的状态切换,这个时候就可以使用下面的代码 ```javascript { prop: 'status', // 设置字段,这里仅做演示 label: '状态', switch: true // 添加该字段 }, ``` `catchadmin`在后端通常使用 `enable` 方法做字段切换的路由, 你可以根据实际改动。代码如下 ```php public function enable($id, Request $request) { return $this->model->toggleBy($id, $request->get('field')); } ``` ![CatchAdmin 字段状态切换-laravel admin](https://image.catchadmin.com/202406130855026.png) ### 图片预览 如果表格中需要进行图片预览,那么可以使用下面的配置,只需要使用 `image` 属性 ```js { label: '内容', prop: 'content', image: true, }, ``` 如果你是想要预览,如果是单图的话,你需要使用 `filter` 转换成多图数组 ```js { label: '内容', prop: 'content', image: true, preview: true, filter: (value: any) => { return [value] } }, ``` ### 链接 如果表格中需要某个字段需要链接,那么可以使用下面的配置, 也是非常简单,只需要配置 `link` 属性 ```js { label: '链接', prop: 'url', link: true }, ``` ### 标签展示 如果表格中需要某个字段需要标签,那么可以使用下面的配置, 也是非常简单,只需要配置 `tags` 属性,单个标签 ```js { label: '类型', prop: 'type', tags: true }, ``` 多个标签一般都是配合枚举值,`catchadmin` 枚举一般使用 number,并且从数字`1`开始,数组可以很好配合使用 ```js { label: '类型', prop: 'type', tags: ['danger', 'info', 'success'], filter: (value: number) => { return value === 1 ? '轮播图' : value === 2 ? '友情链接' : '广告' } }, ``` ### 排序 某些场景下,业务中只需要在表格中做某个字段排序。通常来说,elementPlus 只是在前端列表单独一页排序,但是使用下面的代码,可以直接进行后端排序,不需要写任何一行代码,都是自动完成的 ```js { prop: 'sort', label: '排序', sortable: true } ``` ![CatchAdmin 表格排序-laravel admin](https://image.catchadmin.com/202406130856803.png) ### Mask 数据 某些时候,希望数据默认是以 `******` 这样的形式展示,那么你需要这么设置 ```js { prop: 'secret', label: '密钥', mask: true } ``` ![CatchAdmin 表格 Mask 数据-laravel admin](https://image.catchadmin.com/202410261739969.png) --- --- url: /front/http.md --- # HTTP 请求 CatchAdmin 在前端也提供了一个简便的 HTTP 客户端类,这是使用 `axios` 二次包装而成。CatchAdmin 前端所有的接口请求都是通过他。 ## 支持的四种请求方式 ### GET 请求 ```js import http from '@/support/http' http.get('some/url', { a: '' }).then((response) => {}) ``` ### POST 请求 ```js import http from '@/support/http' http.post('some/url', { a: '' }).then((response) => {}) ``` ### PUT 请求 ```js import http from '@/support/http' http.put('some/url', { a: '' }).then((response) => {}) ``` ### DELETE 请求 ```js import http from '@/support/http' http.delete('some/url', { a: '' }).then((response) => {}) ``` ## 设置超时 ```js import http from '@/support/http' http .timeout(10) .get('some/url', { a: '' }) .then((response) => {}) ``` ## 设置头信息 ```js import http from '@/support/http' http .setHeader('Request-From', 'DashBoard') .setHeader('Other', 'Other') .get('some/url', { a: '' }) .then((response) => {}) ``` ## 设置响应类型 ```js import http from '@/support/http' http // 设置 blob 类型 .setResponseType('blob') .get('some/url', { a: '' }) .then((response) => {}) ``` --- --- url: /server/route.md --- # Laravel 路由扩展 :::warning 该功能在核心包 `catchadmin/pro` >= 0.5.4 版本提供 ::: ## 介绍 在后台开发中,处理表格(`table`)是常见的任务。基本的单页表格通常包括列表、数据新增、数据编辑以及行删除这四个主要操作。为此,`Laravel` 提供了一个便捷的 `resource` 路由方法,可以一次性注册相关路由。例如,针对用户管理,我们可以这样定义路由: ```php // CatchAdmin 专业版采用 API 接口开发模式,使用 apiResource 注册路由 Route::apiResource('users', UsesController::class); ``` 此方法自动生成符合 RESTful 规范的五条 API 路由: * `GET` `users` - 显示列表数据 -> `index` 方法 * `POST` `users` - 新增数据 -> `store` 方法 * `PUT` `users/{id}` - 更新数据 -> `update` 方法 * `GET` `users/{id}` - 请求单条数据 -> `show` 方法 * `DELETE` `users/{id}` - 删除数据 -> `destroy` 方法 这样的设计确实很方便,不过如果某个方法不再需要,比如我们不希望保留删除路由,该如何处理呢?这时可以通过 `except` 方法排除掉不需要的路由: ```php Route::apiResource('users', UsesController::class)->except(['destroy']); ``` 后台管理系统中,数据导入导出是常见需求,传统做法需要手动添加额外路由: ```php Route::apiResource('users', UsesController::class)->except(['destroy']); // 手动添加数据导入路由 Route::post('users/import', [UsesController::class, 'import']); ``` 频繁修改路由文件确实不够高效,CatchAdmin 专业版提供了更智能的路由管理解决方案。 ## adminResource CatchAdmin 专业版提供了 `adminResource` 方法来解决这个问题,它是对 Laravel `apiResource` 的智能化扩展。使用方式如下: ```php // 使用 adminResource 智能注册后台管理路由 Route::adminResource('users', UsesController::class); ``` 使用 `adminResource` 后,除了标准的 RESTful 路由外,还自动注册以下后台管理专用路由: * `PUT` `users/enable/{id}` - 状态更改 -> `enable` 方法 * `GET` `export/users` - 导出数据 -> `export` 方法 * `POST` `import/users` - 导入数据 -> `import` 方法 * `PUT` `users/restore/{id}` - 恢复数据 -> `restore` 方法 * `GET` `form/users` - 动态表单 -> `form` 方法 * `GET` `table/users` - 动态表格 -> `table` 方法 CatchAdmin 专业版还对路由注册机制进行了智能化优化。与 Laravel 原生 `apiResource` 不同,后者无论控制器是否包含对应方法都会注册全部五条路由。例如 `UsesController` 仅有 `index` 方法时,仍需要通过 `except` 方法手动排除不需要的路由。 `adminResource` 则采用智能检测机制:只有当控制器中存在对应的 `public` 方法时,才会注册相应路由。这种设计实现了真正的自动化路由管理,生成的路由表更加精简高效。 :::warning 生产环境部署时,建议执行 `php artisan route:cache` 命令缓存路由配置以提升性能。 ::: --- --- url: /server/openapi.md --- # OpenAPI 开放接口 `OpenAPI` 模块为平台提供了强大的接口开放能力。当您需要将后台接口暴露给外部系统或第三方开发者使用,但又不希望为每个接入方创建独立用户账号时,这个模块可以完美地解决这一需求。 ::: tip 核心优势 * 🔐 安全的签名验证机制 * 🚀 灵活的 QPS 限流控制 * 📊 完整的请求日志记录 * 💰 支持余额扣费模式 ::: ## 主要特性 * **签名验证**:基于 HMAC-SHA256 的安全签名机制,确保接口调用的安全性 * **访问控制**:支持按用户设置 QPS 限制,有效控制接口访问频率 * **日志记录**:详细记录每次接口调用,便于监控和问题排查 * **余额管理**:支持预付费模式,可按调用次数或其他维度扣费 ## 数据库结构 OpenAPI 模块使用以下三个核心数据表来管理用户信息、余额和请求日志: | 表名 | 描述 | 主要用途 | | ---------------------- | -------------- | ---------------------------------- | | `openapi_users` | OpenAPI 用户表 | 存储第三方用户的基本信息和认证密钥 | | `openapi_user_balance` | 用户余额表 | 管理用户的可用余额和扣费记录 | | `openapi_request_log` | 请求日志表 | 记录所有 API 调用的详细信息 | ```php Schema::create('openapi_users', function (Blueprint $table) { $table->id()->comment('ID'); $table->string('username')->comment('用户名'); $table->string('mobile', 20)->comment('手机号'); $table->string('password')->comment('密码'); $table->string('company')->nullable()->comment('公司名称'); $table->string('description')->nullable()->comment('描述'); $table->unsignedInteger('qps')->default(100)->comment('每分钟的 QPS'); $table->string('app_key')->comment('app key'); $table->string('app_secret')->comment('密钥'); $table->creatorId(); $table->createdAt(); $table->updatedAt(); $table->deletedAt(); $table->engine = 'InnoDB'; $table->comment('openapi 用户表'); }); Schema::create('openapi_user_balance', function (Blueprint $table) { $table->id(); $table->uuid(); $table->integer('user_id')->comment('用户id'); $table->unsignedInteger('balance')->default(0)->comment('用户余额'); $table->createdAt(); $table->updatedAt(); $table->deletedAt(); $table->engine = 'InnoDB'; $table->comment('用户余额表'); }); Schema::create('openapi_request_log', function (Blueprint $table) { $table->id(); $table->uuid('request_id')->comment('请求id'); $table->string('app_key')->comment('app key'); $table->json('data')->comment('请求数据'); $table->createdAt(); $table->updatedAt(); $table->engine = 'InnoDB'; $table->comment('openapi 请求日志'); }); ``` ## 创建 OpenAPI 用户 ### 通过管理后台创建 在管理后台中找到 OpenAPI 模块,通过可视化界面创建新的第三方用户: ![catchadmin 专业版-openapi 模块](https://image.catchadmin.com/202410201728384.png) ### 关键配置说明 ::: warning QPS 限制说明 `QPS` 字段用于限制访问接口的频率,当前版本按**每分钟**计算。例如设置为 100,表示该用户每分钟最多可调用 100 次接口。 ::: ### 认证密钥 用户创建成功后,系统会自动分配一对认证密钥: * **AppKey**:公开的应用标识符,用于识别调用方 * **AppSecret**:私密的签名密钥,用于生成请求签名,请妥善保管 ::: tip 安全提醒 * `AppSecret` 仅在创建时显示一次,请及时记录保存 * 建议定期更换密钥以提高安全性 * 切勿在客户端代码中硬编码 `AppSecret` ::: ## 快速接入 ### 路由配置 使用 OpenAPI 功能非常简单,只需在路由中添加相应的中间件即可。在项目根目录的 `routes/api.php` 文件中添加以下代码: ```php{2-3} Route::prefix('v1')->middleware([ \Modules\Openapi\Middlewares\CheckSignatureMiddleware::class, \Modules\Openapi\Middlewares\RateLimiterMiddleware::class // 可选:添加限流中间件 ])->group(function () { Route::get('user', function (){ return \Modules\Openapi\Facade\OpenapiResponse::success([ 'message' => 'Hello OpenAPI!', 'timestamp' => time() ]); }); // 更多 API 路由... }); ``` ### 验证接入 配置完成后,在浏览器中访问 `域名/api/v1/user`,如果看到以下响应,说明接入成功: ![catchadmin专业版-openapi](https://image.catchadmin.com/202410201733998.png) ::: info 中间件说明 * `CheckSignatureMiddleware`:必需,用于验证请求签名 * `RateLimiterMiddleware`:可选,用于限制请求频率 ::: ### 响应格式 所有 OpenAPI 接口都应使用统一的响应格式: ```php // 成功响应 return \Modules\Openapi\Facade\OpenapiResponse::success($data, $message); // 错误响应 return \Modules\Openapi\Facade\OpenapiResponse::error($message, $code); ``` ## 接口调用示例 ### 签名算法说明 OpenAPI 使用 HMAC-SHA256 算法对请求参数进行签名验证,确保接口调用的安全性。签名流程如下: 1. **收集参数**:获取所有请求参数(包括 GET 查询参数或 POST 表单参数) 2. **添加时间戳**:添加 `timestamp` 参数(Unix 时间戳) 3. **数组扁平化**:对嵌套数组和对象进行扁平化处理 * 对象:`user.name` -> `user.name` * 数组:`items[0].name` -> `items[0].name` 4. **参数排序**:按参数名进行字典序排序 5. **构建签名串**:将参数按 `key=value&key=value` 格式拼接 6. **生成签名**:使用 `AppSecret` 对签名串进行 HMAC-SHA256 加密 7. **发送请求**:在请求头中添加 `app-key` 和 `signature` ::: warning 重要提醒 参数扁平化是签名验证的关键步骤,必须与服务端逻辑保持完全一致,否则会导致签名验证失败。 ::: ### 扁平化示例 假设有以下复杂参数: ```json { "user": { "name": "张三", "age": 25 }, "items": [ { "id": 1, "name": "商品A" }, { "id": 2, "name": "商品B" } ], "timestamp": 1640995200 } ``` 扁平化后的参数: ```json { "items[0].id": "1", "items[0].name": "商品A", "items[1].id": "2", "items[1].name": "商品B", "timestamp": "1640995200", "user.age": "25", "user.name": "张三" } ``` 最终签名字符串: ``` items[0].id=1&items[0].name=商品A&items[1].id=2&items[1].name=商品B×tamp=1640995200&user.age=25&user.name=张三 ``` ### 多语言调用示例 以下提供了多种编程语言的调用示例,您可以根据项目需求选择合适的实现方式: ::: code-group ```javascript [JavaScript/Node.js] const crypto = require('crypto') const axios = require('axios') class OpenAPIClient { constructor(appKey, appSecret, baseURL) { this.appKey = appKey this.appSecret = appSecret this.baseURL = baseURL } // 扁平化数组 flattenArray(obj, prefix = '') { let result = {} for (let key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key] const newKey = prefix ? `${prefix}.${key}` : key if (Array.isArray(value)) { // 处理数组 value.forEach((item, index) => { if (typeof item === 'object' && item !== null) { Object.assign(result, this.flattenArray(item, `${newKey}[${index}]`)) } else { result[`${newKey}[${index}]`] = item } }) } else if (typeof value === 'object' && value !== null) { // 处理对象 Object.assign(result, this.flattenArray(value, newKey)) } else { result[newKey] = value } } } return result } // 创建签名 createSignature(params) { // 扁平化参数 const flattenedParams = this.flattenArray(params) // 按键名排序 const keys = Object.keys(flattenedParams).sort() // 构建签名字符串 const signStr = keys.map((key) => `${key}=${flattenedParams[key]}`).join('&') return crypto.createHmac('sha256', this.appSecret).update(signStr).digest('hex') } // GET 请求 async get(url, params = {}) { params.timestamp = Math.floor(Date.now() / 1000) const signature = this.createSignature(params) const response = await axios.get(`${this.baseURL}${url}`, { params, headers: { 'app-key': this.appKey, signature: signature } }) return response.data } // POST 请求 async post(url, data = {}) { data.timestamp = Math.floor(Date.now() / 1000) const signature = this.createSignature(data) const response = await axios.post(`${this.baseURL}${url}`, data, { headers: { 'app-key': this.appKey, signature: signature, 'Content-Type': 'application/x-www-form-urlencoded' } }) return response.data } } // 使用示例 const client = new OpenAPIClient('your_app_key', 'your_app_secret', 'https://your-domain.com/api') // GET 请求示例 client .get('/v1/user', { page: 1, limit: 10 }) .then((result) => console.log(result)) .catch((error) => console.error(error)) // POST 请求示例 client .post('/v1/user', { name: 'John', email: 'john@example.com' }) .then((result) => console.log(result)) .catch((error) => console.error(error)) // 复杂参数示例 client .post('/v1/user', { user: { name: '张三', age: 25 }, items: [ { id: 1, name: '商品A' }, { id: 2, name: '商品B' } ] }) .then((result) => console.log(result)) .catch((error) => console.error(error)) ``` ```php [PHP] appKey = $appKey; $this->appSecret = $appSecret; $this->baseURL = rtrim($baseURL, '/'); } /** * 扁平化数组(与服务端逻辑保持一致) */ private function flattenArray($array, $prefix = '') { $result = []; foreach ($array as $key => $value) { $newKey = $prefix ? $prefix . '.' . $key : $key; if (is_array($value)) { if ($this->isAssociativeArray($value)) { // 关联数组 $result = array_merge($result, $this->flattenArray($value, $newKey)); } else { // 索引数组 foreach ($value as $index => $item) { if (is_array($item)) { $result = array_merge($result, $this->flattenArray($item, $newKey . '[' . $index . ']')); } else { $result[$newKey . '[' . $index . ']'] = $item; } } } } else { $result[$newKey] = $value; } } return $result; } /** * 判断是否为关联数组 */ private function isAssociativeArray($array) { if (empty($array)) { return false; } return array_keys($array) !== range(0, count($array) - 1); } /** * 创建签名(与服务端逻辑完全一致) */ private function createSignature($params) { // 扁平化参数 $flattenedParams = $this->flattenArray($params); // 按键名排序 ksort($flattenedParams); // 构建签名字符串 $signStr = ''; foreach ($flattenedParams as $key => $value) { $signStr .= $key . '=' . $value . '&'; } // 去除末尾的 & 符号 $signStr = rtrim($signStr, '&'); return hash_hmac('sha256', $signStr, $this->appSecret); } /** * GET 请求 */ public function get($url, $params = []) { $params['timestamp'] = time(); $signature = $this->createSignature($params); $queryString = http_build_query($params); $fullUrl = $this->baseURL . $url . '?' . $queryString; $headers = [ 'app-key: ' . $this->appKey, 'signature: ' . $signature ]; return $this->makeRequest($fullUrl, 'GET', $headers); } /** * POST 请求 */ public function post($url, $data = []) { $data['timestamp'] = time(); $signature = $this->createSignature($data); $headers = [ 'app-key: ' . $this->appKey, 'signature: ' . $signature, 'Content-Type: application/x-www-form-urlencoded' ]; $fullUrl = $this->baseURL . $url; return $this->makeRequest($fullUrl, 'POST', $headers, http_build_query($data)); } /** * 执行 HTTP 请求 */ private function makeRequest($url, $method, $headers, $data = null) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30, ]); if ($data && $method === 'POST') { curl_setopt($ch, CURLOPT_POSTFIELDS, $data); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { throw new Exception("HTTP Error: " . $httpCode); } return json_decode($response, true); } } // 使用示例 $client = new OpenAPIClient('your_app_key', 'your_app_secret', 'https://your-domain.com/api'); try { // GET 请求示例 $result = $client->get('/v1/user', ['page' => 1, 'limit' => 10]); echo "GET Result: " . json_encode($result) . "\n"; // POST 请求示例 $result = $client->post('/v1/user', ['name' => 'John', 'email' => 'john@example.com']); echo "POST Result: " . json_encode($result) . "\n"; // 复杂参数示例 $complexParams = [ 'user' => [ 'name' => '张三', 'age' => 25 ], 'items' => [ ['id' => 1, 'name' => '商品A'], ['id' => 2, 'name' => '商品B'] ] ]; $result = $client->post('/v1/user', $complexParams); echo "Complex Result: " . json_encode($result) . "\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ``` ```python [Python] import hashlib import hmac import time import requests from urllib.parse import urlencode from typing import Dict, Any, Optional class OpenAPIClient: def __init__(self, app_key: str, app_secret: str, base_url: str): self.app_key = app_key self.app_secret = app_secret self.base_url = base_url.rstrip('/') def _flatten_array(self, obj: Dict[str, Any], prefix: str = '') -> Dict[str, Any]: """扁平化数组(与服务端逻辑保持一致)""" result = {} for key, value in obj.items(): new_key = f"{prefix}.{key}" if prefix else key if isinstance(value, list): # 处理数组 for index, item in enumerate(value): if isinstance(item, dict): result.update(self._flatten_array(item, f"{new_key}[{index}]")) else: result[f"{new_key}[{index}]"] = item elif isinstance(value, dict): # 处理对象 result.update(self._flatten_array(value, new_key)) else: result[new_key] = value return result def _create_signature(self, params: Dict[str, Any]) -> str: """创建签名(与服务端逻辑完全一致)""" # 扁平化参数 flattened_params = self._flatten_array(params) # 按键名排序 sorted_params = sorted(flattened_params.items()) # 构建签名字符串 sign_str = '&'.join([f"{key}={value}" for key, value in sorted_params]) # 使用 HMAC-SHA256 生成签名 signature = hmac.new( self.app_secret.encode('utf-8'), sign_str.encode('utf-8'), hashlib.sha256 ).hexdigest() return signature def get(self, url: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """GET 请求""" if params is None: params = {} # 添加时间戳 params['timestamp'] = int(time.time()) # 生成签名 signature = self._create_signature(params) # 构建请求头 headers = { 'app-key': self.app_key, 'signature': signature } # 发送请求 full_url = f"{self.base_url}{url}" response = requests.get(full_url, params=params, headers=headers, timeout=30) response.raise_for_status() return response.json() def post(self, url: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """POST 请求""" if data is None: data = {} # 添加时间戳 data['timestamp'] = int(time.time()) # 生成签名 signature = self._create_signature(data) # 构建请求头 headers = { 'app-key': self.app_key, 'signature': signature, 'Content-Type': 'application/x-www-form-urlencoded' } # 发送请求 full_url = f"{self.base_url}{url}" response = requests.post(full_url, data=data, headers=headers, timeout=30) response.raise_for_status() return response.json() # 使用示例 if __name__ == "__main__": client = OpenAPIClient( app_key='your_app_key', app_secret='your_app_secret', base_url='https://your-domain.com/api' ) try: # GET 请求示例 result = client.get('/v1/user', {'page': 1, 'limit': 10}) print(f"GET Result: {result}") # POST 请求示例 result = client.post('/v1/user', {'name': 'John', 'email': 'john@example.com'}) print(f"POST Result: {result}") except requests.exceptions.RequestException as e: print(f"Request Error: {e}") except Exception as e: print(f"Error: {e}") ``` ```bash [cURL] #!/bin/bash # 配置参数 APP_KEY="your_app_key" APP_SECRET="your_app_secret" BASE_URL="https://your-domain.com/api" # 创建签名函数(与服务端逻辑保持一致) create_signature() { local params="$1" # 注意:cURL 脚本中的参数需要手动扁平化和排序 # 对于复杂参数,建议使用其他语言的客户端 echo -n "$params" | openssl dgst -sha256 -hmac "$APP_SECRET" -hex | sed 's/^.* //' } # GET 请求示例 echo "=== GET 请求示例 ===" TIMESTAMP=$(date +%s) PARAMS="page=1&limit=10×tamp=$TIMESTAMP" SIGNATURE=$(create_signature "$PARAMS") curl -X GET \ "$BASE_URL/v1/user?$PARAMS" \ -H "app-key: $APP_KEY" \ -H "signature: $SIGNATURE" \ -H "Content-Type: application/json" echo -e "\n" # POST 请求示例 echo "=== POST 请求示例 ===" TIMESTAMP=$(date +%s) POST_DATA="name=John&email=john@example.com×tamp=$TIMESTAMP" SIGNATURE=$(create_signature "$POST_DATA") curl -X POST \ "$BASE_URL/v1/user" \ -H "app-key: $APP_KEY" \ -H "signature: $SIGNATURE" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "$POST_DATA" echo -e "\n" # 使用 jq 格式化 JSON 响应(需要安装 jq) echo "=== 带格式化输出的请求 ===" TIMESTAMP=$(date +%s) PARAMS="timestamp=$TIMESTAMP" SIGNATURE=$(create_signature "$PARAMS") curl -s -X GET \ "$BASE_URL/v1/user?$PARAMS" \ -H "app-key: $APP_KEY" \ -H "signature: $SIGNATURE" \ | jq '.' ``` ::: ### ApiFox 前置脚本 如果您使用 ApiFox 进行接口测试,可以添加以下前置脚本来自动处理签名: ![catchadmin专业版-openapi](https://image.catchadmin.com/202410201737905.png) ```javascript{1-2} // 在 ApiFox 环境变量中配置 app_key 和 app_secret const appKey = pm.environment.get('app_key'); const appSecret = pm.environment.get('app_secret'); // 扁平化数组函数(与服务端逻辑保持一致) function flattenArray(obj, prefix = '') { let result = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key]; const newKey = prefix ? `${prefix}.${key}` : key; if (Array.isArray(value)) { // 处理数组 value.forEach((item, index) => { if (typeof item === 'object' && item !== null) { Object.assign(result, flattenArray(item, `${newKey}[${index}]`)); } else { result[`${newKey}[${index}]`] = item; } }); } else if (typeof value === 'object' && value !== null) { // 处理对象 Object.assign(result, flattenArray(value, newKey)); } else { result[newKey] = value; } } } return result; } function createSign(params) { // 扁平化参数 const flattenedParams = flattenArray(params); // 按键名排序 const keys = Object.keys(flattenedParams).sort(); // 构建签名字符串 const signStr = keys.map((key) => `${key}=${flattenedParams[key]}`).join('&'); return CryptoJS.HmacSHA256(signStr, appSecret).toString(); } let params = {}; params['timestamp'] = Math.floor(Date.now() / 1000); if (pm.request.method == 'GET') { // 处理 GET 请求参数 const queryString = pm.request.url.getQueryString(); if (queryString) { pm.request.url .getQueryString() .split('&') .forEach((item) => { const items = item.split('='); params[items[0]] = items[1]; }); } const signature = createSign(params); // 设置请求头 pm.request.headers.add({ key: 'app-key', value: appKey }); pm.request.headers.add({ key: 'signature', value: signature }); // 更新查询参数 let query = ''; for (let key in params) { query += key + '=' + params[key] + '&'; } pm.request.url.query = query.slice(0, -1); // 去除末尾的 & } else if (pm.request.method == 'POST') { // 处理 POST 请求参数 if (pm.request.body.mode == 'urlencoded') { pm.request.body.urlencoded.each((item) => { params[item.key] = item.value; }); const signature = createSign(params); // 设置请求头 pm.request.headers.add({ key: 'app-key', value: appKey }); pm.request.headers.add({ key: 'signature', value: signature }); // 添加时间戳参数 pm.request.body.urlencoded.add({ disabled: false, key: 'timestamp', value: params.timestamp }); } // 支持 form-data 格式 if (pm.request.body.mode == 'formdata') { pm.request.body.formdata.each((item) => { params[item.key] = item.value; }); const signature = createSign(params); pm.request.headers.add({ key: 'app-key', value: appKey }); pm.request.headers.add({ key: 'signature', value: signature }); pm.request.body.formdata.add({ key: 'timestamp', value: params.timestamp }); } } ``` ### 环境配置 在 ApiFox 中配置环境变量,设置您的 `app_key` 和 `app_secret`: ![catchadmin专业版-openapi](https://image.catchadmin.com/202410201739500.png) ### 测试验证 配置完成后,通过 ApiFox 发送请求,如果出现以下结果,说明接入成功: ![catchadmin专业版-openapi](https://image.catchadmin.com/202410201740762.png) ::: tip 测试建议 * 先使用简单的 GET 请求进行测试 * 确认时间戳生成和签名算法正确 * 检查请求头是否正确设置 * 验证参数排序逻辑 ::: ## 中间件配置 OpenAPI 模块提供了两个核心中间件来确保接口的安全性和稳定性: ### 可用中间件 | 中间件 | 类名 | 功能描述 | 是否必需 | | -------- | -------------------------- | ----------------------- | -------- | | 签名验证 | `CheckSignatureMiddleware` | 验证请求签名的有效性 | ✅ 必需 | | 速率限制 | `RateLimiterMiddleware` | 控制用户请求频率(QPS) | 🔄 可选 | ### 使用方式 ```php{2-3} Route::prefix('v1')->middleware([ \Modules\Openapi\Middlewares\CheckSignatureMiddleware::class, // 必需 \Modules\Openapi\Middlewares\RateLimiterMiddleware::class // 可选 ])->group(function () { // 您的 API 路由 }); ``` ::: warning 注意事项 * `CheckSignatureMiddleware` 是必需的,用于保证接口安全 * `RateLimiterMiddleware` 建议在生产环境中使用,防止接口滥用 * 中间件的顺序很重要:签名验证应该在速率限制之前 ::: ## 异常处理 ### 自定义异常 OpenAPI 模块的所有异常都需要继承 `OpenapiException` 基类。这样可以确保异常处理的一致性: ```php{3,7} namespace Modules\Openapi\Exceptions; use Modules\Openapi\Enums\Code; // 示例:不合法的 AppKey 异常 class InvalidAppKeyException extends OpenapiException { protected $code = Code::INVALID_APP_KEY; public function __construct($message = null) { parent::__construct($message ?: '无效的 AppKey'); } } ``` ### 异常渲染配置 在 `bootstrap/app.php` 文件中配置 OpenAPI 异常的统一渲染处理: ```php{9-13} $app = Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { // 中间件配置 }) ->withExceptions(function (Exceptions $exceptions) { // OpenAPI 异常统一处理 $exceptions->render(function (OpenapiException $exception, Request $request) { return OpenapiResponse::error( $exception->getMessage(), $exception->getCode() ); }); // 其他异常处理... })->create(); ``` ::: info 异常处理说明 * 所有 OpenAPI 相关异常都会被自动捕获并格式化返回 * 异常响应格式与成功响应保持一致 * 建议为不同的错误场景创建专门的异常类 ::: ## 状态码枚举 ### 枚举定义 OpenAPI 模块使用统一的状态码枚举来标识不同的响应状态。所有枚举都需要实现 `Modules\Openapi\Enums\Enum` 接口: ```php{4,6-14} namespace Modules\Openapi\Enums; enum Code: int implements Enum { // 基础状态码 case SUCCESS = 10000; // 请求成功 case FAILED = 10001; // 请求失败 // 认证相关错误 case APP_KEY_LOST = 10002; // AppKey 丢失 case SIGNATURE_LOST = 10003; // 签名丢失 case INVALID_APP_KEY = 10004; // 无效的 AppKey case INVALID_SIGNATURE = 10005; // 无效的签名 case INVALID_TIMESTAMP = 10006; // 无效的时间戳 // 业务相关错误 case BALANCE_NOT_ENOUGH = 10007; // 余额不足 case RATE_LIMIT = 10008; // 请求频率超限 /** * 获取状态码对应的中文描述 */ public function name(): string { return match ($this) { self::SUCCESS => '请求成功', self::FAILED => '请求失败', self::APP_KEY_LOST => 'AppKey 丢失', self::SIGNATURE_LOST => '签名丢失', self::INVALID_APP_KEY => '无效的 AppKey', self::INVALID_SIGNATURE => '无效的签名', self::INVALID_TIMESTAMP => '无效的时间戳', self::BALANCE_NOT_ENOUGH => '余额不足', self::RATE_LIMIT => '请求过于频繁,请稍后重试' }; } /** * 比较枚举值 * @param mixed $value * @return bool */ public function equal(mixed $value): bool { return $this->value === $value; } } ``` ### 状态码说明 | 状态码 | 常量名 | 描述 | 触发场景 | | ------ | ------------------ | ------------- | --------------------- | | 10000 | SUCCESS | 请求成功 | 正常响应 | | 10001 | FAILED | 请求失败 | 通用错误 | | 10002 | APP\_KEY\_LOST | AppKey 丢失 | 请求头缺少 app-key | | 10003 | SIGNATURE\_LOST | 签名丢失 | 请求头缺少 signature | | 10004 | INVALID\_APP\_KEY | 无效的 AppKey | AppKey 不存在或已禁用 | | 10005 | INVALID\_SIGNATURE | 无效的签名 | 签名验证失败 | | 10006 | INVALID\_TIMESTAMP | 无效的时间戳 | 时间戳过期或格式错误 | | 10007 | BALANCE\_NOT\_ENOUGH | 余额不足 | 用户余额不够扣费 | | 10008 | RATE\_LIMIT | 请求频率超限 | 超过 QPS 限制 | ::: tip 自定义状态码 如需添加新的状态码,建议从 10100 开始,避免与系统预留码冲突。 ::: ## 最佳实践 ### 安全建议 1. **密钥管理** * 定期更换 `AppSecret`,建议每 3-6 个月更换一次 * 使用环境变量存储密钥,避免硬编码 * 为不同环境(开发、测试、生产)使用不同的密钥 2. **签名验证** * 时间戳有效期建议设置为 5-15 分钟 * 对敏感接口可以添加额外的业务级验证 * 记录异常签名尝试,便于安全审计 3. **频率控制** * 根据业务场景合理设置 QPS 限制 * 考虑为不同类型的接口设置不同的限流策略 * 提供友好的限流提示信息 ### 性能优化 1. **缓存策略** * 缓存用户的 `AppKey` 和 `AppSecret` 映射关系 * 使用 Redis 等缓存工具提高验证效率 * 合理设置缓存过期时间 2. **日志管理** * 定期清理过期的请求日志 * 考虑使用异步方式记录日志 * 重要信息可以同步到专门的日志系统 ### 监控告警 建议监控以下指标: * API 调用成功率 * 平均响应时间 * 异常签名尝试次数 * QPS 超限频率 * 用户余额预警 ## 常见问题 ### 签名验证失败 **问题**:接口返回 "无效的签名" 错误 **解决方案**: 1. 检查参数排序是否正确(按字典序) 2. 确认时间戳格式(Unix 时间戳) 3. 验证 `AppSecret` 是否正确 4. 确保参数拼接格式为 `key=value&key=value` 5. **重要**:检查数组扁平化是否正确实现(嵌套对象和数组必须扁平化) ### 请求频率超限 **问题**:接口返回 "请求过于频繁" 错误 **解决方案**: 1. 检查当前 QPS 设置 2. 优化调用频率,添加请求间隔 3. 联系管理员调整 QPS 限制 4. 考虑使用批量接口减少调用次数 ### 时间戳过期 **问题**:接口返回 "无效的时间戳" 错误 **解决方案**: 1. 确保服务器时间同步 2. 检查时间戳生成逻辑(必须是 Unix 时间戳) 3. 默认时间窗口为 60 秒,确保请求在有效时间内发送 4. 使用 NTP 服务同步时间 ::: info 时间戳验证逻辑 服务端验证:`abs(time() - $timestamp) <= 60`(默认 60 秒) 可通过配置 `api.timestamp_period` 调整时间窗口 ::: *** ::: info 技术支持 如果在使用过程中遇到问题,请参考本文档或联系技术支持团队。 ::: --- --- url: /server/permission.md --- # RBAC 权限系统 CatchAdmin 专业版采用标准的 RBAC(基于角色的访问控制)权限模型,实现用户-角色-权限的多对多关联关系。如需深入了解 RBAC 权限机制,可参考这篇[基于角色的访问控制](https://docs.oracle.com/cd/E19253-01/819-7061/rbac-38/index.html)文档。 :::info [catchadmin 专业版权限讲解](https://www.bilibili.com/video/BV1PfWNeGEdN) ::: ## 基本约定 * 超级管理员不受任何权限控制 * 对于 RBAC 权限控制,`GET` 请求默认放行,不受权限限制 ## 权限模块 CatchAdmin 专业版默认不启用权限模块和动态菜单功能。使用前需要先激活权限模块 ```php php artisan catch:module:install permissions ``` :::tip 开启之后如果没有权限菜单,可以刷新一下 ::: ### 中间件 权限模块提供专用的权限验证中间件,实现访问控制功能 ```php title="modules/Permissions/Middlewares/PermissionGate.php" class PermissionGate { public function handle(Request $request, \Closure $next) { // GET 请求默认放行,不进行权限验证 if ($request->isMethod('get')) { return $next($request); } /* @var User $user */ $user = $request->user(getGuardName()); // 验证用户权限,无权限时抛出异常 if (! $user->can()) { throw new PermissionForbidden(); } return $next($request); } } ``` ### 权限配置 了解中间件机制后,重点需要掌握权限的数据结构组成。前后端分离项目的权限配置相比传统渲染项目更为复杂,建议先熟悉 [Vue 路由](https://router.vuejs.org/)相关知识。在**权限管理/菜单管理**页面点击新增,可看到权限配置界面 ![pSl4dVP.png](https://s1.ax1x.com/2023/01/16/pSl4dVP.png) `CatchAdmin` 将权限分为三种类型 * **目录** 目录仅仅就是一级菜单 * **菜单** 菜单就是主要的页面 * **按钮** 每个页面的操作,每个按钮都对应后端的控制的一个 `action`,这个非常重要 * 路由 `Path` 对应前端 `vue` 路由的 `path` * 组件 对应前端 `vue` 路由的 `component` * 目录类型一般都是选择 Layout 组件 * 菜单类型则是选择对应页面的组件 传统 Laravel 项目通过 Controller 实现页面和操作的权限控制。前后端分离项目中,页面渲染交由前端处理,但数据操作仍由后端控制,因此 RBAC 权限系统主要负责 API 访问控制。后端重点关注`按钮类型`权限,即对控制器 `action` 方法的访问控制。 因此需要为控制器的每个 `action` 操作配置相应的权限标识,实现精细化权限控制。 ### 权限判断 CatchAdmin 专业版采用模块化架构,权限标识格式规范如下 ``` module@controller@action ``` 例如权限模块的角色列表操作,权限验证标识格式示例 ```php Modules\permissions\Http\Controller\RolesController@index ``` #### 当前用户是否有权限 ```php // 验证当前用户是否具有指定权限 Auth::user()->can(string $permission = null); ``` * permission 参数格式为 `module@controller@action`,例如 `permissions@Roles@index` #### 用户的权限 ```php // 获取用户的所有权限列表 /*@var Model\Roles $user*/ $user->withPermissions()->permissions; ``` #### 角色权限 ```php /*@var Model\Roles $role*/ $role->getPermissions() ``` --- --- url: /start/video.md --- ## 视频教程 专业版入门基础视频,可以帮助快速进行开发和了解其相关功能 [专业版视频合集](https://www.bilibili.com/video/BV1Voe3ePEHJ/?spm_id_from=333.788\&vd_source=c09f2335efc2c9c761760541e9592e25) --- --- url: /server/webhook.md --- # Webhook 通知 CatchAdmin 内置了几个常用平台的 Webhook 通知机器人,方便后台监听反馈系统指标,目前支持的平台有 * 飞书 * 钉钉 * 企微 ![CatchAdmin webhook 通知 Laravel Admin](https://image.catchadmin.com/202411250831208.png) 目前内置一个异常通知事件,只需要填入对应平台的 `webhook` 地址即可 ## 扩展 就以`飞书通知为例`, 每个 webhook 平台需要实现两个方法 ```php namespace Modules\System\Support\Webhook; use Catch\Exceptions\FailedException; use Illuminate\Support\Facades\Http; // 继承平台类 class Feishu extends Platform { // 必须实现的方法 public function sign(): string { return base64_encode(hash_hmac( 'sha256', '', $this->timestamp."\n".$this->secret, true )); } // 必须实现发送方法 public function send(string $msgType, string $content): bool { $this->timestamp = time(); $postData = [ 'timestamp' => $this->timestamp, 'msg_type' => $msgType, 'content' => [ $msgType => $content, ], ]; // 签名校验 if ($this->secret) { $postData['sign'] = $this->sign(); } $response = Http::asJson()->post($this->webhook, $postData); if (! $response->ok()) { throw new FailedException('飞书 Webhook 推送失败'); } if (! $response['code']) { return true; } throw new FailedException('飞书 Webhook 错误: '.$response['msg']); } } ``` 实现对应平台之后,需要将对应的实现加入到 `webhook` 通知中,目前项目使用的硬编码方法 找到 `modules/System/Support/Webhook.php` 文件,将对应平台添加进去 ```php protected function getWebhookPlatform(): WebhookInterface { $platform = [ Webhooks::FEISHU => Feishu::class, // 飞书 ][$this->webhook->platform] ?? null; if (! $platform) { throw new FailedException('The platform dont support now.'); } return app($platform)->config($this->webhook->webhook, $this->webhook->secret); } ``` ## 使用 实现之后,如何使用系统呢?首先来看看目前内置的`异常`通知,找到 `modules/System/Models/Webhooks.php`模型文件,然后添加类型 ```php class Webhooks extends Model { // 推送事件 public const EXCEPTION_EVENT = 'exception'; // 添加另外的事件 public const OTHER_EVENT = 'other_event'; /** * 异常事件 * * @return Collection */ public static function exceptions(): Collection { return self::where('event', self::EXCEPTION_EVENT)->get(); } /** * 添加一个 other event * * @return Collection */ public static function otherEvents(): Collection { return self::where('event', self::OTHER_EVENT)->get(); } } ``` 前端还要添加一个事件选项,找到 `modules/Common/Repository/Options/WebhookEvents.php` 文件,添加下面的数组 ```php class WebhookEvents implements OptionInterface { public function get(): array { return [ [ 'label' => '异常事件', 'value' => 'exception', ], [ 'label' => '其他事件', 'value' => 'other_event', ] ]; } } ``` ## 触发事件 在事件触发的地方安排 `webhook` 通知,代码如下 ```php // $webhook = new Webhook(Webhooks::otherEvents()); $webhook->setValues(['other events'])->send(); ``` --- --- url: /server/code-generate.md --- # 代码生成 **代码生成** 真的是每个项目都必备的了!代码生成一般都是为了减少`curd`的重复工作。因为后台管理的`curd`真的是必备的,所以代码生成还是很有必要的。目前代码生成可以自动生成以下文件 * 控制器 Controller * 表单验证请求 * 模型 Model * 自动写入 restful 路由 * 数据库文件(migration) * 前端列表页面 * 前端表单页面 * 自动生成权限菜单 :::warning * 因为项目是前后端分离的,所以代码生成只能在开发环境使用 * 代码生成工具并不是智能工具,它是根据已有信息渲染对应的模板 ::: :::info 旧版本视频教程地址[Catchadmin 专业版模块开发](https://www.bilibili.com/video/BV1nDpoe1EYE) ::: ## 基本使用 ### 模块 众所周知,`CatchAdmin` 是模块化的,那么首先需要干什么呢?没错,就是新增模块。如下图所示 ![CatchAdmin 专业版代码生成 - Laravel Admin](https://image.catchadmin.com/202506091612109.png) 点击新增按钮,按照提示信息输入模块信息,然后点击创建模块。 模块创建之后回生成如下文件,以本地 Test 模块为例,如下图所示 ![CatchAdmin 专业版代码生成 - Laravel Admin](https://image.catchadmin.com/202506091613914.png) 来解释下具体文件信息 :::code-group ```php [TestServiceProvider.php] // 模块服务,用于加载模块的配置和事件的,在[模块化]章节有详细介绍 class TestServiceProvider extends CatchModuleServiceProvider { /** * route path * * @return string */ public function moduleName(): string { // TODO: Implement path() method. return 'test'; } } ``` ```php [route.php] // 模块路由 Route::prefix('test')->group(function () { Route::adminResource('pay/order/refunds', PayOrderRefundsController::class); Route::adminResource('admin/users', AdminUsersController::class); //next }); ``` ```php [Installer.php] // 模块安装器,在[模块化]章节有详细介绍 class Installer extends ModuleInstaller { protected function info(): array { // TODO: Implement info() method. return [ 'title' => '测试', 'name' => 'test', 'path' => 'Test', 'keywords' => 'test', 'description' => 'test', 'provider' => TestServiceProvider::class, ]; } protected function requirePackages(): void { // TODO: Implement requirePackages() method. } protected function removePackages(): void { // TODO: Implement removePackages() method. } } ``` ::: 模块创建完成之后,然后在创建 `Schema`,这里所谓的 `schema` 就是数据库的表,支持新建表,也支持已有表创建。 ### 新建表 ![CatchAdmin 专业版代码生成 - Laravel Admin](/pro/docs/assets/pS128NF.XBFMrRqF.png) 按照提示填写表信息,然后点击下一步 ![CatchAdmin 专业版代码生成 - Laravel Admin](/pro/docs/assets/pS12dnx.DUhJVr-S.png) 如图,需要在这里添加表字段,完成之后,点击创建即可。如下图 ![CatchAdmin 专业版代码生成 - Laravel Admin](/pro/docs/assets/pS12s4e.CW9lNi3O.png) ### 已有表创建 从已有表里面创建对应生成表记录 ![CatchAdmin 专业版代码生成 - Laravel Admin](https://image.catchadmin.com/202506091622871.png) ## 代码生成 在 `schema` 管理页面的点击对应记录生成代码按钮之后,进入到生成代码的页面 ![CatchAdmin 专业版代码生成 - Laravel Admin](https://image.catchadmin.com/202506111042294.png) ### 参数 生成代码页面有几个比较重要的参数 * `模块` 是必选的,这保证代码生成到某个模块中 * `控制器` 名称是必填的,会在模块的 `Http` 目录下 `Controller` 文件 * `模型名称` 是默认根据表名称生成模型名称,是大驼峰的命名方式 * `菜单名称` 填写了菜单名称会自动生成权限管理的菜单,不填则不生成 * `模型关联关系` 支持生成模型关联关系 * `操作` 目前支持生成导出[数据导出章节](./export.md)和导入[数据导入章节](./import.md) * 分页是否支持分页 * 是否生成表单 * 是否使用动态表单,动态表单相关[文档](https://form-builder.catchadmin.vip) ### 表格和表单生成 :::info 注意代码生成的字段是支持拖拽的,你可以拖拽到合适的位置 ::: ### 表单组件 普通的自带组件这里就不多做解释了。来看看 catchadmin 新增的和后台强制绑定的组件 #### 字典组件 ![CatchAdmin 专业版代码生成 字典组件 - Laravel Admin](https://image.catchadmin.com/202506091650123.png) 生成的效果如下 ![CatchAdmin 专业版代码生成 字典组件 - Laravel Admin](https://image.catchadmin.com/202506091714665.png) #### 附件上传组件 ![CatchAdmin 专业版代码生成 附件上传组件 - Laravel Admin](https://image.catchadmin.com/202506091725566.png) #### 单图/多图上传组件 ![CatchAdmin 专业版代码生成 单图/多图上传组件 - Laravel Admin](https://image.catchadmin.com/202506091726558.png) #### 单文件/多文件上传组件 ![CatchAdmin 专业版代码生成 单文件/多文件上传组件 - Laravel Admin](https://image.catchadmin.com/202506091725907.png) #### 远程级联组件 远程组件需要选择数据源,如下图,其他远程组件类似,如果需要支持树形的话,要选择父级字段。 ![CatchAdmin 专业版代码生成 远程级联组件 - Laravel Admin](https://image.catchadmin.com/202506091654120.png) 生成后的效果 ![CatchAdmin 专业版代码生成 远程级联组件 - Laravel Admin](https://image.catchadmin.com/202506091715773.png) #### 远程下拉组件 ![CatchAdmin 专业版代码生成 远程下拉组件 - Laravel Admin](https://image.catchadmin.com/202506091724707.png) #### 远程树形组件 ![CatchAdmin 专业版代码生成 远程下拉组件 - Laravel Admin](https://image.catchadmin.com/202506091724974.png) #### 远程树形下拉组件 ![CatchAdmin 专业版代码生成 远程树形下拉组件 - Laravel Admin](https://image.catchadmin.com/202506091729938.png) #### 图标选择组件 ![CatchAdmin 专业版代码生成 图标选择组件 - Laravel Admin](https://image.catchadmin.com/202506091714910.png) #### 下拉 类似这种下拉选项的可以支持动态添加,例如 radio checkbox 等 ![CatchAdmin 专业版代码生成 下拉 - Laravel Admin](https://image.catchadmin.com/202506091706997.png) ### 验证规则 :::info 验证规则是 Laravel 内置的验证规则,只支持部分前端规则 ::: #### 以下是前端验证的规则 * `required` 必须填写 * `string` 必须是字符串类型 * `url` URL 格式不正确 * `number` 必须是数字类型 * `email` 邮箱格式不正确 * `boolean` 必须是布尔类型 * `date` 日期格式 验证规则会生成在,如下图 ![CatchAdmin 专业版代码生成 - Laravel Admin](https://image.catchadmin.com/202506091647938.png) ## 后端 ### 枚举 如果你使用的是下拉选项之类的,就是包含`options`选项的组件,后端会自动识别,并且生成文本返回。 例如状态字段,它的 options 选项如下 ```json [ { "label": "正常", "value": "1" }, { "label": "禁用", "value": "2" } ] ``` 那么模型会生成如下面的代码 ```php /** * status 字段转换器 * * @return Attribute */ public function statusText(): Attribute { $text = [2 => '禁用', 1 => '正常']; return Attribute::make(get: fn ($value) => $text[$this->status] ?? ''); } ``` 前端的状态字段也会自动使用`status_text`字段来渲染 ### 图片 多图的情况下,会自动生成下面的代码,这里使用`avatar`字段演示 ```php /** * avatar 转换 * * @return Attribute */ public function avatar(): Attribute { return Attribute::make( get: fn ($value) => ! $value ? [] : array_map(fn ($item) => $item ? url($item) : '', json_decode($value, true)), set: fn ($value) => json_encode(array_map(fn ($value) => remove_app_url($value), $value)) ); } ``` ![CatchAdmin 专业版代码生成,前端会自动生成图片展示 - Laravel Admin](https://image.catchadmin.com/202506091831480.png) ### Switch 组件 首先会在前端表格内生成一个`switch`组件,用于字段值切换 ![CatchAdmin 专业版代码生成,Switch 组件 - Laravel Admin](https://image.catchadmin.com/202506091835857.png) 还会在对应的控制器生成如下代码,因为是 `status` 字段,所以生成下面的默认代码 ```php public function enable(mixed $id): mixed { return $this->model->toggleBy($id); } ``` 如果是其他非`status`字段则生成如下代码 ```php public function enable(mixed $id, Request $request): mixed { $field = $request->get('field'); return $this->model->toggleBy($id, $field); } ``` :::warning 这里需要注意的是,系统内置的状态切换的值,并不是 0 和 1。而是 `1 是` 和 `2 否`,如果不是这么做的,需要你自己再模型重写 `toggleBy` 方法 ::: ### 导出 如图,选择你需要导出的字段 ![CatchAdmin 专业版代码生成,导出 组件 - Laravel Admin](https://image.catchadmin.com/202506111045226.png) 最终代码生成一个导出得文件 `modules/Test/Excel/Export/xxxxExport.php`,文件内容如下 :::info 下面的代码仅作演示, [详细请参考导出数据文档](./export.md) ::: ```php /** * 导出数据 * * * @class Excel\Export\AdminUsersExport */ class AdminUsersExport extends Export { protected array $header = [ 'id', '字典选择器', '图标选择', '远程树形', '文件上传', '但附件', '多附件上传', '图片上传', '多图片上传', '创建时间', '更新时间', ]; /** * @return array */ public function array(): array { return AdminUsers::query()->get()->select([ 'id', 'username', 'password', 'avatar', 'remember_token', 'department_id', 'status_text', 'login_ip', 'login_at', 'created_at', 'updated_at', ])->toArray(); } } ``` 这里主要注意的是。代码生成器会在转换上面的枚举值,将对应的枚举值转换成中文说明 ### 导入 跟导出是一样的,选择字段进行导入 ![CatchAdmin 专业版代码生成,导入 组件 - Laravel Admin](https://image.catchadmin.com/202506111049536.png) 最终代码生成一个导出得文件 `modules/Test/Excel/Export/xxxxImport.php`,文件内容如下 :::info 下面的代码仅作演示, [详细请参考导入数据文档](./import.md) ::: ```php /** * 导入数据 * * * @class Excel\Import\AdminUsersImport */ class AdminUsersImport extends Import { /** * 导入数据 * * * @param Collection $rows * @return void */ public function collection(Collection $rows): void { $rows->each(function ($row) { $model = new AdminUsers; $model->id = $row[0]; $model->username = $row[1]; $model->password = $row[2]; $model->mobile = $row[3]; $model->email = $row[4]; $model->save(); }); } } ``` CatchAdmin 还自动生成了一个模板文件,根据选择字段`label`值。模板文件存放在`storage/static/importTemplates/xxxx导入模板.xlsx`。 :::warning 实际导入业务比较复杂,可以自行修改导入类 ::: --- --- url: /tenancy/command.md --- # 任务调度 使用 Laravel 得任务调度,会自动为当前所有租户执行命令。为了实现这一个功能,在创建命令的时候需要继承 `TenancyCommand`,可以使用 `php artisan catch:tenant:command` 命令创建命令,如下: ```php php artisan catch:tenant:command ``` 然后在命令中会自动继承 `TenancyCommand`,如下: ```php namespace App\Commands; use CatchTenant\Commands\TenancyCommand; class TestCommand extends TenancyCommand { /** * 命令签名 * * @var string */ protected $signature = 'test'; /** * 命令描述 * * @var string */ protected $description = '测试命令 - 在所有租户环境中执行'; /** * 命令处理逻辑 * * @return void */ public function handle(): void { // 此处的代码将在每个租户环境中分别执行 // 可以通过 $this->tenant 获取当前租户信息 $this->info('正在租户 [' . $this->tenant->id . '] 环境中执行命令'); // 执行租户特定的业务逻辑 // ... } } ``` 系统将自动遍历所有租户并在每个租户的环境中执行该命令。 --- --- url: /front/side-route.md --- # 侧边栏&路由 路由和侧边栏是组织起一个后台应用的关键骨架。 项目侧边栏和路由是绑定在一起的,`web/src/router/index.ts` 是整个路由的入口文件,如果是按正常顺序文档看下来,应该知道本项目的路由分为两种情况 * 动态生成的路由,也就是权限管理打开后,菜单管理的数据 所以一般情况下不用管`web/src/router/index.ts`路由这个入口文件。 ## 类型 对于权限和菜单,系统定义了两种类型,查看 `web/src/types` ### 权限类型 ```js title="web/src/types/Permissions.ts" export interface Permission { id: number // id parent_id: number // 父级 ID permission_name: string // 权限名称 type: number // 类型 icon: string // icon 图标 component: string // 组件 module: string // 模块 permission_mark: string // 权限标识 route: string // 路由,对应的是 vue route 的 path redirect: string keepAlive: boolean hidden: boolean // 是否隐藏 is_inner: boolean // 是否是内页 } ``` ### 菜单类型 菜单类型,最终都是由权限类型转换而来,所以一旦是动态生成的路由,那么元数据都是由菜单数据提供 ```js title="resource/admin/types/Menu.ts" import { Component } from 'vue' import { RouteRecordRaw } from 'vue-router' // meta 元数据 // 在记录上附加自定义数据。 // 这个数据将会附着在 vue route 上 export interface Meta { title: string icon: string // icon roles?: string[] // 哪些角色可以访问页面,未实现,保留 cache?: boolean // 页面缓存,未实现,保留 hidden: boolean // 是否隐藏,当设置成 true 时,菜单则不会在侧边栏显示。例如内页编辑页面啊,Login,页面 404 页面啊 keepalive?: boolean // 是否 keepalive 目前未实现,保留数据结构 is_inner?: boolean // 是否是内页 } // @ts-ignore // Menu 类型和 Vue Route 类型一样了 export interface Menu extends Omit { path: string // path 访问路径 name: string // name 菜单名称 meta?: Meta // meta,路由附着的额外数据 redirect?: string component?: Component // 页面组件 children?: Menu[] // 子菜单 } ``` 在了解完这两个相关类型之后,再来看动态菜单和权限如何实现的,静态菜单就不做介绍了。首先找到`web/src/route/guard/index.ts` 文件,从这里开始,这里是路由导航守卫。下面直接通过代码来注解如何实现 ```js title="web/src/route/guard/index.ts" const guard = (router: Router) => { // white list const whiteList: string[] = [WhiteListPage.LOGIN_PATH, WhiteListPage.NOT_FOUND_PATH] router.beforeEach(async (to, from, next) => { // set page title setPageTitle(to.meta.title as unknown as string) // page start progress.start() // 获取用户的 token const authToken = getAuthToken() // 如果 token 存在 if (authToken) { // 如果进入 /login 页面,重定向到首页 if (to.path === WhiteListPage.LOGIN_PATH) { next({ path: '/' }) } else { const userStore = useUserStore() // 获取用户ID if (userStore.getId) { next() } else { try { // 阻塞获取用户信息 // ⚠️ 用户信息已经包含了该用户所有可用权限,在 `permissions` 里 await userStore.getUserInfo() // 如果后端没有返回 permissions,前台则只使用静态路由 if (userStore.getPermissions !== undefined) { // 挂载路由(实际是从后端获取用户的权限) const permissionStore = usePermissionsStore() // 动态路由挂载,这里是主要实现动态路由菜单的地方 const asyncRoutes = permissionStore.getAsyncMenusFrom(toRaw(userStore.getPermissions)) // 在这里使用 addRoute 动态挂在路由 asyncRoutes.forEach((route: Menu) => { router.addRoute(route as unknown as RouteRecordRaw) }) } next({ ...to, replace: true }) } catch (e) { removeAuthToken() next({ path: `${WhiteListPage.LOGIN_PATH}?redirect=/${to.path}` }) } } } progress.done() } else { // 如果不在白名单 if (whiteList.indexOf(to.path) !== -1) { next() } else { next({ path: WhiteListPage.LOGIN_PATH }) } progress.done() } }) router.afterEach(() => { progress.done() }) } export default guard ``` ## 侧边栏 上面经过路由导航守卫之后,动态权限就已经转化为动态菜单了。主要通过这个方法来实现转换 ```js const asyncRoutes = permissionStore.getAsyncMenusFrom(toRaw(userStore.getPermissions)) ``` 这里就不细说里面的实现了,是通过递归实现无限极菜单。但是这里一个非常重要的点,就是权限是通过`pinia` 进行保存的,因为 `pinia` 是响应式的。找到 `web/src/store/user/permissions.ts`,看下 `permissionStore` 的定义 ```js title="web/src/store/user/permissions.ts" interface Permissions { menus: Menu[] // 菜单 asyncMenus: Menu[] // 动态菜单 permissions: Permission[] // 权限 menuPathMap: Map // menu 和 path 的 MAP 数据 } export const usePermissionsStore = defineStore('PermissionsStore', { // state 里面定义的几个数据都是响应式的 state: (): Permissions => { return { menus: [], asyncMenus: [], permissions: [], menuPathMap: new Map(), } }, } ``` 既然菜单都是响应式的,那就好办了呀!菜单的数据就直接从 `store` 获取就可以了。 侧边栏的实现是在 `layout/components/Menu`,侧边栏的菜单也是基于`ElementPlus` 的 `el-menu` 实现的。因为是动态菜单,所以这里的用到了`vue` 的[渲染函数](https://cn.vuejs.org/guide/extras/render-function.html#creating-vnodes)。源码在 `layout/components/Menu/index.vue` 中 --- --- url: /front/entry.md --- # 入口 前端项目放置在根目录 `web` 目录下,关于前端各个目录的作用就不做多介绍了,可以到[项目介绍](/start/overview#目录结构)中查看。`main.ts` 即项目的入口 ```javascript import '@/styles/index.scss' import CatchAdmin from '@/support/catchAdmin' // 首先引入的是 catchadmin 对象 const admin = new CatchAdmin() // 启动项目 admin.bootstrap() ``` 进入到 `CatchAdmin` 对象中,可以看到项目引入了哪些全局组件 ```javascript title="web/src/support/catchAdmin.ts" import { createApp } from 'vue' import type { App as app } from 'vue' import App from '@/App.vue' import router, { bootstrapRouter } from '@/router' import ElementPlus from 'element-plus' import zh from 'element-plus/es/locale/lang/zh-cn' import { bootstrapStore } from '@/stores' import Cache from './cache' import { bootstrapI18n } from '@/i18n' import guard from '@/router/guard' /** * catchadmin */ export default class CatchAdmin { protected app: app protected element: string /** * construct * * @param ele */ constructor(ele: string = '#app') { this.app = createApp(App) this.element = ele } /** * admin boot */ bootstrap(): void { this.useElementPlus().usePinia().useI18n().useRouter().mount() } /** * 挂载节点 * * @returns */ protected mount(): void { this.app.mount(this.element) } /** * 加载路由 * * @returns */ protected useRouter(): CatchAdmin { // 拦截路由 guard(router) bootstrapRouter(this.app) return this } /** * ui * * @returns */ protected useElementPlus(): CatchAdmin { this.app.use(ElementPlus, { locale: Cache.get('language') === 'zh' && zh, }) return this } /** * use pinia */ protected usePinia(): CatchAdmin { bootstrapStore(this.app) return this } /** * use i18n */ protected useI18n(): CatchAdmin { bootstrapI18n(this.app) return this } } ``` 其实总结就是一句话,向 `Vue` 注入组件,最后挂载到 `#app` **dom** 上 ```javascript this.useElementPlus().usePinia().useI18n().useRouter().mount() ``` --- --- url: /front/other.md --- # 其他功能 ## 页面缓存 页面缓存使用 Vue `` 组件。项目默认非开启状态,想要开启可以找到这个文件 ```javascript //src\layout\components\content.vue // 找到下面的代码 ``` 将注释打开,然后注释掉下面的代码`` 还需要找到下面的代码 ```javascript // 大概在 32 行 // const isKeepalive = computed(() => router.currentRoute.value.meta.keepalive) ``` 打开注释该注释即可!!! :::warning 注意 keepalive 组件副作用很多,因为他是将渲染后的页面保存在内存中。页面不会一次渲染,所以对于使用 onMounted 的页面会失效,页面不会重新加载数据 ::: --- --- url: /server/dymaicConfig.md --- # 动态配置 动态配置是框架自带配置一个扩展。动态配置主要为了满足一些修改配置比较频繁,解决一些动态变化的配置可以及时在业务中反映的问题。 在看过一些系统实现之后,发现他们有一个问题,就是无法将动态配置很好嵌入到系统中 :::tip 无法使用系统自带的 `config` 函数获取动态配置 ::: 而是用类似 `Config::class` 类似的配置类获取,个人认为这增加了系统的复杂性。 在了解动态配置之前,先来看下框架自带配置是如何工作的。框架的配置文件一般都是放在根目录的 `config` 文件夹。例如 `database.php` 文件 要获取 `database.php` 的配置内容,需要这么使用 ```php config('database.xxxx') ``` 看的出来,主要用 `.` 这个链接符深层次访问配置,为了不破坏这样的格式,动态配置的设计需要融入它 ## 数据结构 ```php Schema::create('config', function (Blueprint $table) { $table->id(); $table->string('key')->comment('配置的key'); $table->string('value', 1000)->comment('配置的value'); }); ``` ## 示例 用一个七牛云参数配置为例, 例如前端的表单参数格式如下 :::tip 前端传递的参数没有做统一设计,因为变化实在太多,所以入库格式需要自己实现 ::: ```javascript { "driver": "qiniu", "access_key": "123", "secret_key": "123", "bucket": "123", "domain": "123123123" } ``` ### 后端实现 :::tip 注意不要照抄,实现过程各不相同,但最终的入库格式要保持一致,动态配置才可工作 ::: ```php public function store(Request $request) { $driver = $request->get('driver'); if (! $driver) { throw new FailedException('请先选择上传驱动'); } $config['upload.driver'] = $driver; return $this->model->storeBy($config); } ``` 最终入库的形式将会是这样的 ![CatchAdmin 动态配置 - Laravel Admin](https://image.catchadmin.com/image.png) ### 动态加载 在数据正确入库之后,还要在框架启动的时候自动加载。专业版中,是在系统模块的服务提供者中实现的。为了提高框架的整体性能,这部分配置使用了缓存 ```php class SystemServiceProvider extends CatchModuleServiceProvider { public function boot(): void { ## 从缓存中获取 $systemConfig = Cache::get('system_config', []); ## 动态写入到框架的 config 配置中 if ($systemConfig && is_array($systemConfig)) { foreach ($systemConfig as $k => $value) { app('config')->set($k, $value); } } } } ``` --- --- url: /server/helper.md --- # 助手函数 CatchAdmin 自带的一些好用的助手函数 ## 打印 如果因为跨域而无法使用 `dd` 函数输出,那么可以使用下面的 ```php dd_('支持跨域打印') ``` ## 后台缓存函数 这个函数主要用来统一管理后台缓存的,根据缓存前缀配置。需要配置自定义的缓存前缀需要在 env 文件配置 ```php CATCH_ADMIN_CACHE_KEY=这里自定义配置 ``` ```php admin_cache(string $key, \Closure|\DateTimeInterface|\DateInterval|int|null $ttl, Closure $callback) ``` ### 使用 ```php admin_cache('cache_key', 300, function () { return 'admin_cache' }); ``` 永久存储,ttl 设置为 `null` 或者 `0` ```php admin_cache('cache_key', null, function () { return 'admin_cache' }); ``` ### 获取后台缓存 ```php admin_cache_get('cache_key', []); ``` ### 删除后台缓存 ```php admin_cache_delete('cache_key'); ``` ## 获取当前链接所有表 这个方法可以获取当前数据库链接所有表信息 ```php get_all_tables(?string $connection = null, bool $removePrefix = true) ``` * $connection 链接 * $removePrefix 是否删除表前缀 ## 移除 app url 如果需要移除链接中的 app url 配置的值,可以使用它 ```php remove_app_url(string $url) ``` --- --- url: /server/consoles.md --- # 命令介绍 新版 `catchadmin` 命令都是以 **catch** 开头, 想要查看全部命令的话,可以使用 ```shell php artisan | grep catch ``` ## 查看版本号 ```shell php artisan catch:version ``` ## 项目安装 ```shell php artisan catch:install ``` 该命令是用于初始化项目 ## 打包项目 ```shell php artisan catch:build ``` 这个命令会自动打包项目到根目录,会生成一个 zip 文件。可以直接上传到服务器 ## 模块安装 ```shell php artisan catch:module:install ``` 安装模块,请根据对应的显示的模块安装就行了 ### 安装所有模块 可以一次性安装所有的模块 ```shell php artisan catch:module:install --all ``` ## 创建 Migrate 文件 ```shell php artisan catch:make:migration ``` ## 创建 Seed 文件 ```shell php artisan catch:make:seeder ``` ## 执行 Migrate ```shell php artisan catch:migrate ``` 执行模块的 migrates 文件, 创建模块相关表结构 ```shell php artisan catch:migrate permissions ``` ## 执行 Seed ```shell php artisan catch:db:seed ``` 执行模块的 seed 文件,填充初始化数据 ```shell php artisan catch:migrate permissions ``` ## 导出模块相关的菜单 ```shell php artisan catch:export:menu ``` * `--p` 可选参数,是否导出树形结构 导出权限模块的菜单,并且生成`seed`文件。这个命令只在打包模块时会有用处,如果模块不用与与其他人共享的话,基本用不着 :::tip 根据提示选择导出的模块即可 ::: ## 生成模型 ```shell php artisan catch:make:model ``` 生成模型文件 ```shell php artisan catch:make:model permissions Users ``` 内容如下 ```php namespace Modules\Permissions\Models; use Catch\Base\CatchModel as Model; class Users extends Model { protected $table = 'users'; protected $fillable = [ 'id', 'username', 'password', 'email', 'avatar', 'remember_token', 'department_id', 'creator_id', 'status', 'login_ip', 'login_at', 'created_at', 'updated_at', 'deleted_at', ]; } ``` ## 更新后台用户密码 ```shell php artisan catch:pwd ``` ## 同步地区数据 ```shell php artisan catch:areas ``` ## 生成 CatchAdmin API 文档 ```shell php artisan catch:api:doc ``` ## 增量更新命令 如果项目需要增量做变更,可以使用它来做增量变更。这个命令是基于 git tag 来进行增量文件变更。 例如专业版有 `3.4.0` 和 `3.3.0`。你需要这两个版本之间的增量文件。使用下面的命令 ```shell php artisan catch:archive:tags ``` 然后选择 `3.3.0` 和 `3.4.0`,会在根目录生成一个 zip 文件 :::warning 注意版本大的在后面 ::: ## 生成补丁包 如果项目需要生成补丁包,可以使用下面的命令,指定两个 `tag` 即可 ```php php artisan catch:generate:patch v1.1 v1.2 ``` ## 补丁更新 可以把补丁包给客户,使用下面的命令用于升级 ```php php artisan catch:upgrade 补丁包名称 ``` --- --- url: /server/response.md --- # 响应 CatchAdmin 为了统一整个后台的响应格式,对后台的响应数据格式做了代码层面的劫持,所有的响应数据以固定的格式进行输出,如下 ### 成功响应(非分页) | 字段 | 类型 | 说明 | | ------- | ------------- | -------- | | code | int | 返回码 | | data | Object|Array | 返回数据 | | message | string | 返回信息 | ### 错误响应 | 字段 | 类型 | 说明 | | ------- | ------ | -------- | | code | int | 返回码 | | message | string | 返回信息 | ### 成功响应(分页格式) | 字段 | 类型 | 说明 | | ------- | ------ | ------------ | | code | int | 返回码 | | data | Array | 返回数据 | | limit | int | 每页显示数量 | | message | string | 返回信息 | | total | int | 总数 | 使用者一般在控制器的方法返回结果就可以了,例如下面这样的 ```php public function index() { return ['hello' => 'world']; } ``` 在控制器返回都是返回 success 的响应状态给前端,如果你需要错误响应,那么就需要使用异常 ```php public function index() { throw new FaileException('处理异常'); } ``` 具体实现可以参考我写的[博客文章-另辟蹊径!如何在 Laravel 更优雅的响应 JSON 数据 ](https://catchadmin.com/post/2024-03/laravel-unified-response),利用 `Illuminate\Foundation\Http\Events\RequestHandled` 事件来 hook 整个响应内容实现,虽然有些绕口,但是在使用起来真的方便太多了。 ## 自定义 从个人开发经验来说,一般来说,后台的响应格式不会有什么变化,除非出现特殊需求,需要特定的响应格式。CatchAdmin 提供了一个非常易用的操作来实现特定的响应格式 ```php use Catch\Support\ResponseBuilder; public function index() { return ResponseBuilder::success()->with('hello', 'world'); } ``` 最后的响应的内容如下 ![CatchAdmin 专业版响应](https://image.catchadmin.com/202409131350017.png) 还可以这么使用,先使用 `code` 静态方法,然后再进行链式调用 ```php public function index() { return ResponseBuilder::code(10000) ->with('hello', 'world') ->with('hi', 'world') ->data($data) ->message('Hello world'); } ``` 这样就可以轻而易举的实现自定义响应内容了,一般没有特殊的数据格式,建议使用 CatchAdmin 默认的响应即可 --- --- url: /server/language.md --- # 国际化 在 `v4.2.5` 版本中,专业版已经将后台语言数据使用后端提供,使用 laravel 框架的多语言功能。在根目录下`lang`目录。目前包含两个语言 * zh * en 目录结构如下 ```php ├─lang │ ├─zh (中文) │ | generate.php | | login.php │ | module.php │ | register.php │ | system.php │ ├─en (英文) │ | generate.php | | login.php │ | module.php │ | register.php │ | system.php ``` 规定以文件名作为某个功能模块的 key 使用,所以具体实现如下 ```php public function translate($lang) { //return admin_cache('lang_'.$lang, 300, function () use ($lang) { $translations = []; $files = File::allFiles(lang_path($lang)); foreach ($files as $file) { $translations[$file->getFilenameWithoutExtension()] = require $file->getRealPath(); } return $translations; // }); } ``` :::warning 默认是不使用缓存的,如果需要,你可以打开缓存,毕竟文件查找得过程是需要耗费时间,尤其是数据多了之后得情况下 ::: 前端还是在`i18n`目录下,跟之前的有所区别,但区别不大,只是加入异步加载得功能,具体实现如下 ```js import Cache from '@/support/cache' import { createI18n } from 'vue-i18n' import type { App } from 'vue' import http from '@/support/http' // 语言包缓存的键名 const LANG_CACHE_KEY = 'cached_lang_pack_' // 创建i18n实例 const i18n = createI18n({ locale: Cache.get('language') || 'zh', fallbackLocale: 'zh', globalInjection: true, legacy: false, messages: {} // 初始化为空对象,语言包将从后端加载 }) // 扩展 i18n 类型 declare module 'vue-i18n' { interface I18n { loadLanguage: (lang: string) => Promise } } /** * 从后端加载语言包 * @param locale 语言代码 * @returns Promise 加载是否成功 */ const loadTranslations = async (locale: string): Promise => { // 如果没有缓存,从后端获取语言包 try { const response = await http.get(`/lang/${locale}`) if (response.data && response.data.data) { // 设置语言包 i18n.global.setLocaleMessage(locale, response.data.data) // 更新当前语言 i18n.global.locale.value = locale // 保存语言设置到缓存 Cache.set('language', locale) // 缓存语言包数据 Cache.set(`${LANG_CACHE_KEY}${locale}`, response.data.data) return true } return false } catch (error) { // 如果是默认语言且没有缓存,则失败 if (locale === 'zh') { return false } return loadTranslations('zh') } } // 将 loadLanguage 方法挂载到 i18n 对象上 i18n.loadLanguage = loadTranslations /** * 初始化i18n * @param app Vue应用实例 */ export async function bootstrapI18n(app: App): Promise { // 加载默认语言包 const defaultLocale = Cache.get('language') || 'zh' try { // 尝试加载默认语言包 const success = await loadTranslations(defaultLocale) // 如果默认语言加载失败且不是中文,尝试加载中文 if (!success && defaultLocale !== 'zh') { await loadTranslations('zh') } } catch (error) { // 如果加载失败,创建一个空的语言包以避免应用崩溃 i18n.global.setLocaleMessage('zh', {}) i18n.global.locale.value = 'zh' } // 注册i18n插件 app.use(i18n) } export default i18n ``` 当然这整个过程还是需要前后端配合的,前端依旧使用 `t` 函数进行多语言翻译。所以整个开发过程中,前后端需要定义好翻译的 key,前端使用 ```js // t 函数进行翻译 t('generate.code.xxxx.xxxx') ``` 这样就可以使用后端来输出翻译数据给前端,即使修改了数据则不需要重新打包 --- --- url: /server/image.md --- # 图片 在现代 Web 应用开发中,高效的图片处理是提升用户体验和优化网站性能的关键因素。虽然大多数企业级应用会选择使用 CDN 服务来处理图片资源,但这往往会带来不必要的额外成本。为了解决这一问题,专业版实现了一套功能强大且易于使用的本地图片处理解决方案,让开发者能够轻松实现图片元数据提取、水印添加、文字叠加、尺寸调整以及动态图片响应等高级功能。 :::warning 适用范围说明 本章节介绍的图片处理功能专为本地图片存储设计。如果您的应用使用 OSS(对象存储服务)或其他云存储解决方案,请参考相应的云服务文档获取图片处理方法。 ::: ## 环境配置 ### 系统依赖 图片处理功能依赖于以下图像处理扩展之一,请确保您的服务器已安装并启用: * GD 库 (默认选项):PHP 内置的图像处理库,适用于大多数基础图像处理需求 * Imagick :ImageMagick 的 PHP 扩展,提供更强大的图像处理能力和更广泛的格式支持 如果您的系统尚未安装这些扩展,请先完成安装配置。 ### 环境变量配置 在项目根目录的 .env 文件中添加以下配置项(如已存在则无需重复添加): ```php # 默认处理图片的驱动 GD 扩展 CATCH_IMAGE_DRIVER=gd # 默认从 uploads 磁盘读取 CATCH_IMAGE_READ_FROM=uploads ``` ### 配置文件 在 `config/catch.php` 配置文件添加下面的配置,如果已有,请忽略 ```php return [ /* |-------------------------------------------------------------------------- | 图片处理配置 | | 控制图片处理的核心参数和默认行为 |-------------------------------------------------------------------------- */ 'image' => [ // 图片处理驱动,从环境变量获取,默认使用GD库 'driver' => env('CATCH_IMAGE_DRIVER', 'gd'), // 图片处理附加选项,可根据需要扩展 'options' => [ // 在此添加自定义选项 ], /** * 默认读取图片的存储磁盘 * 此设置决定了图片处理功能默认从哪个存储磁盘读取源图片 */ 'read_from' => env('CATCH_IMAGE_READ_FROM', 'uploads'), ], ] ``` ## 图片元数据 图片元数据功能允许您快速获取图片的各种属性信息,如尺寸、文件大小、MIME 类型等,这对于图片管理和验证非常有用。 ```php use Catch\Support\Image\Image; // 指定图片的相对路径(相对于默认存储磁盘,通常是uploads) $path = 'avatars/example.jpg'; // 获取图片的完整元数据 $metadata = Image::of($path) ->meta() ->data(); // 输出结果示例 /* [ "filename" => "example.jpg", "size" => "1.2 MB", "size_bytes" => 1258291, "datetime" => "2023-06-15 14:32:47", "width" => 1920, "height" => 1080, "mimetype" => "image/jpeg" ] */ ``` ### 实际应用场景 * 图片验证 :在上传前检查图片尺寸是否符合要求 * 存储优化 :根据图片大小决定是否需要压缩处理 * 前端展示 :获取图片尺寸以优化页面布局 * 图片分类 :根据图片类型和大小进行自动分类 ## 图片水印 水印功能允许您在图片上叠加另一张图片作为水印,并提供丰富的位置控制和透明度调整选项,适用于版权保护、品牌展示等场景。 ### 可用方法 图片水印功能允许您在图片上添加水印图片,并提供多种方法来控制水印的位置、透明度等属性。以下是所有可用的方法: #### 位置控制方法 这些方法用于设置水印在图片上的位置: | 方法 | 描述 | | --------------- | ------------------------ | | `bottom()` | 将水印放置在图片底部中央 | | `bottomLeft()` | 将水印放置在图片左下角 | | `bottomRight()` | 将水印放置在图片右下角 | | `center()` | 将水印放置在图片中央 | | `top()` | 将水印放置在图片顶部 | | `topLeft()` | 将水印放置在图片左上角 | | `topRight()` | 将水印放置在图片右上角 | | `right()` | 将水印放置在图片右侧 | | `left()` | 将水印放置在图片左侧 | #### 偏移与透明度控制 | 方法 | 描述 | | ------------------------ | ---------------------------------------------------------------- | | `offset(int $x, int $y)` | 设置水印的 X 和 Y 轴偏移量 | | `opacity(int $opacity)` | 设置水印的透明度,范围为 0-100(0 为完全透明,100 为完全不透明) | #### 保存与自定义处理 | 方法 | 描述 | | --------------------------------------------------------------- | -------------------------------------------------------- | | `save(string $path, ?string $disk = null, array $options = [])` | 保存添加水印后的图片到指定路径,可选择指定磁盘和其他选项 | | `saving(Closure $saving)` | 设置一个自定义的保存处理闭包函数 | ## 使用示例 ```php // 图片的相对地址,默认使用 uploads 磁盘 $path = 'avatars/5fi5kxDVbd7NGhzY4b0NphYMa5Tg5lWDnBRjqhxU.jpg'; // 水印图片,绝对路径 $waterImage = \Illuminate\Support\Facades\Storage::disk('uploads')->path('water.jpg'); / 创建带水印的图片并保存 Image::of($sourcePath) ->watermark($watermarkPath) // 设置水印图片 ->bottomRight() // 放置在右下角 ->offset(20, 20) // 距离边缘20像素 ->opacity(40) // 设置40%的透明度 ->saving(function ($watermark, $image) { // 在添加水印前对原图进行额外处理 $image->brightness(5)->contrast(10); }) ->save( 'products/product-display-watermarked.jpg', // 保存路径 'public', // 保存到public磁盘 ['quality' => 85] // 设置85%的图片质量 ); ``` :::warning 透明度参数说明 透明度参数 opacity 的有效范围为 0-100: * 0 表示完全透明(水印不可见) * 100 表示完全不透明 * 推荐值:20-60,根据水印图片和背景复杂度调整设置超出范围的值将导致 InvalidArgumentException ::: ## 图片文字 图片文字功能允许您在图片上添加自定义文本,支持丰富的字体、颜色、位置和特效设置,适用于版权声明、说明文字、营销文案等多种场景。 ### 基本用法 ```php use Catch\Support\Image\Image; // 在图片上添加简单文字 Image::of('events/conference.jpg') ->text('2023年技术峰会') // 设置文字内容 ->fontsize(28) // 设置字体大小 ->color('#FFFFFF') // 设置白色文字 ->alignCenter() // 水平居中 ->valignBottom() // 垂直靠底部 ->offset(0, 30) // 距底部30像素 ->save('events/conference-titled.jpg'); ``` ### 保存到存储盘 将图像保存到 Laravel 存储盘: ```php Image::of('path/to/image.jpg') ->text('你好,世界!') ->fontsize(24) ->color('#FF0000') ->save('output.jpg', 'public'); ``` ### 高级定制 实现复杂文本格式,如添加描边、旋转文本或自动换行: ```php Image::of('path/to/image.jpg') ->text('这是一段很长的文本,将自动换行。') ->fontsize(20) ->ttf('path/to/font.ttf') ->color('#FFFFFF') ->stroke('#000000', 2) ->alignCenter() ->valignTop() ->angle(45) ->wrap(200) ->save('path/to/output.jpg'); ``` ### 自定义处理 使用 `saving` 方法通过闭包进行额外的图像处理: ```php Image::of('path/to/image.jpg') ->text('这是一段很长的文本,将自动换行。') ->fontsize(30) ->saving(function ($warpText, $image) { $image->brightness(10)->contrast(20); }) ->save('path/to/output.jpg'); ``` ### 方法 | 方法名 | 参数 | 描述 | 返回值 | | -------------- | ------------------------------------------------------------- | ---------------------------------------------- | ---------------- | | `offset` | `int $x = 0`, `int $y = 0` | 设置文本相对于图像左上角的 X 和 Y 偏移(像素) | `static` | | `save` | `string $path`, `?string $disk = null`, `array $options = []` | 应用文本配置并保存图像,可指定存储盘 | `ImageInterface` | | `fontsize` | `?float $size` | 设置字体大小(点) | `static` | | `ttf` | `?string $ttfFile` | 设置自定义 TTF 字体文件路径 | `static` | | `color` | `mixed $color` | 设置文本颜色(例如十六进制、RGB 数组) | `static` | | `stroke` | `?string $stroke`, `int $width = 1` | 设置文本描边颜色和宽度(像素) | `static` | | `alignLeft` | - | 设置水平左对齐 | `static` | | `alignRight` | - | 设置水平右对齐 | `static` | | `alignCenter` | - | 设置水平居中对齐 | `static` | | `valignTop` | - | 设置垂直顶部对齐 | `static` | | `valignBottom` | - | 设置垂直底部对齐 | `static` | | `valignMiddle` | - | 设置垂直居中对齐 | `static` | | `lineHeight` | `float $lineHeight` | 设置多行文本的行高倍数(默认 1.25) | `static` | | `angle` | `?float $angle` | 设置文本顺时针旋转角度(度) | `static` | | `wrap` | `?int $wrap` | 设置文本自动换行的最大宽度(像素) | `static` | | `center` | - | 水平居中文本(基于图像宽度) | `static` | | `middle` | - | 垂直居中文本(基于图像高度) | `static` | | `saving` | `Closure $callback` | 设置保存前的自定义图像处理闭包 | `static` | ### 示例 以下是综合示例,展示多种功能: ```php Image::of('path/to/image.jpg') ->text('欢迎体验图像处理!') ->fontsize(32) ->ttf('fonts/SimSun.ttf') ->color('#333333') ->stroke('#FFFFFF', 2) ->alignCenter() ->valignMiddle() ->lineHeight(1.5) ->angle(30) ->wrap(300) ->saving(function ($warpText, $image) { $image->resize(800, null, function ($constraint) { $constraint->aspectRatio(); }); }) ->save('output.jpg', 'public', ['quality' => 90]); ``` 此示例: * 在 `input.jpg` 上添加文本“欢迎体验图像处理!”。 * 使用 32 点宋体字体,深灰色文本,白色描边(2 像素宽)。 * 水平和垂直居中。 * 设置行高为 1.5,旋转 30 度。 * 在 300 像素宽度内自动换行。 * 将图像宽度调整为 800 像素,保持纵横比。 * 以 90% 质量保存到 `public` 存储盘的 `output.jpg`。 ### 错误处理 * 确保源图像路径 `$path` 存在。 * 若提供 TTF 字体文件,需确保文件存在。 * 使用存储盘时,需正确配置存储盘。 * 无效的对齐方式或颜色值可能导致 Intervention Image 库抛出异常。 ## 图片尺寸 在日常开发中,我们经常需要对图片进行尺寸调整,以适应不同的显示需求。专业版提供了强大的图片尺寸调整功能,支持多种调整方式,如等比例缩放、覆盖模式、填充模式等。 ### 基本用法 ```php use Catch\Support\Image\Image; // 图片的相对地址,默认使用 uploads 磁盘 $path = 'avatars/example.jpg'; // 调整图片尺寸并保存 Image::of($path) ->resize(800, 600) // 设置目标宽度和高度 ->scale() // 使用等比例缩放模式 ->save('resized/example.jpg'); // 保存到指定路径 ``` ### 调整方法 图片尺寸调整类支持多种调整方法,您可以根据需要选择合适的方式: | 方法 | 描述 | | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | `scale()` | 等比例缩放图片,保持原始比例 | | `scaleDown()` | 等比例缩小图片(不会放大),保持原始比例 | | `cover(string $position = 'center')` | 覆盖模式调整图片尺寸,可能会裁剪部分图片 | | `coverDown(string $position = 'center')` | 覆盖模式调整图片尺寸(不会放大),可能会裁剪部分图片 | | `pad(mixed $background = 'ffffff', string $position = 'center')` | 填充模式调整图片尺寸,会在图片周围添加背景色 | | `contain(mixed $background = 'ffffff', string $position = 'center')` | 包含模式调整图片尺寸,会保持图片比例并添加背景色 | | `crop(int $offset_x = 0, int $offset_y = 0, mixed $background = 'ffffff', string $position = 'top-left')` | 裁剪图片 | | `resizeCanvas(mixed $background = 'ffffff', string $position = 'center')` | 调整画布大小 | | `resizeCanvasRelative(mixed $background = 'ffffff', string $position = 'center')` | 相对调整画布大小 | | `trim(int $tolerance = 0)` | 裁剪图片边缘 | | `disallowOriginSize()` | 不允许超过原始图片尺寸 | ### 位置参数 多个方法支持位置参数,可用的位置值包括: * `center`:中心位置 * `top`:顶部中心 * `top-left`:左上角 * `top-right`:右上角 * `bottom`:底部中心 * `bottom-left`:左下角 * `bottom-right`:右下角 * `left`:左侧中心 * `right`:右侧中心 ### 保存图片 调整图片尺寸后,您可以使用 `save` 方法将图片保存到指定路径: ```php Image::of('avatars/example.jpg') ->resize(800, 600) ->scale() ->save( 'resized/example.jpg', // 保存路径 'public', // 可选,指定存储磁盘 ['quality' => 90] // 可选,保存选项 ); ``` ### 自定义处理 您可以使用 `saving` 方法设置一个自定义的保存处理闭包函数: ```php Image::of('avatars/example.jpg') ->resize(800, 600) ->scale() ->saving(function ($resize, $image) { // 在保存前对图片进行额外处理 $image->greyscale(); }) ->save('resized/example.jpg'); ``` ### 使用示例 #### 等比例缩放 ```php Image::of('avatars/example.jpg') ->resize(800, null) // 只设置宽度,高度自动按比例调整 ->scale() ->save('resized/example.jpg'); ``` #### 覆盖模式(裁剪适应) ```php Image::of('avatars/example.jpg') ->resize(300, 300) // 固定宽高 ->cover('center') // 从中心裁剪 ->save('resized/example_cover.jpg'); ``` #### 填充模式(添加背景) ```php Image::of('avatars/example.jpg') ->resize(300, 300) ->pad('f0f0f0') // 使用浅灰色背景 ->save('resized/example_pad.jpg'); ``` #### 裁剪图片 ```php Image::of('avatars/example.jpg') ->resize(300, 200) ->crop(50, 50) // 从左上角偏移 50px ->save('resized/example_crop.jpg'); ``` #### 裁剪边缘 ```php Image::of('avatars/example.jpg') ->resize(null, null) // 保持原始尺寸 ->trim(10) // 裁剪边缘,容差值为 10 ->save('resized/example_trim.jpg'); ``` ### 注意事项 * 如果不指定宽度或高度(设为 `null`),将保持原始图片的相应尺寸。 * 使用 `scaleDown` 或 `coverDown` 方法可以防止图片被放大,只会缩小图片。 * 背景色参数可以使用十六进制颜色代码(如 `'ffffff'`)或其他 Intervention Image 支持的颜色格式。 * 确保目标保存路径所在的目录已存在且有写入权限。 ## 图片响应 日常中,在管理图片的时候,我们需要对图片进行裁剪压缩,减小带宽的压力。一般都是操作原图,然后保存成小图。无法动态的响应图片大小。专业版提供了一个非常简单的操作来实现。 图片响应类是专业版提供的一个用于处理图片输出的工具类,它允许您以不同的格式响应图片,并支持设置图片的宽度、高度和质量等参数。 ### 基本用法 ```php use Catch\Support\Image\Image; Route::get('image', function () { // 图片的相对地址,默认使用 uploads 磁盘 $path = 'avatars/example.jpg'; return Image::of($path) ->response() // 调用响应 ->width(200) // 设置宽度 ->height(150) // 设置高度(可选) ->quality(80) // 设置质量 ->webp(); // 输出为WEBP格式 }); ``` ### 支持的图片格式 图片响应类支持多种常见的图片格式输出,您可以根据需要选择合适的格式: | 方法 | 描述 | | -------- | --------------------------------------------- | | `webp()` | 将图片以 WEBP 格式输出,通常具有更好的压缩率 | | `png()` | 将图片以 PNG 格式输出,适合需要透明背景的图片 | | `jpeg()` | 将图片以 JPEG 格式输出,适合照片等复杂图像 | | `gif()` | 将图片以 GIF 格式输出,支持动画效果 | ### 动态调用其他格式 除了上述预定义的格式方法外,您还可以通过动态方法调用其他支持的格式: ```php return Image::of($path) ->response() ->width(300) ->avif(); // 使用AVIF格式 ``` ### 参数设置 #### 设置图片尺寸 ```php return Image::of($path) ->response() ->width(200) // 设置宽度为200像素 ->height(150) // 设置高度为150像素 ->png(); ``` 如果只设置宽度或高度中的一个,图片将按比例缩放。 #### 设置图片质量 ```php return Image::of($path) ->response() ->quality(75) // 设置质量为75%(范围0-100) ->jpeg(); ``` 质量参数范围为 0-100,值越高图片质量越好,但文件大小也越大。 #### 切换磁盘 您可以指定从哪个磁盘读取图片: ```php return Image::of('avatars/example.jpg') ->response() ->disk('custom_disk') // 从自定义磁盘读取图片 ->width(200) ->png(); ``` 磁盘配置在 `config/filesystem.php` 的 `disks` 部分定义。 #### 动态参数响应 支持类似 OSS 处理的动态处理,目前支持的 宽高和质量三个 ```php Route::get('image', function () { return Image::of('avatars/5fi5kxDVbd7NGhzY4b0NphYMa5Tg5lWDnBRjqhxU.jpg') ->response() ->disk('some_your_disk') ->width(request()->get('width')) ->height(request()->get('height')) ->quality(request()->get('quality')) ->png(); }) ``` 访问图片的地址就变成这样 ```shell /image?width=100&height=50&quality=100 ``` ### 支持输出图片格式 * png; * jpeg; * gif * avif * webp --- --- url: /tenancy/basic.md --- # 基础 本文档将详细介绍 CatchAdmin 多租户系统的基础功能和使用方法,帮助您更好地理解和应用多租户架构。 ## 日志系统 在多租户环境中,日志管理是一个重要的环节。为了清晰区分总后台(中央管理系统)和各个租户后台的运行情况,租户扩展模块对日志存储进行了合理的分割处理。 ### 日志存储结构 * **总后台日志**:`storage/logs` 目录存储中央系统的所有运行日志,包括系统级别的操作记录、错误信息等。 * **租户日志**:`storage/logs/tenant-{uuid}` 目录专门存储对应租户的运行日志,确保租户数据的隔离性。 其中 `{uuid}` 是系统分配给每个租户的唯一标识符,确保了日志的唯一性和可追溯性。这种结构设计使管理员能够轻松区分和查找特定租户的日志信息。 ## 文件上传系统 多租户环境下,文件存储同样需要进行隔离管理。CatchAdmin 专业版提供了完善的文件上传和存储机制,确保不同租户的文件互不干扰。 ### 上传磁盘类型 目前专业版本地上传系统配置了三个独立的上传磁盘,各自承担不同的存储职责: * **`uploads`**: 主要用于上传业务相关的文件和图片,如用户头像、产品图片等。 * **`static`**: 专门用于存储静态资源文件,如 CSS、JavaScript、字体文件等。 * **`certs`**: 用于安全存储证书相关文件,如 SSL 证书、API 密钥等敏感资料。 ### 存储目录结构 文件存储的目录结构设计与日志系统类似,采用了清晰的层级划分: * **总后台文件**:`storage/uploads` 目录存储中央系统的所有上传文件。 * **租户文件**:`storage/uploads/tenant-{uuid}` 目录存储特定租户的上传文件。 这种结构设计不仅保证了数据隔离,还便于系统进行文件管理和维护。同样的结构也适用于 `static` 和 `certs` 磁盘。 ## 命令系统 在多租户架构中,命令执行是一个需要特别关注的环节。默认情况下,Artisan 命令只会在主后台(中央系统)中执行,这对于系统级别的操作是合适的。 ### 多租户命令执行 然而,在某些场景下,您可能需要让命令在所有租户环境中执行,例如: * 数据同步任务 * 定时清理作业 * 批量数据处理 * 系统通知发送 这时,您需要创建一个继承自 `TenancyCommand` 的命令类,系统将自动处理多租户环境下的命令执行逻辑。 ### 示例代码 以下是创建一个多租户命令的示例: ```php use CatchTenant\Commands\TenancyCommand; class TestCommand extends TenancyCommand { /** * 命令签名 * * @var string */ protected $signature = 'test'; /** * 命令描述 * * @var string */ protected $description = '测试命令 - 在所有租户环境中执行'; /** * 命令处理逻辑 * * @return void */ public function handle(): void { // 此处的代码将在每个租户环境中分别执行 // 可以通过 $this->tenant 获取当前租户信息 $this->info('正在租户 [' . $this->tenant->id . '] 环境中执行命令'); // 执行租户特定的业务逻辑 // ... } } ``` ### 注册与调用 创建命令后,需要在 `app/Console/Kernel.php` 中注册该命令,然后可以通过以下方式调用: ```bash php artisan test ``` 系统将自动遍历所有租户并在每个租户的环境中执行该命令。 --- --- url: /tenancy.md --- # 多租户架构 (Multi-Tenancy) :::warning 如果你正在使用 Laravel Octane,请勿使用。目前不支持 ::: ## 体验 可以在这里[注册租户体验](https://pro.catchadmin.com/tenant/register),注册完成之后会自动注册租户 由于是体验,我们会每日自动删除租户数据 ## 如何购买 支持单独购买该扩展,由于是适配专业版的,如果购买了专业版,则可以直接使用该扩展 :::warning 不允许外包私有化部署给客户,如果需要私有给客户部署。请联系专业版开发团队单独报价 ::: ## 什么是多租户? 多租户是一种软件架构模式,允许单个应用程序实例同时为多个客户组织(称为"租户")提供服务。每个租户的数据和配置保持隔离,但共享同一套应用程序代码和基础设施资源。这种架构极大地提高了资源利用率并简化了应用程序维护。 ## 多租户架构的优势 * **成本效益**:通过资源共享降低每个租户的运营成本 * **运维简化**:集中管理单一代码库,减少维护工作量 * **高效升级**:一次部署即可为所有租户提供新功能 * **数据分析**:便于跨租户数据分析(在适当隔离的前提下) * **可扩展性**:根据需求灵活分配资源 ## 常见实现方式 ### 单库模式(共享数据库,共享架构) 在单库模式中,所有租户的数据存储在同一个数据库实例和相同的表结构中,通过添加`tenant_id`字段来区分不同租户的数据。 **优点:** * 实现简单,开发成本低 * 资源利用率高 * 维护成本低,只需管理单一数据库 * 系统升级简单 **缺点:** * 数据隔离性较弱,存在潜在安全风险 * 单个租户的大量数据可能影响其他租户性能 * 难以针对特定租户进行定制化配置 * 使用自增主键时数据迁移复杂,后期升级到多库架构成本高昂 ### 多库模式(租户独立数据库) 多库模式为每个租户提供独立的数据库实例,实现完全的数据隔离。 **优点:** * 数据隔离性强,安全性高 * 可针对特定租户进行独立扩展和优化 * 租户间性能互不影响 * 支持租户特定的数据库配置和定制 * 便于实现合规要求(如数据存储地域限制) **缺点:** * 初始搭建和维护成本较高 * 资源利用率相对较低 * 跨租户操作和数据分析复杂 * 系统升级需要协调多个数据库 ## 扩展使用多库架构 本扩展采用使用第二种多库模式架构方案,为每个租户创建独立的数据库。这种方案虽然前期维护成本较高,但提供了以下关键优势: * **完全数据隔离**:租户数据完全分离,提高安全性和合规性 * **灵活的扩展能力**:可以为高需求租户分配专用资源 * **性能保障**:单一租户的负载不会影响其他租户 * **未来可扩展性**:随着业务发展,可轻松进行架构调整和资源分配 我们会在下面的章节详细说明租户包安装过程 --- --- url: /server/dictionary.md --- # 字典功能 字典功能在后台一直是一个存在感很低的功能,尤其是前后端分离系统中。因为字典的数据在服务端,想要跟前端数据联动的话,需要对原始跟字段数据做一些标记。它最大的用处可能在生成代码的,为一些 `Radio` 组件或者 `Select` `Switch` 组件提供一些基础数据。如下 ![Catchadmin 专业版字典功能-Laravel Admin](https://image.catchadmin.com/202408231339856.png) :::info [CatchAdmin 专业版字典功能视频教程](https://www.bilibili.com/video/BV1RTWReFEHs/) ::: ## 扩展 当字典功能单独作为一个增删改查的功能,只能给前端生成页面提供一些基础数据的时候,其在后台的作用真的是非常小。但是在业务中又有很多地方用到这个。的确给定义一些选项提供了很大的方便,很大程度上保证了数据可以集中管理,不会那么分散。 但是在业务中,会有相当一部分逻辑判断会使用到字典的值。一旦需要使用到,很可能就是出现下面的代码 ```php // 这里以 status 举例,有 1 和 2 两个状态 if ($model->status === 2) {} ``` 这种代码在项目里如果大量出现的话,必定会是非常不可读的代码。会导致后期维护产生很多问题。所以以字典为枚举数据管理点,为字典自动生成枚举,提供给项目里面使用,最后会产生这样的代码 ```php // 状态断言 if (Status::Enable->assert($model->status)) {} ``` 这样的代码相较于之前有什么好处呢? * 可读写提高非常之多 * 一旦 `model->status === 2` 大量出现在代码里,如果这个状态改变了,那么改动的地方非常至多,使用枚举,只需要改动枚举值即可 ## 枚举生成 我们在字典功能提供了非常方便枚举生成功能 * 自动生成 * 对已有的枚举,会对字典内的值和当前枚举进行对比,如果不同,会覆盖生成 ![Catchadmin 专业版字典枚举生成-Laravel Admin](https://image.catchadmin.com/202408231835985.png) * 列表操作的可以将列表所有字典数据生成枚举 * 数据操作枚举可以生成对应字典的枚举类 --- --- url: /server/columnAccess.md --- # 字段权限 * 基于`角色`的字段权限 CatchAdmin 提供了一个与[数据权限](./datarange.md)一样方便的字段权限控制组合`ColumnAccess` trait,使用它,将可以无缝在列表对字段可读权限控制。效果如下 ![CatchAdmin 字段权限-Laravel Admin](https://image.catchadmin.com/202408211537638.png) 这对于需求字段控制的后台需求,将可以大大提高开发效率 ## 权限类型 * `可读权限` 是否可以查询 * `可写权限` 是否可以新增数据库字段或者更新数据库字段 ## 使用 以后台的岗位为例, 只需要在岗位模型上添加下面两行代码 ```php use Modules\Permissions\Models\Traits\ColumnAccess; // [!code ++] class Jobs extends Model { use ColumnAccess; // [!code ++] } ``` 然后找到这个字段管理列表,配置字段权限 ![CatchAdmin 字段权限-Laravel Admin](https://image.catchadmin.com/202408211553757.png) 然后点击字段权限,如下图 ![CatchAdmin 字段权限-Laravel Admin](https://image.catchadmin.com/202408211555315.png) 你可以看到需要管理的表的字段,只需要给字段分配角色即可 :::info 只有分配了字段权限的字段才会进行权限控制,没有分配不会进行权限控制 ::: ## ColumnAccess ```php trait ColumnAccess { /** * @param array $columns * @return array */ public function readable(array $columns): array { return $this->filter($columns); } /** * @param array $columns * @return array */ public function writable(array $columns): array { return $this->filter($columns, false); } } ``` * readable 方法可以过滤当前模型的字段 * writable 方法可以过滤当前模型的字段 一般情况下,Laravel 的 Select 查询有以下几种情况 * select(\['\*']) 使用 \* * 单独的 column * 别名的 column (例如: user\_name as username) * 连表的 column (例如: user.name) * 连表别名的 column (例如: user.name as username) 对于这几种查询情况,无需处理,直接调用 `readable` 方法即可。 --- --- url: /tenancy/install.md --- # 安装指南 :::warning 购买提示 多租户扩展是 CatchAdmin 的高级功能模块,作为单独额外的扩展包提供,需要额外购买。如果您是 CatchAdmin 专业版用户,可以单独购买该扩展模块。 ::: ## 安装步骤 多租户扩展的安装过程非常简便,只需要几个简单的命令即可完成。下面是详细的安装步骤: ### 1. 安装扩展包 首先,使用 Composer 安装多租户扩展包: ```shell composer require catchadmin/tenancy ``` 该命令将会下载并安装所有必要的依赖包和文件。 ### 2. 初始化租户数据 安装完成后,需要运行以下命令来初始化租户系统所需的数据结构和配置: ```shell php artisan catch:tenant:install ``` 该命令将执行以下操作: * 发布必要的配置文件 * 发布数据库迁移文件 * 创建租户相关的数据表 * 准备前端所需的视图文件 ## 配置文件详解 安装多租户扩展后,系统会自动将配置文件发布到 `config/tenancy.php` 路径。该配置文件包含了多租户系统的所有核心设置,您可以根据实际需求进行自定义调整。 下面是配置文件的完整内容,并附有每个配置项的详细说明: ```php return [ 'tenant_model' => Tenant::class, // 租户模型 'id_generator' => UUIDGenerator::class, // ID 生成器 'domain_model' => Domain::class, // 域名模型 /** * 是否缓存租户域名,可以减少租户上下文切换查询,默认开启 * */ 'is_cache_tenant_domains' => [ 'enable' => true, 'ttl' => 3600, // 秒 ], /** * 托管您的中央应用程序的域名列表。 * * 仅在您使用域或子域识别中间件时相关。 */ 'central_domains' => [ '127.0.0.1', 'localhost', env('TENANT_CENTRAL_DOMAIN'), ], /** * 租户引导程序在租户初始化时执行。 * 它们的职责是使 Laravel 功能具有租户意识。 * * 要配置它们的行为,请参见下面的配置键。 */ 'bootstrappers' => [ Modules\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class, // 数据库租户引导程序 Modules\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class, // 缓存租户引导程序 Modules\Tenancy\Bootstrappers\LogTenancyBootstrappper::class, // 日志租户引导程序 Modules\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class, // 文件系统租户引导程序 Modules\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class, // 队列租户引导程序 // Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // 注意:需要 phpredis ], /** * 租户中间件 * * 中间件,在请求处理之前执行。 */ 'tenancy_middlewares' => [ InitializeTenancyByDomain::class, // 租户初始化 InitializeTenancyPermissions::class, // 租户可用性初始化 InitializeTenancySystemSetting::class, // 租户系统配置初始化 ], /** * 数据库租户配置。由 DatabaseTenancyBootstrapper 使用。 */ 'database' => [ 'central_connection' => env('DB_CONNECTION', 'central'), // 中央数据库连接 /** * 租户数据库名称的创建方式: * 前缀 + 租户 ID + 后缀。 */ 'prefix' => 'tenant', 'suffix' => '', /** * 租户数据库管理器类,处理租户数据库的创建和删除。 */ 'managers' => [ 'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class, // SQLite 数据库管理器 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class, // MySQL 数据库管理器 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class, // PostgreSQL 数据库管理器 ], ], /** * 缓存租户配置。由 CacheTenancyBootstrapper 使用。 */ 'cache' => [ 'prefix' => 'tenant:%tenant%:', // 缓存键的前缀,%tenant% 将被替换为租户键,并在原始存储前缀之前添加。 'stores' => [ env('CACHE_STORE'), ], /* * 是否使会话具有租户意识(仅在会话驱动程序基于缓存时使用)。 * * 注意:这将隐式地将您的配置会话存储添加到上面的前缀存储列表中。 */ 'scope_sessions' => true, 'tag_base' => 'tenant', // 缓存标签的基础名称,后跟租户 ID。 ], /** * Log 日志记录 * * 这里设置需要隔离租户日志的日志通道 */ 'logging' => [ 'channels' => [ 'daily', 'single', 'query', ], 'suffix_base' => 'tenant-', ], /** * 文件系统租户配置。由 FilesystemTenancyBootstrapper 使用。 * https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper. */ 'filesystem' => [ /** * 在 'disks' 数组中列出的每个磁盘将以 suffix_base 作为后缀,后跟租户 ID。 */ 'suffix_base' => 'tenant-', 'disks' => [ 'uploads', // 上传磁盘 'static', // 静态文件存储磁盘 'certs', // 证书磁盘 ], /** * 对于本地磁盘使用此设置。 * * 请参见 https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper */ 'root_override' => [ // 根目录在 storage_path() 后面加上后缀的磁盘。 'uploads' => '%storage_path%', 'static' => '%storage_path%', 'certs' => '%storage_path%', ], /** * storage_path() 是否应被后缀化。 * * 注意:禁用此设置可能会破坏本地磁盘租户功能。只有在您使用外部文件存储服务(如 S3)时才禁用此设置。 * * 对于绝大多数应用程序,此功能应启用。但在某些边缘情况下,它可能会导致问题(例如在 Vapor 中使用 Passport - 见 #196),因此 * 如果您遇到这些边缘情况问题,您可能希望禁用此功能。 */ 'suffix_storage_path' => true, /** * 默认情况下,asset() 调用也会进行多租户处理。您可以使用 global_asset() 和 mix() * 来获取全局的、非租户特定的资产。然而,当您在租户应用程序中使用 * asset() 调用的包时,可能会遇到一些问题。为避免此类问题,您可以 * 禁用 asset() 辅助函数的租户处理,并在您希望使用租户特定资产(产品图像、头像等)的位置 * 明确使用 tenant_asset() 调用。 */ 'asset_helper_tenancy' => true, ], /** * Redis 租户配置。由 RedisTenancyBootstrapper 使用。 * * 注意:您需要 phpredis 来使用 Redis 租户。 * * 注意:如果您仅将 Redis 用于缓存,则不需要使用此设置。 * Redis 租户仅在您直接进行 Redis 调用时相关, * 无论是使用 Redis 门面还是通过注入作为依赖项。 */ 'redis' => [ 'prefix_base' => 'tenant:', // Redis 中的每个键都将以此 prefix_base 开头,后跟租户 ID。 'prefixed_connections' => [ // Redis 连接,其键被加上前缀,以区分一个租户的键与另一个租户的键。 // 'default', ], ], /** * Queue 租户配置 */ 'queue' => [ // 是否开启队列监控 'monitor' => env('TENANCY_QUEUE_MONITOR', false), // Queue 队列 'connections' => [ // 主域名 'central' => [ 'driver' => 'database', 'connection' => env('DB_CONNECTION', 'mysql'), 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 90, 'central' => true, ], // 租户 'tenant' => [ 'driver' => 'database', 'table' => 'jobs', 'connection' => env('DB_CONNECTION', 'mysql'), 'queue' => 'tenant', 'retry_after' => 90, ], ], ], /** * 租户用到的事件 */ 'events' => [ // 租户上下文切换前 Events\InitializingTenancy::class => [ ], // 租户上下人切换后 Events\TenancyInitialized::class => [ Listeners\BootstrapTenancy::class, // 启动租户 ], // 租户上下文切换失败 Events\TenancyEnded::class => [ Listeners\RevertToCentralContext::class, // 退出租户 ], ], /** * 是否注册租户路由。 * * 租户路由包括租户资产路由。默认情况下,此路由是 * 启用的。但如果您使用外部 * 存储(例如 S3 / Dropbox)或有自定义资产控制器,则可能需要禁用它们。 */ 'routes' => true, /** * 路由中间件 */ 'route_middlewares' => array_merge( config('catch.route.middlewares', []), [PermissionGate::class] ), /** * 租户迁移命令使用的参数。 */ 'migration_parameters' => [ '--force' => true, // 在生产中运行迁移时,此设置需要为 true。 '--path' => [database_path('migrations/tenant')], // 租户迁移路径 '--realpath' => true, ], /** * 租户填充命令使用的参数。 */ 'seeder_parameters' => [ '--class' => 'DatabaseSeeder', // 根填充类 // '--force' => true, ], ]; ``` ## 数据库迁移文件 安装多租户扩展后,系统会自动发布三个重要的数据库迁移文件,这些文件用于创建多租户系统所需的数据表结构: ### 核心数据表 * **租户表**:`database/migrations/tenancy/2019_09_15_000010_create_tenants_table.php` * 存储所有租户的基本信息 * 包含租户名称、唯一标识符、状态等字段 * 作为多租户系统的核心数据结构 * **域名表**:`database/migrations/tenancy/2019_09_15_000020_create_domains_table.php` * 管理租户绑定的自定义域名 * 实现域名与租户的关联映射 * 支持多域名绑定到同一租户 * **套餐表**:`database/migrations/tenancy/2025_06_21_105037_tenant_plan.php` * 管理不同级别的租户套餐方案 * 定义套餐的功能限制、价格等信息 * 支持租户套餐的升级和降级 这些迁移文件在运行 `php artisan catch:tenant:install` 命令时会自动执行,创建相应的数据表结构。如果您需要自定义这些表的结构,可以在执行迁移前对这些文件进行修改。 ## 前端视图文件 多租户扩展需要相应的前端视图文件来支持用户界面的展示和交互。安装过程中,系统会自动发布这些视图文件到指定的目录。 ### 视图文件路径配置 视图文件的发布路径由 `config/catch.php` 配置文件中的 `views_path` 项决定: ```php /* |-------------------------------------------------------------------------- | 前端 Vue 视图文件夹路径 | | 如果不设置,将不会生成相关的 Vue 文件 |-------------------------------------------------------------------------- */ 'views_path' => base_path('web'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR), ``` ### 默认发布路径 根据上述配置,多租户相关的前端视图文件将默认发布到项目根目录下的 `web/src/views/tenancy` 文件夹中。这些文件包含了租户管理、域名配置、套餐管理等功能的前端实现。 ### 自定义视图路径 :::warning 注意事项 如果您的前端项目不在默认的 `web` 目录中,或者您使用了不同的目录结构,可以手动将生成的 `tenancy` 文件夹复制到您实际前端项目的 `views` 目录下。这样可以确保多租户功能在前端正常展示。 ::: ### 视图文件结构 发布的视图文件包含以下主要组件: * **租户管理**:用于创建、编辑和管理租户 * **域名管理**:用于配置租户的自定义域名 * **套餐管理**:用于设置和管理不同的租户套餐 * **队列任务监控**:用于查看对应的队列任务运行情况 * **调度任务监控**:用于查看调度任务运行情况 * **日志管理**:用于查看租户的日志 ## 前端配置 完成后端安装和配置后,还需要对前端应用进行相应的配置,以使前端能够正确识别和处理多租户模式。 ### 环境变量配置 在前端项目的环境变量文件中添加多租户模式的配置。根据您的部署环境,可以选择在开发环境文件 `.env` 或生产环境文件 `.env.production` 中进行配置: ```shell # 启用多租户模式 VITE_TENANCY_MODE=true ``` ### 配置说明 设置 `VITE_TENANCY_MODE=true` 后,前端应用将启用以下功能: * **域名识别**:自动识别当前访问的域名,并将其与相应的租户关联 * **租户特定界面**:根据当前租户加载相应的界面和配置 * **权限控制**:根据租户的套餐和权限设置显示或隐藏特定功能 * **资源隔离**:确保前端请求正确访问当前租户的资源 ### 前端路由配置 启用多租户模式后,前端路由系统会自动加载租户相关的路由配置,这些配置在之前发布的视图文件中已经包含。 ### 验证配置 完成上述配置后,重新启动前端应用,并访问租户管理页面(通常位于系统设置或管理员菜单中),确认多租户功能是否正常加载。 --- --- url: /start/bt-deploy.md --- # 宝塔部署 开源版本由宝塔官方提供了。现在在宝塔的应用商店通过 `catchadmin` 可以搜到。既然连开源版本都有,那么我们的专业版也必须要有。虽然不能上架商店,然后通过专业版提供的命令也可以让这个过程变得很简单 :::warning 核心在 `0.5.5` 版本提供该功能 ::: ## 常见问题 在部署之前,先把常见问题列在这里,防止后面遇到的不知道怎么解决。 ### 数据库导入失败 找到根目录下`database/schema/mysql-schema.sql` SQL 文件自行导入。精力有限,不能实时跟着宝塔版本走 ### 提示 undefined Illuminate\Filesystem\exec `exce` 禁用函数未打开,最好去查看对应 PHP 版本的配置,就是你正在使用 PHP 版本,防止多环境导致配置问题 ### 一键部署为什么没有本地开发模块? 一键部署部署只是帮你初始化部署项目,本地的所有数据都不会同步。这些数据都需要自己同步,不会过多干预项目本身。 ## 打包 通过下面的命令来打包项目 ```shell php artisan catch:build ``` 按照提示输入对应的信息即可。最后会在根目录生成一个名为 `project.zip` 压缩包。 :::warning 域名记得一定要 `https` 开头 ::: ## 上传压缩包到一键部署 到宝塔后台,如下图依次点击。`软件商店` -> `一键部署` -> `其他` ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280903516.png) 然后点击导入项目, 如图 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280906911.png) 表单内容可以按照自己项目要求填写,最后把刚才打包好的 `project.zip` 打包上传,点击提交等待。 上传成功之后,列表出现一条部署信息,如下图。 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280908501.png) 点击`一键部署`,会弹出如图所示。输入部署的域名即可 :::warning 这里一定要看清楚,是用`域名作为目录`,宝塔会自动生成这样的格式,例如你的域名是 `xxx.xxx.com`,他会变成 `xxx_xxx_com`。所以你自己要改成域名格式 ::: ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280917221.png) 点击提交,等待自动部署完成即可。如果出现下面的问题 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280935232.png) 不需要理会,因为是宝塔的自动部署,会自动处理。直接刷新页面就可以了 找到对应的域名设置 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280947617.png) 打开配置,找到对应的 `配置文件`,然后删除下图的 `location` ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280949472.png) :::warning 关闭这个跨站攻击 ::: ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202412031842396.png) ### 申请证书 继续申请证书 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280951817.png) ### 配置 申请完毕之后,点击到根目录,需要初始化数据 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280956741.png) 进入到目录之后,点击 `.env` 文件,然后按照如下图所示填写配置信息,主要就是数据库信息 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411280959642.png) ### 获取数据库信息 如下图,获取对应的数据库信息,依次对应 `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411281006085.png) 再回到 `.env` 文件,修改数据库信息,如下 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411281009463.png) ### 初始化数据 配置好数据库信息之后,需要初始化数据。如图打开终端 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411281010104.png) 使用下面两个命令 ```shell # 初始化超级管理员 /www/server/php/82/bin/php artisan catch:db:seed user # 初始化权限菜单 /www/server/php/82/bin/php artisan catch:db:seed permissions # 设置软链接 /www/server/php/82/bin/php artisan storage:link ``` 如果遇到 `call to undefined function Illuminate\Filesystem\symlink()`, 请到下图禁用函数的地方,删除掉 `symlink` 函数 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202412031718145.png) ### 访问 使用你打包是的域名访问,例如我是使用的 `https://sa.catchadmin.com`, 那么对应的地址就是 `https://sa.catchadmin.com/#/login`。会看到登录页面 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411281014844.png) 点击登录,就可以进入后台了。 ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411281015133.png) :::warning 登录后台第一件事,记得修改密码。一定要修改!!! ::: ![CatchAdmin 专业版-宝塔部署](https://image.catchadmin.com/202411281015066.png) --- --- url: /server/tips.md --- # 小技巧 这里将会更新一些`Catchadmin`后台管理系统中的一些小技巧和 Laravel 框架中的小 tips,以便开发者更好适应这个框架的开发。也 👏 欢迎开发者补充 ## 加速 composer 加载(优化建议) :::warning 不要在开发环境使用,只适用于生产环境 ::: ```shell composer dump-autoload -o --no-dev --classmap-authoritative ``` * `dump-autoload -o` 转换 PSR-0/4 autoloading 到 classmap 获得更快的载入速度 * `--no-dev` 禁用 autoload-dev 规则 * `--classmap.authoritative` 只加载 ClassMap 中的文件,不必到磁盘文件系统进行任何检查 ## 取消路由中间件 `CatchAdmin` 后台路由默认加了设置的中间件,目前的是有四个。 ```php Catch\Middleware\AuthMiddleware // 认证中间件 Catch\Middleware\JsonResponseMiddleware // Json 响应中间件 Modules\User\Middlewares\OperatingMiddleware // 操作日志记录的中间件 Modules\Permissions\Middlewares\PermissionGate // 权限认证的中间件 ``` 由于在模块服务中添加的路由通常都是**全局**注册到后台所有路由中,但是有时候你并不需要这些路由, 比如做微信公众号验证的时候,并不需要这些路由。你可以使用下面的技巧`withoutMiddleware(config('catch.route.middlewares'))` 取消模块中所有的公共中间件 ```php Route::withoutMiddleware(config('catch.route.middlewares')) ->prefix('wechat') ->group(function(){ Route::prefix('official')->group(function (){ Route::get('sign', [OfficialAccountController::class, 'sign']); }); //next }); ``` ## 响应自定义 目前 `CatchAdmin` 中的响应结构都是固定的, 格式如下 ```php return [ 'message' => '', 'data' => '', 'code' => '' ] ``` 某些情况下你需要特定的结构的话,并不需要上面固定的结构,那么你可以使用下面的技巧. ```php Route::withoutMiddleware('Catch\Middleware\JsonResponseMiddleware') ->group(function(){ Route::prefix('test')->group(function (){ Route::get('test', [TestController::class, 'index']); }); }); ``` 这是由于 `CatchAdmin`通过中间件将所有响应都转换成了`JsonResponse`,然后通过劫持 `JsonResponse` 对象,返回固定的响应格式。 所以只要取消 `JsonResponseMiddleware`, 然后再自己自定义针对特定接口做相应的数据结构即可 ## 验证属性遇到第一个错误直接返回 如果不做任何控制的话,Laravel 的验证会将每个属性的规则全部验证一遍,然后返回属性的第一个错误,这样非常浪费,例如属性如果需要数据库验证的话,那么即使前面的规则不符合了,还会使用到数据库验证,例如下面的例子 ```php $request->validate([ 'code' => [ 'required', 'size:6', function (string $attribute, mixed $value, \Closure $fail) use ($request) { // 这里是数据验证 code 码,例如手机验证码 }] ]); ``` 按照目前的 rule,即使前台发送的 code 不符合 size:6 这个要求,它还是会使用从数据库验证,非常浪费。所以需要遇到第一个 error 就返回,可以这么使用 ```php $request->validate([ 'code' => [ 'bail', // 添加这个属性即可 'required', 'size:6', function (string $attribute, mixed $value, \Closure $fail) use ($request) { // 这里是数据验证 code 码,例如手机验证码 }] ]); ``` 如果是直接通过 `$request->validate()` 进行请求数据的验证,那么每个属性都需要加 `bail` 规则,如果你使用的是 `FormRequest` 进行验证。那么可以使用下面的技巧。添加 `stopOnFirstFailure` 属性 ```php protected $stopOnFirstFailure = true; ``` :::info 通过将 stopOnFirstFailure 属性添加到请求类,一旦发生单个验证失败,它应该停止验证所有属性 ::: --- --- url: /front/layout.md --- # 布局 不管是否进行二次开发,在开始之前都需要了解一下后台的页面布局。这对于认识前端系统非常重要。 布局文件放在 `web/src/layout` 下,这个差不多是标准了。看到 **layout** 文件夹,默认就是布局所在 ```php ├─components │ ├─header (头部组件) │ │ ├─index.vue | | ├─lang.vue (多语言组件) | | ├─logo.vue (logo 组件) | | ├─menuSearch.vue (菜单搜索组件) | | ├─notification.vue (通知组件) | | ├─profile.vue (个人组件) | | ├─theme.vue (主题组件/暗黑模式) | ├─ Menu(头部组件) │ │ ├─index.vue | | ├─item.vue (菜单 item 组件) | | ├─mask.vue (mask 组件) | | ├─menus.vue (菜单组件) | | │ └─content.vue 主题内容 │ └─sider.vue 侧边栏 ├─index.vue ``` ![catchadmin 专业版 Laravel Admin](https://s1.ax1x.com/2023/01/18/pS3JQy9.png) 采用的是传统的双栏布局,即左侧是 **Sider** 右侧是内容。可以从 `layout/index.vue` 看出布局 ```html title="web/src/layout/index.vue" ``` 内容区域分为`Header` 和 `router-view`,可以在 `layout/components/content.vue` 中 ```html title="web/src/layout/components/content.vue" ``` 所以当在 `vue router` 使用 `Layout` 组件是,组件的内容便会展示在 `layout` 内容组件的 `router-view` 中。譬如说 ```javascript title="web/src/layout/index.ts" import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' import type { App } from 'vue' export const constantRoutes: RouteRecordRaw[] = [ { path: '/dashboard', component: () => import('/admin/layout/index.vue'), children: [ { path: '', name: 'Dashboard', meta: { title: 'Dashboard', icon: 'home', hidden: false }, component: () => import('/admin/views/dashboard/index.vue') } ] } ] ``` `dashboard` 组件,也就是首页。使用的是 vue 路由嵌套的规则,Dashboard 组件被插入到内容组件的 `` 区域,这跟插槽有点类似了 ```php /layout/index /layout/index +------------------+ +-----------------+ | Layout | | Layout | | +--------------+ | | +-------------+ | | | Dashboard | | +------------> | | Develop | | | | | | | | | | | +--------------+ | | +-------------+ | +------------------+ +-----------------+ ``` :::info 如果不了解 vue router,可以先去[vue-router](https://router.vuejs.org/zh/guide/)看下文档 ::: --- --- url: /front/functions.md --- # 常用函数集合 前端项目提供了非常多的常用函数,在文件 `src/support/helper.ts` 中。这些函数可以帮助我们更高效地开发前端应用。 ## 环境变量相关 ### env 获取环境变量的值。 ```typescript env(key: string): any ``` **参数:** * `key`: 环境变量名称 **返回值:** * 对应环境变量的值 **示例:** ```typescript // 获取 API 基础 URL const baseUrl = env('VITE_BASE_URL') // 获取应用名称 const appName = env('VITE_APP_NAME') ``` ### isProd 判断当前环境是否是生产环境。 ```typescript isProd(): boolean ``` **返回值:** * `true`: 当前是生产环境 * `false`: 当前不是生产环境 **示例:** ```typescript if (isProd()) { // 生产环境下的逻辑 } else { // 开发环境下的逻辑 } ``` ## 认证相关 ### rememberAuthToken 保存认证令牌到缓存中。 ```typescript rememberAuthToken(token: string): void ``` **参数:** * `token`: 认证令牌字符串 **示例:** ```typescript // 登录成功后保存令牌 rememberAuthToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...') ``` ### removeAuthToken 从缓存中移除认证令牌。 ```typescript removeAuthToken(): void ``` **示例:** ```typescript // 退出登录时移除令牌 removeAuthToken() ``` ### getAuthToken 获取当前的认证令牌。 ```typescript getAuthToken(): string | null ``` **返回值:** * 认证令牌字符串,如果不存在则返回 `null` **示例:** ```typescript const token = getAuthToken() if (token) { // 用户已登录 } else { // 用户未登录 } ``` ### getBearerToken 获取带有 Bearer 前缀的认证令牌,用于 HTTP 请求头。 ```typescript getBearerToken(): string | null ``` **返回值:** * 带有 Bearer 前缀的认证令牌字符串,如果不存在则返回 `null` **示例:** ```typescript const headers = { Authorization: getBearerToken() } // 发送请求 fetch('/api/user', { headers }) ``` ## 界面相关 ### isMiniScreen 判断当前是否是小屏幕设备(宽度小于 500px)。 ```typescript isMiniScreen(): boolean ``` **返回值:** * `true`: 当前是小屏幕 * `false`: 当前不是小屏幕 **示例:** ```typescript if (isMiniScreen()) { // 小屏幕适配逻辑 } else { // 大屏幕适配逻辑 } ``` ### t 国际化翻译函数,基于 i18n。 ```typescript t(translate: string): string ``` **参数:** * `translate`: 翻译键名 **返回值:** * 翻译后的文本 **示例:** ```typescript // 假设 i18n 配置中有 { "welcome": "欢迎使用" } const message = t('welcome') // 结果: "欢迎使用" ``` ### setPageTitle 设置页面标题。 ```typescript setPageTitle(title: string): void ``` **参数:** * `title`: 页面标题 **示例:** ```typescript // 设置页面标题为 "用户中心" setPageTitle('用户中心') // 如果有站点标题配置,结果会是 "用户中心-站点名称" ``` ## 类型判断 ### isUndefined 判断值是否是 undefined。 ```typescript isUndefined(value: any): boolean ``` **参数:** * `value`: 要判断的值 **返回值:** * `true`: 值是 undefined * `false`: 值不是 undefined **示例:** ```typescript if (isUndefined(user.name)) { // 处理 name 未定义的情况 } ``` ### isFunction 判断值是否是函数。 ```typescript isFunction(value: any): boolean ``` **参数:** * `value`: 要判断的值 **返回值:** * `true`: 值是函数 * `false`: 值不是函数 **示例:** ```typescript if (isFunction(callback)) { callback() } ``` ### isBoolean 判断值是否是布尔类型。 ```typescript isBoolean(value: any): boolean ``` **参数:** * `value`: 要判断的值 **返回值:** * `true`: 值是布尔类型 * `false`: 值不是布尔类型 **示例:** ```typescript if (isBoolean(config.enabled)) { // 处理 enabled 是布尔值的情况 } ``` ### isNumber 判断值是否是数字类型。 ```typescript isNumber(value: any): boolean ``` **参数:** * `value`: 要判断的值 **返回值:** * `true`: 值是数字类型 * `false`: 值不是数字类型 **示例:** ```typescript if (isNumber(age)) { // 处理 age 是数字的情况 } ``` ## 字符串处理 ### ucfirst 将字符串的首字母转为大写。 ```typescript ucfirst(str: string): string ``` **参数:** * `str`: 要处理的字符串 **返回值:** * 首字母大写后的字符串 **示例:** ```typescript const name = ucfirst('john') // 结果: "John" ``` ### lcfirst 将字符串的首字母转为小写。 ```typescript lcfirst(str: string): string ``` **参数:** * `str`: 要处理的字符串 **返回值:** * 首字母小写后的字符串 **示例:** ```typescript const name = lcfirst('John') // 结果: "john" ``` ### randomString 生成指定长度的随机字符串。 ```typescript randomString(e: number = 10): string ``` **参数:** * `e`: 生成的字符串长度,默认为 10 **返回值:** * 生成的随机字符串 **示例:** ```typescript // 生成默认长度(10)的随机字符串 const str1 = randomString() // 生成长度为 5 的随机字符串 const str2 = randomString(5) ``` ### strToFunction 将字符串转换为函数。 ```typescript strToFunction(str: string, params?: any): Function ``` **参数:** * `str`: 要转换的字符串 * `params`: 可选的参数 **返回值:** * 转换后的函数 **示例:** ```typescript // 将字符串转换为函数 const fn = strToFunction('function() { return "Hello World"; }') const result = fn() // 结果: "Hello World" // 带参数的情况 const fn2 = strToFunction('function(name) { return "Hello " + name; }', { name: 'John' }) const result2 = fn2('John') // 结果: "Hello John" ``` ## 文件处理 ### generateFilename 生成唯一的文件名。 ```typescript generateFilename(filename: string): string ``` **参数:** * `filename`: 原始文件名 **返回值:** * 生成的唯一文件名 **示例:** ```typescript const uniqueName = generateFilename('image.jpg') // 结果可能是: "1634567890abcDEF.jpg" ``` ### getFileExt 获取文件的扩展名。 ```typescript getFileExt(filename: string): string ``` **参数:** * `filename`: 文件名 **返回值:** * 文件的扩展名(包含点号) **示例:** ```typescript const ext = getFileExt('document.pdf') // 结果: ".pdf" ``` ### getFilename 从路径中获取文件名。 ```typescript getFilename(filename: string): string ``` **参数:** * `filename`: 文件路径 **返回值:** * 文件名 **示例:** ```typescript const name = getFilename('/path/to/document.pdf') // 结果: "document.pdf" ``` ## 数组处理 ### unique 数组去重。 ```typescript unique(arr: Array): Array ``` **参数:** * `arr`: 要去重的数组 **返回值:** * 去重后的数组 **示例:** ```typescript const arr = [1, 2, 2, 3, 3, 4] const uniqueArr = unique(arr) // 结果: [1, 2, 3, 4] ``` ## 日期处理 ### date 将时间戳转化成指定格式的日期字符串。 ```typescript date(format: string, timestamp: number): string ``` **参数:** * `format`: 日期格式,如 'YYYY-MM-DD HH:mm:ss' * `timestamp`: 时间戳(秒) **返回值:** * 格式化后的日期字符串 **示例:** ```typescript const formattedDate = date('YYYY-MM-DD HH:mm:ss', 1634567890) // 结果可能是: "2021-10-18 15:31:30" ``` ## 其他工具函数 ### \_window 获取全局配置。 ```typescript _window(key: string): any ``` **参数:** * `key`: 配置键名 **返回值:** * 配置值,如果不存在则返回 `null` **示例:** ```typescript const baseUrl = _window('BASE_URL') ``` ### getBaseUrl 获取项目的基础 URL。 ```typescript getBaseUrl(): string ``` **返回值:** * 项目的基础 URL **示例:** ```typescript const baseUrl = getBaseUrl() // 在非生产环境下返回 '/api' // 在生产环境下返回配置的 BASE_URL 或环境变量中的 VITE_BASE_URL ``` ### warpHost 为路径添加主机名,确保路径是完整的 URL。 ```typescript warpHost(path: string | null): string | null ``` **参数:** * `path`: 路径 **返回值:** * 添加主机名后的完整 URL **示例:** ```typescript const fullUrl = warpHost('/images/logo.png') // 如果前后端不在同一域名下,结果可能是: "https://api.example.com/images/logo.png" // 如果前后端在同一域名下,结果是: "/images/logo.png" ``` --- --- url: /faq.md --- # 常见问题 ## 依赖问题 由于 `Laravel11` 刚发布,无法安装。可能是由于镜像更新慢导致的,所以建议取消使用镜像。使用'魔法'(懂得吧)去下载 ### 推荐镜像 ```shell composer config -g repos.packagist composer https://packagist.pages.dev ``` ### 使用腾讯云镜像 使用下面的命令即可 ```shell composer config -g repos.packagist composer https://mirrors.tencent.com/composer/ ``` ### 使用华为云镜像(目前建议) ``` composer config -g repo.packagist composer https://repo.huaweicloud.com/repository/php/ ``` ## 设置了菜单,却只显示子级? 目前菜单设置上,如果是二级菜单,并且只有`一个`二级菜单的情况下,那么只会显示二级菜单。 ## 路由未找到 遇到这个问题首先去查看路由 ``` php artisan route:list ``` 看看接口路由是否在路由表里,如果没有找到,那么就应该看看模块是否开启,在 `storage/app/modules.json` 文件里查看,以权限模块为例 ```json { "title": "权限管理", "name": "permissions", "path": "permissions", "keywords": "权限, 角色, 部门", "description": "权限管理模块", "provider": "\\Modules\\Permissions\\Providers\\PermissionsServiceProvider", "version": "1.0.0", "enable": true // 该字段是否开启 } ``` ## 模块路由命名重复 最近整 PRO 的时候遇到一个问题, CMS 模块和 SHOP 模块都有一个分类控制,名为 `CategoryController`, 正常来说只要这么分组设置路由就可以了 ```php // cms 分类路由 Route::prefix('cms')->group(function () { Route::apiResource('category', CategoryController::class); }); // shop 分类路由 Route::prefix('shop')->group(function () { Route::apiResource('category', CategoryController::class); }); ``` 但是这里会出现一个问题,当使用 ```sh php artisan route:cache ``` 的时候,会提示一个错误 ```shell Unable to prepare route [api/shop/category] for serialization. Another route has already been assigned name [category.index]. ``` 这个问题就是两个路由的 `name` 重复了, 无法进行缓存了。这里就需要设置成这样,只改 shop 的路由即可 ```php // shop 分类路由 Route::prefix('shop')->group(function () { Route::apiResource('category', CategoryController::class)->names('shop_category') }); ``` ## Specified key was too long; max key length is 1000 bytes ```shell SOLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 1000 bytes (... ``` 如果安装的时候遇到这种错误,可以在使用下面的应急方案 :::code-group ```php [app/Providers/AppServiceProvider.php] namespace App\Providers; use Illuminate\Support\ServiceProvider; // 添加这一行 use Illuminate\Support\Facades\Schema; class AppServiceProvider extends ServiceProvider { /** * Register any application services. */ public function register(): void { // } /** * Bootstrap any application services. */ public function boot(): void { // 添加这一行 Schema::defaultStringLength(191); } } ``` ::: --- --- url: /start/develop.md --- # 开发实践 ## AI 开发 [catchadmin 专业版文档-context7](https://context7.com/doc.catchadmin.vip-7a824a1/llmstxt) 如果正在使用任何一家 AI IDE 开发,例如 cursor 或者 claude code 等等。你都可以通过 `context7 mcp` 来协作开发。你只需要安装 [context7 mcp](https://github.com/upstash/context7),然后使用下面的 `mcp` 的配置。 ```json // cursor 示例 { "mcpServers": { "context7": { "url": "https://mcp.context7.com/mcp", "headers": { "CONTEXT7_API_KEY": "YOUR_API_KEY" } } } } ``` 在 cursor 配置如下图,则说明 mcp 成功了 ![catchadmin 专业版-cursor context7 mcp 配置](https://image.catchadmin.com/202509181901584.png) 目前我每次使用,都会先让 AI 通过 context7 mcp 查询文档即可,提示词如下。 ``` 请先通过 context7 mcp 查询 catchadmin 相关文档(这里可以是模型,可以其他功能) ``` ## 升级 下载代码之后,首先需要使用 git 初始化你的仓库。初始化仓库是为了能够跟踪代码变更,方便后续的升级和团队协作。 ```shell git init ``` git 初始化之后会有一个默认分支,这个分支是你自己的主分支(通常是 `master` 或 `main`)。然后需要 checkout 一个特殊的分支出来,就叫 `catch` 分支。这个分支名称是项目特定的,用于接收官方更新。 ```shell git checkout -b catch ``` 这个 `catch`(这是示例分支,可以根据实际用其他名称) 分支主要用来升级,不做任何业务开发。创建完成后,你应该立即切回到自己的 master 分支。在 master 分支上进行业务开发,或者从 master 分支再切出其他功能分支进行开发都可以。重要的是不要在 `catch` 分支上进行任何业务代码的修改,保持它的纯净性,这样才能顺利接收官方更新。 当官方发布新版本时,你需要到官网下载对应的 `patch` 包。这些补丁包包含了从当前版本到新版本的所有代码变更。下载完成之后,把补丁包放到项目根目录,然后切换到 `catch` 分支,使用下面的命令应用补丁: ```shell # 首先切换到catch分支 git checkout catch # 应用补丁文件,--whitespace=nowarn参数可以忽略空白字符的警告 php artisan catch:upgrade xxx.zip(补丁文件) ``` 这个命令会将补丁中的所有变更应用到 `catch` 分支。应用完成后,你需要检查是否有冲突或错误。确认无误后,再切回到你的开发分支(这里以 master 为例),并将 `catch` 分支的变更合并进来: ```shell # 切换回开发分支 git checkout master # 合并catch分支的变更 git merge catch ``` 这样就完成了项目升级。如果合并过程中出现冲突,你需要手动解决这些冲突,确保合并后的代码能够正常工作。合并完成后,建议运行项目测试,确认升级没有引入新的问题。 --- --- url: /uniapp/start.md --- # 开始 `uniapp` 端是基于 [开源项目项目 Unibest](https://codercup.github.io/unibest-docs/)。这是我找了很多 uniapp 启动架子中,算是坑最少了。然后自带的工具也很齐全。包含 * 请求 * 状态 * ui 框架 * 多语言 * 图标 等等,这些基本功能都不需要我们去手动开发。我自己使用下来也非常方便。当然目前他的一些代码细节还没有全部理出来。感兴趣的可以基于它深入开发。 :::warning 这个项目的依赖使用 pnpm,所以第一步你需要先安装 pnpm, 使用 `npm install -g pnpm` 安装 ::: ## 演示图 小程序端,目前配合[微应用](../start/app.md)实现快速启动项目开发,类似 laravel start kit。下图仅作演示,不包含任何实际业务数据。但是基础的用户认证功能已经实现,可以专注于业务实现。 ## 下载项目 在`个人中心` -> `授权仓库` 下载 `uniapp` 版本,解压进入项目根目录之后,使用下面的命令安装命令 :::info 我建议你使用 `pnpm`。其实我自己使用 yarn 安装也是可行的。 ::: 使用下面的命令初始化安装依赖 ```shell pnpm i ``` ## 项目结构 ```shell ├── api # 存放接口目录 │   └── login.ts # 登录接口 ├── env ## 存放环境变量文件 │   ├── .env │   ├── .env.development │   ├── .env.production │   └── .env.test ├── env.d.ts # 存放环境变量类型定义文件 ├── favicon.ico ├── hooks # 存放钩子目录 │   ├── .gitkeep │   └── useNavbarWeixin.ts # 使用微信导航 ├── index.html ├── interceptors # 存放拦截器目录 │   ├── index.ts # 拦截器 │   ├── request.ts # 请求拦截器 │   └── route.ts # 路由拦截器 ├── layouts # 存放布局目录 │   ├── default.vue │   └── demo.vue ├── pages # 存放页面目录 │   ├── index # 首页 │   │   └── index.vue │   ├── login # 登录页面 │   │   └── index.vue │   └── my # 个人中心 │   └── index.vue ├── pages-sub # 存放子页面目录 │   └── demo.vue ├── shell │   └── postinstall.js ├── static # 存放静态资源目录 │   ├── logo.png │   ├── logo.svg │   └── tabbar │   ├── example.png │   ├── exampleHL.png │   ├── home.png │   ├── homeHL.png │   ├── personal.png │   └── personalHL.png ├── store # 存放 store 目录 │   ├── count.ts # │   ├── index.ts # │   └── user.ts # ├── utils # 工具目录 │   ├── http.ts # http 客户端 │   ├── index.ts # 工具函数 │   └── toPath.ts # 路径跳转 ├── tsconfig.json ├── types │   └── auto-import.d.ts ├── typing.ts ├── commitlint.config.cjs ├── uni.scss ├── uno.config.ts # unocss 配置 ├── unocss.css # unocss 样式 ├── pages.config.ts # 存放页面配置文件 ├── project.config.json ├── project.private.config.json ├── App.vue # 主入口文件 ├── main.js # 主入口文件 ├── manifest.config.ts ├── package.json ├── .editorconfig ├── .eslintignore ├── .eslintrc-auto-import.json ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── .stylelintignore ├── .stylelintrc.cjs ├── LICENSE ├── README.md └── vite.config.ts # vite 配置 ``` ## 配置 在开发前,你还需要进行一些基本配置,打开专业版的后台管理。如下图所示,请配置对应的小程序的 `appid` 和 `appsecret` ![小程序配置](https://image.catchadmin.com/202412141254345.png) 然后配置请求的接口, 找到 `env/.env` 文件,添加以下内容 ```ts VITE_APP_TITLE = // 应用名称 VITE_SERVER_BASEURL = // 请求的接口地址 ``` ## 开发使用 :::info 开发直接使用 vscode 即可 ::: 使用下面的命令启动开发模式 ```shell pnpm dev:mp ``` 启动之后打开小程序开发者工具,导入编译的目录,在项目根目录的 `dist/dev/mp-weixin`。可以看到下面的页面 ![小程序开发配置](https://image.catchadmin.com/202505241030306.png) ## 打包 使用下面命令打包小程序 ```shell pnpm build:mp-weixin ``` 打包之后小程序在项目根目录 `dist\build\mp-weixin`,可以通过微信开发者工具上传打包目录 --- --- url: /server/async.md --- # 异步任务 ## 概述 在 Web 应用开发中,有些操作可能需要较长的处理时间,如果在 HTTP 请求中直接处理这些操作,会导致用户等待时间过长,影响用户体验。异步任务就是为了解决这类问题而设计的,它允许将耗时操作放到后台执行,用户无需等待即可得到响应。 ### 什么是异步任务? 异步任务是指将耗时的操作从主请求流程中分离出来,放到后台独立执行的一种机制。这样可以: * 提高系统响应速度,避免用户长时间等待 * 合理分配系统资源,避免资源占用过高 * 提供任务执行状态的可视化监控 ### 适用场景 异步任务适用于以下场景: * **数据导入导出**:处理大量数据的导入导出操作 * **发送邮件通知**:批量发送邮件或消息通知 * **数据统计分析**:需要长时间运行的数据统计和分析 * **文件处理**:大文件的上传、处理、转换等操作 * **第三方 API 调用**:调用响应较慢的外部服务 CatchAdmin 后台提供了一个完整的异步任务解决方案,包括可视化界面和易于接入的 API,使开发者能够轻松实现自己的后台异步任务。如下所示: ::info 本功能是基于任务调度的多进程,有延迟。如果需要即时的队列服务,建议使用 Laravel 自带的队列,但是需要自己维护了 ::: ![catchadmin 后台异步任务 Laravel Admin](https://image.catchadmin.com/20240516084032.png) 目前后台导入导出功能已经实现了异步任务处理,那么该如何自定义实现自己的异步任务呢?本文将详细介绍。 :::warning 异步任务执行默认是关闭的。请先到根目录下的 `routes/console.php` 按照提示打开即可 ::: :::info [视频教程](https://www.bilibili.com/video/BV1YQW3e3EXD/) ::: ## 异步任务表结构 ```php Schema::create('async_task', function (Blueprint $table) { $table->id(); $table->string('task')->comment('task任务对应的 class 名称'); $table->string('params')->default('')->comment('任务所需参数'); $table->integer('start_at')->default(0)->comment('开始时间'); $table->tinyInteger('status')->default(1)->comment('状态:un_start=1,running=2,finished=3,error=4'); $table->integer('time_taken')->default(0)->comment('运行耗时'); $table->string('error')->default('')->comment('执行结果错误'); $table->string('result')->default('')->comment('执行结果'); $table->string('retry')->default(0)->comment('重试次数'); $table->createdAt(); $table->updatedAt(); $table->deletedAt(); }); ``` ## 定义 需要实现自定义的后台异步任务,需要实现一个异步任务接口 `AsyncTaskInterface` ```php namespace Catch\Contracts; interface AsyncTaskInterface { /** * push task */ public function push(): mixed; /** * run task */ public function run(array $params): mixed; } ``` ## 接口实现 ```php use Modules\System\Models\AsyncTask; class AsyncTask implements AsyncTaskInterface { // push 方法将当前任务推送到任务队列 public function push(): mixed { return app(AsyncTask::class) ->storeBy([ 'task' => get_called_class(), 'params' => '', // 参数需要自定义实现 ]); } // run 方法 任务具体业务线,它接受传进来的参数 public function run(array $params): mixed { // params 就是 push 进去的 params 参数 // 自定义实现任务 } } ``` ## 使用 在需要使用 `AsyncTask` 的地方,将任务推送到任务列表中 ```php $async = new AsyncTask() $async->push() ``` ## 添加定时任务 到 `系统管理`->`定时任务` 页面,添加如下图的定时任务配置 ![catchadmin 异步任务](https://image.catchadmin.com/202506041127530.png) ## 启动 :::warning 下面的命令是本地测试使用,切勿在线上使用 ::: ```shell php artisan schedule:work ``` ## 示例 在用户模块`modules/User` 目录下,提供了一个完整的示例,基于用户导出。 :::warning 导出是经过包装的,不要直接使用,你需要自己实现异步接口的 push 和 run 方法来接入实际业务 ::: ::: code-group ```php [modules/User/Export/User.php] namespace Modules\User\Export; use Catch\Contracts\AsyncTaskInterface; use Catch\Support\Excel\Export; use Modules\System\Support\Traits\AsyncTaskDispatch; class User extends Export implements AsyncTaskInterface { // 推送任务 trait use AsyncTaskDispatch; protected array $header = [ 'id', '昵称', '邮箱', '创建时间', ]; // 查询出来导出的数据 public function array(): array { // TODO: Implement array() method. return \Modules\User\Models\User::query() ->select('id', 'username', 'email', 'created_at') ->without('roles') ->get([ 'id', 'username', 'email', 'created_at', ])->toArray(); } } ``` ```php [AsyncTaskDispatch] // 推送 trait namespace Modules\System\Support\Traits; use Modules\System\Models\AsyncTask; /** * @method array getParams() */ trait AsyncTaskDispatch { public function push(): mixed { // 直接推送异步任务表 return app(AsyncTask::class) ->storeBy([ 'task' => get_called_class(), 'params' => count($this->getParams()) ? json_encode($this->getParams()) : '', ]); } } ``` ::: ### 使用 ::: code-group ```php [modules/User/Http/Controllers/UserController.php] public function export(\Modules\User\Export\User $export): mixed { return $export->push(); } ``` ::: 到 `用户管理` 页面点击导出之后,会生成这样一条任务。如下图 ![catchadmin 异步任务](https://image.catchadmin.com/202506041125798.png) 执行成功之后会生成如下图的 excel 文件 ![catchadmin 异步任务](https://image.catchadmin.com/202506041130268.png) --- --- url: /start/api.md --- # 接口文档 最近看到社区很多项目都在使用`注释/注解`生成接口文档,本来从我个人角度来看,项目中使用这种方式还不如自己手动写接口。但是看到那么多人觉得这种方式很方便,我觉得还是自己来试试,没想到体验下来还真的觉得不错。IDE 加上 AI 补全,效率真的很高 😆。这多亏了 Laravel 丰富的社区,让我能体会到这种便捷性。 :::info [【Laravel scribe】 功能强大的注释接口文档生成包](https://github.com/knuckleswtf/scribe/) ::: :::warning 使用注释生成接口文档的功能,`catchadmin/core` 版本要大于等于 `5.0` ::: ## 使用 :::info 下面的对于该包的介绍,仅限在项目中使用到的,其实这个包有很强大的功能,如果 CatchAdmin 无法实现你对于功能的要求,可以通过它的文档来进行扩展 ::: ### 分组 `分组功能`主要针对控制器,在控制器上使用对应的 `Doc 注解` 即可 ```php /** * @group 公共模块 * * @subgroup 地区管理 * @subgroupDescription CatchAdmin 后台地区管理 */ class AreaController { // } ``` * `@group` 一级目录 (管理对应的控制器) * `@subgroup` 二级目录 (对应的就是控制器) * `@subgroupDescription` 二级目录描述 ### 接口 接口对应的就是控制器的方法,所以大部分的功能都是实现在这个上面,来看一个稍微复杂点的 ```php /** * 更新用户 * * @urlParam id int required 用户ID * * @bodyParam username string required 用户名 * @bodyParam password string 密码 * @bodyParam email string 邮箱 * @bodyParam mobile string 手机号 * @bodyParam department_id int 部门 * @bodyParam roles integer[] 角色 * @bodyParam jobs integer[] 职位 * * @responseField data bool 是否更新成功 * * @param $id * @param UserRequest $request * @return mixed */ public function update($id, UserRequest $request): mixed { // } ``` * `@urlParam` `path` 参数 例如: `/user/{id}` ``` @urlParam 字段名 类型 [required(可选)] 注释 ``` * `@bodyParam` `body` 参数,实际就是表单对应的部分 ``` @bodyParam 字段名 类型 [required(可选)] 注释 ``` * `@responseField` 响应的字段 ``` @responseField 字段名 类型 注释 ``` * `@queryParam` `query` 参数,例如: `/user?page=1&limit=1` ``` @queryParam 字段名 类型 [required(可选)] 注释 ``` :::info 如果添加了 `required`,则表示该字段必填,如果没有,则表示可选 ::: ### 参数对应的有效类型 * `string` 字符串 * `integer` 整型 * `number` 数字 * `boolean` bool * `object` 对象 * `file` 文件 * `[]` 数组 例如 `integer[]`, `string[]` ### 认证 默认 `CatchAdmin` 后台接口都需要授权的,如果生成文档的时候不需要授权,可以这么使用 ```php /** * 登录 * * @unauthenticated * */ public function login(Request $request, AuthService $auth): array { // 登录 } ``` * `@unauthenticated` 使用该 Docblock 即可自动在文档标记 ### 生成文档 CatchAdmin 提供了非常简单命令,生成专业版对应的接口文档。如果你开发的后台是基于 CatchAdmin 模块模式开发的,那么这个命令可以帮你一键生成。 但是如果你想生成 C 端的对应的接口文档,则需要自己使用原始包来生成,因为生成对应的文档需要一定规则。你可以根据自己的项目设置。CatchAdmin 生成接口文档都有一些预配置。你可以查看 `config/catch_api_doc.php` 配置文件 ```shell php artisan catch:api:doc ``` 使用该命令,会在项目的根目录 `api-doc` 文件夹中生成接口文档细则。`api-doc` 是基于 Vitepress 生成的文档站点,生成接口之后,安装 vitepress 的依赖 ```php yarn install // 启动文档 yarn docs:dev ``` 启动之后,你会看到这样的站点 ![CatchAdmin 专业版接口文档](https://image.catchadmin.com/202409171400758.png) ### 导入到 Apifox 如果你需要类似 apifox 这样的客户端,可以在使用 `php artisan catch:api:doc` 命令之后,也同时会在 `api-doc` 根目录下生成 `postman.json` 元数据文件。 如图所示 ![CatchAdmin 专业版接口文档](https://image.catchadmin.com/202409171234566.png) `项目设置` > `导入数据` 选择 `Postman`,点击 `导入` 该 Json 文件即可 --- --- url: /develop/api-log.md --- # 接口监控 ![CatchAdmin 专业版接口监控](https://image.catchadmin.com/202506151218914.png) 接口监控功能可以很好的监控接口的请求情况,分析接口的监控以及频率 :::warning 接口监控使用强依赖 Redis,所以你必须先要开启 Redis ::: 监控功能默认是关闭,需要通过配置开启 ```php return [ // 默认关闭,在 .env 添加 CATCH_SYSTEM_API_LOG 配置 'system_api_log' => env('CATCH_SYSTEM_API_LOG', false), ]; ``` 在`.env`文件配置,如下配置即可 ```php CATCH_SYSTEM_API_LOG=true ``` ## 表结构 表结构设计 ```sql CREATE TABLE `system_connector_log` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID', `username` VARCHAR ( 100 ) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', `path` VARCHAR ( 255 ) COLLATE utf8mb4_general_ci NOT NULL COMMENT '接口地址', `method` VARCHAR ( 255 ) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'GET' COMMENT '请求方法', `user_agent` VARCHAR ( 255 ) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'ua', `ip` VARCHAR ( 50 ) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'ip地址', `controller` VARCHAR ( 100 ) COLLATE utf8mb4_general_ci NOT NULL COMMENT '控制器', `action` VARCHAR ( 50 ) COLLATE utf8mb4_general_ci NOT NULL COMMENT '方法', `time_taken` INT NOT NULL COMMENT '耗时(ms)', `status_code` INT NOT NULL COMMENT '状态码', `from` TINYINT NOT NULL DEFAULT '1' COMMENT '请求来源:1=后台,2=前台', `creator_id` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建人ID', `created_at` INT NOT NULL COMMENT '请求开始时间', `updated_at` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT '更新时间', `deleted_at` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT '软删除', PRIMARY KEY ( `id` ) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '接口日志'; CREATE TABLE `system_connector_log_statistics` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', `path` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '接口地址', `average_time_taken` int NOT NULL DEFAULT '0' COMMENT '平均耗时(ms)', `count` int NOT NULL DEFAULT '0' COMMENT '总请求次数', `fail_count` int NOT NULL DEFAULT '0' COMMENT '失败次数', `success_count` int NOT NULL DEFAULT '0' COMMENT '成功次数', `created_at` int unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', `updated_at` int unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', `deleted_at` int unsigned NOT NULL DEFAULT '0' COMMENT '软删除', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='接口日志日统计'; ``` ## 配置 Redis 在根目录`.env`文件,找到如下配置,[laravel redis 文档](https://laravel-docs.catchadmin.com/docs/12/database/redis) ```php #php redis 扩展 REDIS_CLIENT= REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 ``` ## 配置定时任务 配置好 redis 之后,需要配置对应的接口消费的任务。目前项目采用 crontab 的方式。找到 `routes/console.php` 配置 ```php // 接口日志消费,每分钟执行一次 Schedule::command('connector:log:record')->everyMinute(); // 接口告警 Schedule::command('connector:frequency:warning')->everyMinute(); // 接口统计,每日一次 Schedule::command('statistics:connector:log')->daily(); ``` 如果在本地开发,可以运行下面的命令 ```shell php artisan schedule:work ``` ## 监听事件 如果需要应用到 C 端或者其他任意端,可以直接通过事件 ```php use Modules\System\Events\ConnectorLogEvent; // 记录接口日志,在需要日志的地方分发事件即可 Event::dispatch(new ConnectorLogEvent( $user?->username, $user?->id, ConnectorLog::FROM_DASHBOARD )); ``` 事件需要三个参数 * `用户名` 可以为 null * `用户ID` 可以为 null * `应用的接口平台` 默认是后台,系统内置一个 app 枚举值 `ConnectorLog::FROM_APP`,具体根据实际业务更改添加 --- --- url: /uniapp/api.md --- # 接口请求 在 `uniapp` 项目中,我们规定了接口的统一目录`api`目录。再上一节的目录结构中有所说明。 ## GET 请求 ```ts import { http } from '@/utils/http' return http({ url: `/someurl`, method: 'GET', query: { name: 'value' } }) ``` ## POST 请求 ```ts import { http } from '@/utils/http' return http({ url: `/someurl`, method: 'POST', data: { account, password, type: 'password' } }) ``` ## 文件上传 ```ts import { uniFileUpload } from '@/utils/http' return uniFileUpload({ url: `/someurl`, method: 'POST', filePath: filePath, // 文件地址 name: 'avatar', // 文件名称,这里叫 avatar formData: { // form 表单数据 ...params, type } }) ``` ## 如何使用 找到 `api/login.ts`,首先查看下面的代码。创建一个使用账户密码登录 `login` 方法。 ```ts export const login = (account: string, password: string) => { return http({ url: `/login`, method: 'POST', data: { account, password, type: 'password' } }) } ``` ### 在页面引入 ```ts // 首先引入 login 方法 import { login } from '@/api/login' login('account', 'password').then((r) => { // 处理返回的响应 }) ``` --- --- url: /server/pay.md --- # 支付功能 专业版基于 [yansongda/pay](http://pay.yansongda.cn/) 包装一份非常易用的支付工厂类。只需要在后台保存支付配置之后,使用下面的调用即可 ```php // 公众号支付 \Modules\Pay\Support\PayFactory::make(\Modules\Pay\Enums\Pay::WECHAT_PAY) ->mp([ 'subject' => '公众号支付测试', 'amount' => 1, 'openid' => 'xxxxxxxxxxxxxxx' ]); ``` :::warning 支付功能提供的是最基础的支付功能,不包含任何业务。功能代码需要根据实际业务编写 ::: ## 支付接口 提供三个支付接口,如下 ```php use Modules\Pay\Support\NotifyData\NotifyData; interface PayInterface { // 退款 public function refund(array $params): mixed; // 回调 public function notify(): mixed; } ``` ## 支付基础类 专业版在基础类实现了创建订单号逻辑,这个订单号只是默认填充的,需要根据自己的实际业务来实现 ```php abstract class Pay implements PayInterface { /** * 支付实例 */ abstract protected function instance(): mixed; /** * 使用代理方法支付 * * @return mixed * * @throws RandomException */ public function __call($name, $params) { $params = array_merge(['action' => $name], ...$params); $payEvent = new PayEvent($this->createTradeData($params)); $params = Event::dispatch($payEvent); return $this->instance()->{$name}($params[0]); } /** * 回调 * * @throws ContainerException * @throws InvalidParamsException */ public function notify(): mixed { $notifyData = $this->instance()->callback()->toArray(); $notify = $this->getNotifyData($notifyData); try { Event::dispatch(new PayNotifyEvent($notify)); } catch (\Throwable $e) { // 失败如何处理 } finally { return $this->instance()->success(); } } /** * 获取回调数据 */ abstract protected function getNotifyData(array $data): NotifyData; /** * 订单号前缀 */ abstract protected function orderNoPrefix(): string; /** * @return PayPlatform */ abstract protected function platform(): PayPlatform; /** * 创建订单号 * * @throws RandomException */ protected function createOrderNo(): string { $prefix = $this->orderNoPrefix(); return $prefix.date('YmdHis').random_int(1000000, 9999999).Str::random(10); } /** * 创建退款订单号,退款订单号加个 R 字符 * * @throws RandomException */ public function createRefundOrderNo(): string { return 'R'.$this->createOrderNo(); } /** * @return array|string[] * * @throws RandomException */ protected function createTradeData(array $params): array { $params['order_no'] = $this->createOrderNo(); $params['platform'] = $this->platform(); return $params; } /** * 加载支付配置 * * @throws ContainerException|DdException */ protected function loadPayConfig(string $key): void { PayProvider::config(PayConfig::get($key)); } } ``` ## 具体实现的支付 以支付宝为例,如下代码 ```php /** * @method ResponseInterface|Rocket web(array $order) 网页支付 * @method ResponseInterface|Rocket h5(array $order) H5 支付 * @method ResponseInterface|Rocket app(array $order) APP 支付 * @method Rocket|Collection mini(array $order) 小程序支付 * @method Rocket|Collection pos(array $order) 刷卡支付 * @method Rocket|Collection scan(array $order) 扫码支付 * @method Rocket|Collection transfer(array $order) 账户转账 */ class AliPay extends Pay { public function refund(array $params): mixed { $refundData = $this->createRefundData([ 'refund_amount' => $params['amount'], ]); } /** * @return \Yansongda\Pay\Provider\Alipay * * @throws \Yansongda\Artful\Exception\ContainerException */ protected function instance(): mixed { PayProvider::config(config('pay.alipay')); return PayProvider::alipay(); } /** * 包装回调数据 */ protected function getNotifyData(array $data): NotifyData { return new AliPayNotifyData($data); } protected function orderNoPrefix(): string { return 'A'; } } ``` ## 支付参数 由于各个平台的支付参数都不相同,不仅平台参数有所差异,每个平台的支付方式的参数也所有差异,我们需要同意管理起来,所以也对支付参数进行了格式化。以支付宝的为例 ```php // 在 modules/Pay/Support/PayParams common(); } /** * @return array */ public function h5(): array { return array_merge($this->common(), [ 'quit_url' => $this->params['quit_url'], ]); } /** * app 参数 * * @return array */ public function app(): array { return $this->common(); } /** * 小程序参数 * * @return array */ public function mini(): array { return array_merge($this->common(), [ 'buyer_id' => $this->params['buyer_id'], ]); } /** * pos 机参数 * * @return array */ public function pos(): array { return array_merge($this->common(), [ 'auth_code' => $this->params['auth_code'], ]); } /** * 扫码参数 * * @return array */ public function scan(): array { return $this->common(); } /** * 公共参数 * * @return array */ protected function common(): array { return [ 'out_trade_no' => $this->params['order_no'], 'total_amount' => $this->params['amount'], 'subject' => $this->params['subject'] ?? '支付', ]; } } ``` 这样就可以统一管理参数了。将这些都散落的数据都聚合格式化之后。在对相关业务包装和后续业务维护都很有利。尽可能屏蔽上下文的依赖,独立使用它。 ## 回调数据接口 这里为了隐藏支付回调业务处理的细节,对回调数据进行了一次包装。 ```php // 回调数据接口 interface NotifyDataInterface { /** * 是否支付成功 * * @return bool */ public function isPaySuccess(): bool; /** * 是否退款成功 * * @return bool */ public function isRefundSuccess(): bool; /** * 是否是退款 * * @return bool */ public function isRefund(): bool; /** * 获取支付平台的订单号 * * @return string */ public function getOutTradeNo(): string; /** * 获取本地订单号 * * @return string */ public function getTradeNo(): string; } ``` 例如支付宝的回调数据处理,必须实现接口的几个方法,这样在回调数据处理的时候就不需要关心具体实现了。 ```php /** * 微信回调数据 */ class AliPayNotifyData implements NotifyDataInterface { /** * 是否支付成功 */ public function isPaySuccess(): bool { return $this->getTradeState() == 'trade_success'; } /** * 是否退款成功 */ public function isRefundSuccess(): bool { return $this->getRefundStatus() == 'success'; } /** * 获取退款状态 */ public function getRefundStatus(): string { return strtolower($this->data['resource']['refund_status']); } /** * 交易是否完成 * * @return bool */ public function isTradeFinished(): bool { return $this->getTradeState() == 'trade_finished'; } /** * 交易是否等待付款 * * @return bool */ public function isWaitBuyerPay(): bool { return $this->getTradeState() == 'wait_buyer_pay'; } /** * 交易是否关闭 * * @return bool */ public function isTradeClose(): bool { return $this->getTradeState() == 'trade_closed'; } /** * 获取交易状态 * * @return string */ public function getTradeState(): string { return strtolower($this->data['trade_status']); } /** * 是否是退款回调 */ public function isRefund(): bool { return $this->data['resource']['original_type'] == 'refund'; } /** * 获取平台交易订单号 * * @return string */ public function getOutTradeNo(): string { return $this->data['trade_no']; } /** * 获取本地交易订单号 * * @return string */ public function getTradeNo(): string { return $this->data['out_trade_no']; } } ``` ### 回调实现 以支付宝为例,只需要实现 `RefundNotify` 和 `PayNotify` 两个回调方法即可。对于处理本地的相同的业务逻辑,可以写在 `Notify` 基础类中。这两个方法是为了区分不同平台数据处理业务逻辑。如果相同,你可以提取到基类中 ```php namespace Modules\Pay\Support\Notify; class AliPayNotify extends Notify { public function refundNotify(): mixed { // 根据实际业务处理 } public function payNotify(): mixed { // 根据实际业务处理 } } ``` ## 事件 支付模块提供两个事件 * `Modules\Pay\Events\PayEvent` * `Modules\Pay\Events\PayNotifyEvent` ### 监听者 * `Modules\Pay\Events\PayListener` * `Modules\Pay\Events\PayNotifyListener` ### 实现 你可以看到这两个监听者的实现也很简单,因为我们已经将整个支付的差异都屏蔽到了不同的上下文中。你只需要关注那一小块业务处理上,而无需关注支付中所有细节处理。 ::: code-group ```php [PayNotifyListener.php] class PayNotifyListener { /** * Create the event listener. * * @return void */ public function __construct() { // } /** * Handle the event. * * @param PayNotifyEvent $event * @throws \Throwable */ public function handle(PayNotifyEvent $event): void { // $notifyData = $event->data; if ($notifyData instanceof AliPayNotifyData) { (new AliPayNotify($notifyData))->notify(); } if ($notifyData instanceof WechatPayNotifyData) { (new WechatPayNotify($notifyData))->notify(); } if ($notifyData instanceof UniPayNotifyData) { (new UniPayNotify($notifyData))->notify(); } if ($notifyData instanceof DouYinPayNotifyData) { (new DouYinPayNotify($notifyData))->notify(); } } } ``` ```php [PayListener.php] class PayListener { /** * Create the event listener. * * @return void */ public function __construct(){} /** * Handle the event. * * @param PayEvent $event * @return array */ public function handle(PayEvent $event): array { // $params = $event->params; $order = Order::createNew([ 'subject_id' => $params['subject_id'], 'order_no' => $params['order_no'], 'amount' => $params['amount'], 'platform' => $params['platform'], 'user_id' => $params['user_id'], 'action' => $params['action'], 'pay_status' => PayStatus::Pending, ]); if ($order) { return match ($params['platform']) { PayPlatform::Alipay => (new AliPayParams($params))->{$params['action']}(), PayPlatform::UnionPay => (new UniPayParams($params))->{$params['action']}(), PayPlatform::Wechat => (new WechatPayParams($params))->{$params['action']}(), PayPlatform::Douyin => (new DouyinPayParams($params))->{$params['action']}(), default => [], }; } return []; } } ``` ::: 这是整个支付模块的思想,当然具体业务实现还需要根据实际业务功能添加。但是这已经在很大程度上屏蔽了支付功能的实际复杂度。只需要关注业务实现即可 --- --- url: /server/import.md --- # 数据导入功能 数据导入是后台管理系统的重要功能,用于批量处理和同步数据。CatchAdmin 专业版提供了完整的 Excel 数据导入解决方案,基于 Laravel-Excel 深度封装,提供简洁易用的导入器。只需几行代码即可实现完整的数据导入功能。以下通过用户批量导入示例进行说明。 :::tip 本案例代码都写在了用户模块的 UserController 中 ::: ## 用户导入器 假设需要导入以下格式的 Excel 表格数据 ```php // excel 格式如下 id 昵称 邮箱 密码 1 test tests@admin.com 123456 2 tests test@admin.com 123456 ``` 要实现 Excel 数据导入,需要创建自定义导入器类。CatchAdmin 专业版提供了简化的导入器实现方式,只需继承 `Import` 基础组件即可快速构建数据导入功能。实现示例如下 ```php namespace Modules\User\Import; use Catch\Support\Excel\Import; use Illuminate\Support\Collection; use Modules\User\Models\User as UserModel; class User extends Import { /** * Excel 数据处理方法 * @param Collection $users Excel 表格数据集合 */ public function collection(Collection $users) { // Excel 数据已被转换为 Laravel Collection 对象 // 每行数据为一个数组,可直接进行批量处理 $users->each(function ($user) { // 创建用户模型实例 $userModel = new UserModel(); $userModel->username = $user[1]; // 昵称字段 $userModel->email = $user[2]; // 邮箱字段 $userModel->password = $user[3]; // 密码字段 $userModel->save(); // 保存用户数据 }); } } ``` 完成导入器定义后,在控制器中创建数据导入接口。在 `UserController` 中添加导入方法 ```php /** * 用户数据批量导入接口 */ public function import(Request $request, \Modules\User\Import\User $import) { // 获取上传的 Excel 文件 $file = $request->file('file'); // 执行数据导入处理 return $import->import($file); } ``` 配置数据导入路由 ```php // 用户批量导入路由 Route::post('user/import', [UserController::class, 'import']); ``` 后端接口开发完成后,前端只需配置导入组件即可。使用 `CatchTable` 组件时,设置导入路由参数 ```vue ``` 这样就完成了完整的数据导入功能,实现了前后端的无缝对接。 如果不使用 `CatchTable` 组件,也可以直接使用 `Import` 组件 ```jsx ``` ## 异步导入 对于大批量数据导入场景,建议使用异步导入避免请求超时。CatchAdmin 专业版支持异步导入任务,只需对导入器进行简单改造。修改 `UserImport` 导入器类 ```php namespace Modules\User\Import; use Catch\Contracts\AsyncTaskInterface; use Catch\Support\Excel\Import; use Illuminate\Support\Collection; use Modules\System\Support\Traits\AsyncTaskDispatch; use Modules\User\Models\User as UserModel; // 实现异步任务接口,支持后台异步处理 class User extends Import implements AsyncTaskInterface { // 引入异步任务调度 Trait use AsyncTaskDispatch; /** * 异步批量处理用户数据 */ public function collection(Collection $users) { $users->each(function ($user) { $userModel = new UserModel(); $userModel->username = $user[1]; // 昵称字段 $userModel->email = $user[2]; // 邮箱字段 $userModel->password = $user[3]; // 密码字段 $userModel->save(); // 保存到数据库 }); } } ``` 修改控制器中的 `import` 方法,启用异步导入模式 ```php /** * 异步批量导入用户数据 */ public function import(Request $request, \Modules\User\Import\User $import) { // 启用异步模式进行数据导入 return $import->async()->import($request->file('file')); } ``` 完成异步导入配置后,需要启动任务调度程序处理导入任务。在 `app/Console/Kernel.php` 中配置定时任务 :::tip 异步导入与异步导出使用相同的任务调度机制 ::: ```php /** * 配置异步任务调度 */ protected function schedule(Schedule $schedule): void { // 每分钟执行一次异步任务处理 $schedule->command('async:task')->everyMinute(); } ``` 异步导入任务的执行状态、进度信息和处理结果,可在后台管理系统的`系统管理/异步任务`模块中实时监控和管理 --- --- url: /server/export.md --- # 数据导出功能 数据导出功能是后台管理系统的核心需求,CatchAdmin 专业版提供了完整的 Excel 数据导出解决方案。基于 [Laravel Excel](https://docs.laravel-excel.com/3.1/getting-started/) 进行深度封装,支持简单的数据导出和高性能的大数据量导出。同时集成 `xlswriter` 扩展,可实现百万级数据导出且内存占用极低 ## 基本使用 CatchAdmin 专业版提供了抽象基础类,继承该类可快速实现 Excel 数据导出功能。以下是用户数据导出的实现示例 ```php namespace Modules\User\Export; // 该类就是基础核心类,通过集成它可以快速实现导出 use Catch\Support\Excel\Export; class User extends Export { // 这里需要设置导出的头信息 protected array $header = [ 'id', '昵称', '邮箱', '创建时间' ]; // 实现 array 方法,返回导出的数据 public function array(): array { // TODO: Implement array() method. return \Modules\User\Models\User::query() ->select('id', 'username', 'email', 'created_at') ->without('roles') ->get([ 'id', 'username', 'email', 'created_at' ])->toArray(); } } ``` 只需要轻松的几行代码,便可实现导出 Excel。这只是实现了导出类,那么如何使用呢? 也很简单,例如在 `UserController` 添加 export 方法,注入导出类 ```php public function export(\Modules\User\Export\User $export) { return $export->download(); } ``` 直接通过 `download` 方法实现 `excel` 的下载。这个看起来似乎还比较繁琐,每次都要写一个导出类。一般平时导出的需求没有这么复杂,那么有没有更加简便的方法?答案是有的。看下面的例子 ```php public function export() { return User::query() ->select('id', 'username', 'email', 'created_at') ->without('roles') ->get() ->download(['id', '昵称', '邮箱', '创建时间']); } ``` CatchAdmin 专业版通过扩展 Laravel Collection 添加 `download` 方法,无需编写专门的导出类即可快速实现 Excel 数据导出功能。 对于大数据量导出场景,同步导出可能导致请求超时或内存溢出。当需要导出数万条数据且包含复杂计算查询时,虽然可以使用 Laravel Queue,但缺乏任务状态监控和集中管理功能。CatchAdmin 专业版提供了异步任务管理机制,支持导出任务的状态追踪和进度监控 ```php select('id', 'username', 'email', 'created_at') ->without('roles') ->get([ 'id', 'username', 'email', 'created_at' ])->toArray(); } } ``` 好了,任务搞好了,再来看看控制器如何写 ```php /** * @return void */ public function export(\Modules\User\Export\User $export) { // 控制器就更加简单了,直接 push 推送即可 return $export->push(); } ``` 完成异步任务配置后,需要启动任务调度程序。在 `app/Console/Kernel.php` 中添加定时任务调度 ```php protected function schedule(Schedule $schedule): void { $schedule->command('async:task')->everyMinute(); } ``` 异步任务的执行状态、进度信息和详细日志,可在后台管理系统的`系统管理/异步任务`模块中实时查看和管理 ## 大数据量导出 ### Xlswriter [Xlswriter](https://xlswriter-docs.viest.me/) 是高性能的 Excel PHP 扩展,专为大数据量处理优化。 CatchAdmin 专业版对 `xlswriter` 进行了深度封装,支持百万级数据导出且内存占用极低。通过优化的数据处理机制,可实现大规模数据导出而不会导致内存溢出。由于大数据量导出耗时较长,建议配合异步任务使用 :::info 只支持游标(cursor)查询模式,也就是惰性查询。非惰性会退化成 Laravel Excel 实现 ::: #### 快捷导出 ```php // 使用 Xlswriter 进行高性能数据导出 User::query() ->select('id', 'username', 'email', 'created_at') ->cursor() // 使用游标查询,避免内存溢出 ->download(['id', '昵称', '邮箱', '创建时间']); ``` 支持游标模式分块导出,进一步优化内存使用 ```php // 结合游标查询和分块处理,提升性能 User::query() ->select('id', 'username', 'email', 'created_at') ->cursor() // 游标查询模式 ->chunk(5) // 分块处理,每次处理 5 条记录 ->download(['id', '昵称', '邮箱', '创建时间']); ``` #### 单独导出类 也可以写一份单独导出类,可支持异步 ```php select('id', 'username', 'email', 'created_at') ->limit(100) ->cursor() ->chunk(10); } /** * 设置样式 */ public function formats(): array { $handle = $this->excelObject->getHandle(); $format = new \Vtiful\Kernel\Format($handle); $boldStyle = $format->bold()->fontSize(10)->toResource(); return [ ['A1', 10, $boldStyle] ]; } /** * 编辑密码保护,注意不是文档密码保护 * * @return string|null */ public function password():?string { return '123456'; } } ``` ### CSV 方案 对于超大规模数据导出,推荐使用 CSV 格式。虽然异步任务支持大批量数据导出,但 Excel 格式在极大数据量下仍可能出现内存不足问题。CatchAdmin 专业版的 CSV 导出方案经过优化,支持百万级数据导出且内存占用极低。在测试环境中成功导出 130 万条数据,内存消耗控制在合理范围内 ```php // CSV 大数据量导出示例 return User::query() ->select('id', 'username', 'email', 'created_at') ->without('roles') // 必须使用游标查询以确保低内存占用 ->cursor() ->downloadAsCsv(['id', '昵称', '邮箱', '创建时间']); ``` :::warning 记住一定要使用 `cursor` 游标查询数据,否则会导出失败 ::: --- --- url: /server/datarange.md --- # 数据权限管理 ## 介绍 数据权限功能主要用于企业级应用的精细化权限控制,默认不启用。数据权限与用户角色紧密关联,支持多种权限控制策略,如图 ![cathadmin 专业版数据权限介绍](https://s1.ax1x.com/2023/01/16/pSlzXdO.png) 角色权限支持以下数据访问控制类型 * 全部数据权限 * 自定数据权限 * 部门数据权限 * 部门及以下数据权限 * 仅本人数据权限 :::info [cathadmin 专业版数据权限介绍](https://www.bilibili.com/video/BV1PDpSeaEnx) ::: ## 约定 使用数据权限功能需要在数据表中设置用户标识字段,用于权限过滤。CatchAdmin 专业版使用 `creator_id` 字段作为数据创建者标识 ## 使用配置 * 在使用之前一定要确认你已经在角色上设置了**数据权限**范围 * 如果使用部门权限,那么用户就必须设置**所属部门** 完成以上两步,使用上就非常简单,但必须是`权限模块`开启的情况,并且使用权限模块。因为数据权限是和角色强绑定的。 ```php // 引入数据权限 Trait // modules/Permissions/Models/Traits/DataRange.php use Modules\Permissions\Models\Traits\DataRange; // 在模型中使用数据权限功能 Class AnyModel extends CatchModel { use DataRange; // 自动应用数据权限过滤 } ``` 使用 **DataRange trait** 后,模型查询会自动应用数据权限过滤规则。 如需手动控制权限范围,可以单独调用数据权限查询作用域 ```php // 手动应用数据权限过滤 Model::select('*')->dataRange()->get(); ``` --- --- url: /server/upload.md --- # 文件上传功能 CatchAdmin 专业版提供了完整的文件上传解决方案,支持本地上传、OSS 云存储、COS 云存储等多种上传方式。系统基于 Laravel Filesystem 构建,为开发者提供了灵活且强大的文件管理能力。 本文档将详细介绍各种上传组件的使用方法,包括图片上传、单文件上传、多文件上传、大文件分片上传等功能。除特殊说明外,所有组件默认基于本地存储。 ## 后端本地上传配置 CatchAdmin 专业版基于 Laravel Filesystem 提供了两个预配置的本地存储磁盘(disk),满足不同的文件存储需求: * **uploads**: 用于存储用户上传的文件,支持公开访问,适合图片、文档等需要直接访问的文件 * **static**: 用于存储系统内部文件,私有访问,适合配置文件、日志等敏感文件 以下是系统内置的磁盘配置,开发者可以根据项目需求添加自定义的 disk 配置: ```php // config/filesystem.php [ 'disks' => [ // 公共上传目录 - 用户上传的文件存储 'uploads' => [ 'driver' => 'local', // 使用本地存储驱动 'root' => storage_path('uploads'), // 存储根目录:storage/uploads 'url' => env('APP_URL').'/uploads', // 公开访问URL前缀 'visibility' => 'public', // 文件可公开访问 'throw' => false, // 操作失败时不抛出异常 ], // 私有静态文件目录 - 系统内部文件存储 'static' => [ 'driver' => 'local', // 使用本地存储驱动 'root' => storage_path('static'), // 存储根目录:storage/static 'visibility' => 'private', // 文件私有访问 'directory_visibility' => 'private', // 目录私有访问 'throw' => false, // 操作失败时不抛出异常 ], ] ] ``` ### 磁盘配置参数说明 | 参数 | 说明 | uploads 配置 | static 配置 | | ---------------------- | ----------------- | ----------------------------- | ----------------------------- | | `driver` | 存储驱动类型 | local(本地存储) | local(本地存储) | | `root` | 文件存储根目录 | storage/uploads | storage/static | | `url` | 公开访问 URL 前缀 | /uploads | 无(私有访问) | | `visibility` | 文件访问权限 | public(公开) | private(私有) | | `directory_visibility` | 目录访问权限 | 继承文件权限 | private(私有) | | `throw` | 错误处理方式 | false(返回错误而不抛出异常) | false(返回错误而不抛出异常) | ### 前端组件配置参数 前端上传组件通过 `props` 属性来指定使用哪个存储磁盘和存储路径。所有上传组件都支持以下通用配置参数: ```ts { // 指定存储磁盘,对应后端 filesystem 配置中的 disk 名称 disk: { type: String, default: 'uploads' // 默认使用 uploads 磁盘(公开访问) }, // 指定存储子目录,相对于磁盘根目录的路径 path: { type: String, default: 'attachments' // 默认存储在 attachments 文件夹下 } } ``` **参数说明:** * `disk`: 选择后端配置的存储磁盘,如 `uploads`(公开)或 `static`(私有) * `path`: 文件存储的子目录,最终存储路径为 `{disk.root}/{path}/filename` ## 前端上传组件 CatchAdmin 专业版提供了多种上传组件,满足不同的业务场景需求。 ### 图片上传组件 (UploadImage) 用于上传单张图片文件,支持图片预览和格式限制。 **基础使用:** ```vue ``` **自定义存储配置:** ```vue ``` **组件属性:** | 属性 | 类型 | 默认值 | 说明 | | ----------------- | ------ | -------------------------- | -------------------------- | | `v-model` | String | - | 绑定上传后的文件路径 | | `class` | String | - | CSS 样式类名,设置组件尺寸 | | `file-extensions` | Array | `['.jpg', '.png', '.gif']` | 允许上传的文件扩展名 | | `disk` | String | `'uploads'` | 存储磁盘名称 | | `path` | String | `'images'` | 存储子目录路径 | | `max-size` | Number | `2048` | 最大文件大小(KB) | ### 单文件上传组件 (UploadFile) 用于上传单个文件,支持多种文件格式,适合文档、表格等文件上传。 **基础使用:** ```vue ``` **自定义存储配置:** ```vue ``` **组件属性:** | 属性 | 类型 | 默认值 | 说明 | | ----------------- | ------------- | -------------------------- | ------------------------------ | | `v-model` | String/Object | - | 绑定上传后的文件信息 | | `file-extensions` | Array | `['.txt', '.doc', '.pdf']` | 允许上传的文件扩展名 | | `disk` | String | `'uploads'` | 存储磁盘名称 | | `path` | String | `'files'` | 存储子目录路径 | | `max-size` | Number | `10240` | 最大文件大小(KB) | | `accept` | String | - | HTML accept 属性,文件类型限制 | ### 多文件上传组件 (UploadFiles) 用于同时上传多个文件,支持批量文件处理,适合需要上传多个文档的场景。 **基础使用:** ```vue ``` **自定义存储配置:** ```vue ``` **组件属性:** | 属性 | 类型 | 默认值 | 说明 | | ----------------- | ------- | -------------------------- | ---------------------- | | `v-model` | Array | `[]` | 绑定上传后的文件列表 | | `file-extensions` | Array | `['.txt', '.doc', '.pdf']` | 允许上传的文件扩展名 | | `disk` | String | `'uploads'` | 存储磁盘名称 | | `path` | String | `'files'` | 存储子目录路径 | | `max-files` | Number | `5` | 最大上传文件数量 | | `max-size` | Number | `10240` | 单个文件最大大小(KB) | | `multiple` | Boolean | `true` | 是否支持多选文件 | ### 大文件分片上传组件 (ChunkUpload) 专为大文件(GB 级别)设计的分片上传组件,支持断点续传和上传进度显示。适用于视频、大型文档等文件上传。 **基础使用:** ```vue ``` **自定义分片配置:** ```vue ``` **组件属性:** | 属性 | 类型 | 默认值 | 说明 | | --------------- | ------------- | ------------------------ | ------------------------------ | | `v-model` | String/Object | - | 绑定上传后的文件信息 | | `action` | String | `'/upload/chunk'` | 分片上传接口地址 | | `chunk-size` | Number | `2 * 1024 * 1024` | 分片大小(字节),默认 2MB | | `max-file-size` | Number | `2 * 1024 * 1024 * 1024` | 最大文件大小(字节),默认 2GB | | `auto-upload` | Boolean | `true` | 是否自动开始上传 | | `retry-count` | Number | `3` | 分片上传失败重试次数 | **配置示例:** ```vue ``` ### 附件上传组件 (AttachUpload) 通用附件上传组件,支持各种文件类型的上传,可配置单文件或多文件模式。 **基础使用(单文件):** ```vue ``` **多文件上传:** ```vue ``` **组件属性:** | 属性 | 类型 | 默认值 | 说明 | | ----------- | ------------ | --------------- | ---------------------------------------- | | `v-model` | String/Array | - | 单文件模式绑定字符串,多文件模式绑定数组 | | `class` | String | - | CSS 样式类名 | | `multi` | Boolean | `false` | 是否支持多文件上传 | | `disk` | String | `'uploads'` | 存储磁盘名称 | | `path` | String | `'attachments'` | 存储子目录路径 | | `max-files` | Number | `5` | 多文件模式下最大文件数量 | | `max-size` | Number | `10240` | 最大文件大小(KB) | ## 云存储上传 CatchAdmin 专业版支持主流云存储服务,提供更高的可靠性和 CDN 加速能力。 ### 阿里云 OSS 上传组件 (OssUpload) 集成阿里云对象存储服务,支持直传和服务端签名上传,提供高可用的文件存储方案。 **前提条件:** 需要在后台管理系统中配置 OSS 相关参数(AccessKey、SecretKey、Bucket 等)。 ![专业版上传-OSS上传](https://image.catchadmin.com/202505120832848.png) **基础使用:** ```vue ``` **组件属性:** | 属性 | 类型 | 默认值 | 说明 | | ---------- | ------ | ----------- | -------------------- | | `v-model` | String | - | 绑定上传后的文件 URL | | `class` | String | - | CSS 样式类名 | | `path` | String | `'uploads'` | OSS 存储路径前缀 | | `max-size` | Number | `10240` | 最大文件大小(KB) | ### 腾讯云 COS 上传组件 (CosUpload) 集成腾讯云对象存储服务,提供稳定的云端文件存储和访问能力。 **前提条件:** 需要在后台管理系统中配置 COS 相关参数(SecretId、SecretKey、Bucket、Region 等)。 ![专业版上传-cos上传](https://image.catchadmin.com/202505120832297.png) **基础使用:** ```vue ``` **组件属性:** | 属性 | 类型 | 默认值 | 说明 | | ---------- | ------ | ----------- | -------------------- | | `v-model` | String | - | 绑定上传后的文件 URL | | `class` | String | - | CSS 样式类名 | | `path` | String | `'uploads'` | COS 存储路径前缀 | | `max-size` | Number | `10240` | 最大文件大小(KB) | --- --- url: /start/upgrade.md --- # 更新日志 ## v043.1 * [新增大文件分片上传组件](../server/upload.md#大文件分片上传组件 "ChunkUpload") * 新增 `xlswriter` 扩展导出,支持百万级导出,[文档](../server/export.md#Xlswriter) * 支持 Keepalive 组件,可以缓存组件状态。可通过后台权限菜单配置 * 优化了后台暗夜模式的样式 * 优化邮箱模板发送 * 新增远程API获取数据组件 `` * 添加 middle 插槽 丰富 catchtable * catchtable 搜索添加远程接口下拉选择[文档](../front/catch-table.md#远程下拉) * 更新前端依赖,支持 vite7.x * 优化了 OSS 上传,支持 oss 添加自定义域名 * 支持修改后台登录地址,可通过前端 `env` 文件修改 ```js VITE_LOGIN_PATH=/test/logins ``` ## v043.0 * 新增邮件营销模块初版(主要) * [优化 Scheam 管理创建功能体验,由之前弹窗操作成步骤创建](https://catchadmin.vip/forum/9fc5df1f-c218-4dab-a5bc-4b6e08cb9028) * [表格搜索新增远程 Select 组件](https://catchadmin.vip/forum/9fbb96bd-49a0-4112-85d7-7f73a9d0b7a7) * 优化页面加载样式,防止白屏 * 优化前端助手函数 * 优化搜索样式,响应式支持 * 修复新增字段生成的时候结构不一致 * 修复初始化用户重复 * 修复代码[生成中下拉组件的下拉选项未正确生成](https://catchadmin.vip/forum/9fbd344d-000d-4273-aef0-1c2a8671a70d) * 修复代码生成关联数据错误 * 核心包优化导出菜单命令 * 核心包添加同步父级数据字段控制 * 核心包优化升级命令 ## v042.6 * 添加系统基础配置,支持上传 logo,修改后台标题,设置是否需要验证码登录 * 优化了域名管理系统,完整适配阿里云和腾讯云接入 * 新增 Query 日志通道 * 优化后台 BaseURL 得获取方式 * 优化本地上传组件 * 优化前端请求错误异常处理 * 支持 catchtable link 组件添加 http 协议 * 还有一些整体灵活性优化 ## v042.5 * 附件支持 oss 平台上传 * 多语言切换到后端返回,不再使用前端静态数据 * 修复富文本上传层级问题 * 动态表单添加[远程组件](https://form-builder.catchadmin.vip/forms/components/remote)支持 * 动态表格支持[动态路由](https://form-builder.catchadmin.vip/forms/table/column#route-%E6%96%B9%E6%B3%95) * 新增 Arrays 组件 * 一些其他优化... ### 更新 下载 `v4.2.5` 补丁包,将补丁包放在项目根目录下 ```shell composer require "catchadmin/form:0.1.4" #执行更新命令 php artisan catch:upgrade xxxx.zip ``` ## v042.4 * 核心包添加了 `admin_cache` 后台缓存等操作,统一后台管理缓存操作 * 优化了前端样式一些细节问题 * 优化 cms 模块文章 支持选择已有 tag * 修复 catchtable 组件方法冲突 * 新增系统管理添加接口每日统计 * 前端支持版本更新提示 * 首次加载会出现短暂白屏,添加加载动画效果 ### 更新 下载 `v4.2.4` 补丁包,将补丁包放在项目根目录下 ```shell composer update catchadmin/pro #执行更新命令 php artisan catch:upgrade xxxx.zip ``` ## v042.3 * 新增新增导入导出创建 * 优化控制模型生成 * 优化整体代码生成结构 * 优化远程组件获取 * catchtable 组件支持按钮权限 ### 更新 下载 `v4.2.3` 补丁包,将补丁包放在项目根目录下 ```shell composer update catchadmin/pro #执行更新命令 php artisan catch:upgrade xxxx.zip ``` ## v042.2 版本发布 * 修复 schema 更新问题 * 优化了代码生成组件 * 新增版本更新命令 ### 更新 下载 `v4.2.2` 补丁包,将补丁包放在项目根目录下 ```shell composer update catchadmin/pro #执行更新命令 php artisan catch:upgrade xxxx.zip ``` ## v042.1 版本发布 * `代码生成`支持生成导出字段可选并且生成单独导出类 * `代码生成`支持生成导入字段可选并且生成单独导入类 * `代码生成`优化了模型生成 * `代码生成`优化了控制器生成 * `代码生成`优化了整体得代码生成组件交互逻辑 * `代码生成`修复了打包类型错误 * catchTable 组件内置[按钮权限](../front/catch-table.md#表格按钮权限) * `核心包`修复文件指针错误 ### 更新 更新核心包 ```shell composer update catchadmin/pro ``` ## v042 版本发布 :::info 这个版本主要极大的提高了代码生成的功能 ::: * `代码生成`支持模型关系生成(hasOne/hasMany/belongsTo/BelongsToMany/HasOneThrough/HasManyThrough) * `代码生成`支持历史参数保存,可重复生成 * `代码生成`支持枚举自动转换 * `代码生成`支持图片自动转换显示 * `代码生成`支持远程数据自动生成 * `代码生成`支持下拉组件选项自动生成 * `代码生成`支持导入导出生成 * `代码生成`支持数据根据组件自动转换 * 新增远程数据组件 * 优化了图标选择组件 * `核心包`添加快捷搜索回调,允许用户数据转换 * `核心包`添加 admin\_cache 助手函数 * `核心包`添加 remove\_app\_url 助手函数 * `核心包`添加 excel 下载 download 方法支持自定义 field ### 更新 更新核心包 ```shell composer update catchadmin/pro ``` ## v041.3 版本发布 * 核心包添加[图片处理](../server/image.md) * [动态表单](https://form-builder.catchadmin.com/docs/forms/table/)扩展表格添加动态操作组件,支持路由跳转,动态参数绑定 * 修改目录可写配置 * 优化前端 excel 下载体验 * 添加表格操作组件 * 优化上传,提供默认后缀 * cms 模块优化 ## v041.2 版本发布 * 核心包添加模块可选参数安装所有模块 * 核心包优化安装 检测环境等信息 * 核心包新增服务端信息计算 * 核心包修复 zip 关闭资源失败 * 动态表单上传添加 disk 配置 ## v041.1 版本发布 * 发布新的 uniapp 快速启动,全面改版 * 微应用端完全适配 uniapp ## v041 版本发布 * (主要) [完善支付模块](../server/pay.md),提供了一整套快速接入支付的可扩展支持 * 开发模式下支持跨域了,现在可以直接使用断点调试 * 优化了上传组件数据响应问题 * 修复商城模块多规格产品中无法更新规格名称 * 上传服务添加了`disk`支持,已适配[上传组件](../server/upload.md) * 会员添加 token 字段 * AI 助手模块完善一部分功能 * ... ## v0401 版本发布 * 修复 Laravel12 升级的兼容性问题 * 提高核心的稳定性,优化安装体验 * 添加 composer run dev 命令,可以一条命令启动两个项目 * 修复 select 组件 options 没有生效 * 修复生成表结构时字段不显示 * 修改代码生成的时候控制器 construct 参数生成错误 * 修复重复安装的是, personal\_access\_tokens 存在会导致报错 * 修复授权 token 参数无效 * 修复认证中间件报错 ... ## v040 版本发布 * **全面升级到 Laravel12 版本,主要适配升级的依赖包的问题** * 新增 `ai 模块` 功能开发完善中 * 修复权限认证 GET 请求配置 * 修复核心包认证 token 不符合规则时,导致系统出现异常 * 修复代码生成的时候路由写入失败 * 优化后台搜索,点击搜索在没返回数据时,保持 loding 状态 * 系统授权码方式更新,禁止明文密码 ... ## v035 版本发布 * Builder 动态组件升级到 `0.1.0` * `动态组件`新增获取所有组件方法 * `动态组件` 新增 `tab` 组件 * `动态组件` 新增动态`方法 function`渲染 [示例](https://form-builder.catchadmin.com/docs/demo/tab) * 优化 CatchTable 组件,可读性更强 * 登录新增`图形验证码`验证 * 优化代码生成,支持单页表单生成 * 使用动态组件重写 CMS 模块 * 添加 `Dynamic` 路由, 使用 `Route::adminResource` 将会自动注册 * 优化 `GetList` 方法, 支持 `tree` 动态转换 ... ## v034 版本发布 * `主要` 新增快速 CURD 构建组件 [文档](https://form-builder.catchadmin.com/docs/forms/intro) ```shell # 获取组件 composer require catchadmin/form ``` * 前端新增 `` 组件 * 优化列表生成 * 优化代码生成,新增枚举动态查询组件 * 修复 catchtable expand 无法展开 * 修复 sanctum token 生成没有过期时间 * 修复菜单创建失败后回滚文件 * 修复删除子权限后菜单会展开 * 添加动态表单的 demo 演示 ... ## v033 版本发布 * 新增宝塔快捷部署命令,详见[宝塔部署文档](./bt-deploy.md) * 更新 app 模块,支持多种微信登录,微信手机号快捷登录,密码登录多渠道支持 * 新增 Uniapp 一个快速开发模块 * 新增 adminResource 路由,详见[路由文档](../server/route.md) * 添加头像存储目录 * 添加微信模块和支付模块菜单 * 代码生成模型名称添加校验 * 修复代码生成路由文件回滚异常 * 移除 scope scss * 修复 cli 模式下全局异常种路由判断异常 * 修复导出缺失搜索参数 ... ## v032 版本发布 * 新增会员 app 模块,包含基于 JWT 身份认证,退出,中间件认证,异常处理等等一些基础设施,用户可以再次基础上快速接入自己的应用。快速开发自己的应用[文档](app.md) * 现在 composer 安装可以安全的运行在 https 上 * 优化了 Vue 表格生成,处理一下非必要的生成 * 优化了认证脚本,增加非常友好的提示,安装体验更进一步 * 修复了后台用户 token 管理异常 * 修复了代码生成无法热更新的(vite 导致) * 修复了 scss 的一些错误(主要是依赖) * 添加软链接配置,catchadmin 文件上传到 storage 目录,无法直接访问,现在你可以通过 `php artisan storage:link` 直接访问上传文件了 ... ## v031 版本发布 * 新增 `openapi` 模块 [查看](../server/openapi.md) * 新增 table mask column 模式 * 优化代码生成的字典获取 * 修复导航异常导致退出,增加提示 * 优化动态菜单的 component 加载,提高兼容性 * 去除上传和登出的权限校验 * 新增支持回车登录 * 修复文件上传的 size 大小校验 ... ## v030 版本发布 * 新增支持注释生成接口文档[查看](./api.md) * 新增接口文档生成支持 Vitepress 和 Postman 等工具 * 新增自定义响应数据格式[查看](../server/response.md) * 优化 Laravel 框架日期 * 修复当前登录用户 Null 导致异常 * 优化会员密码更新 * 优化删除不必要的路由 * 用户模块/公共模块/开发工具模块/权限模块 添加接口文档注释 .... ## v029 版本发布 * 后端新增 Admin 组件,主要用来全局管理用户状态,鉴权 * 优化 Sanctum 每次请求导致多次数据库操作 * 新增模块依赖方法,解决模块之间的依赖 * 修复鉴权中间件异常导致退出 * 新增支持百万下载 csv 组件 * 优化 migrate 生成的文件名 * 优化添加必要的配置注释 .... ## v028 版本发布 * 新增基于`角色`的`字段权限`控制 * 优化异步任务更新 * 新增字典管理关联枚举,自动生成枚举值 * 优化代码生成,支持字典数据关联 * 优化路由管理 * 支持是否填充 creatorid 字段 * 修复中间件异常导致身份认证失败 * 修复自动维护 created\_at & updated\_at 字段 ## v027 版本发布 支持`windows` `macos` `linux` 桌面端 [windows 版本下载 ⏬](https://image.catchadmin.com/catchadmin-1.0.0-setup.exe) [Macos 版本下载 ⏬](https://image.catchadmin.com/catchadmin-1.0.0.dmg) :::info 鉴于 Linux 使用的人较少,这里暂不提供。如果需要,可联系 ::: ## v026 版本发布 * 新增头部水平菜单模式,以应对日益增多的功能菜单 ![catchadmin 专业版 水平菜单模式](https://image.catchadmin.com/202408141433177.png) * 新增搜索 select 组件支持 query 动态搜索 * 优化了 CMS 模块文章发布,新增 SEO 信息 * 优化了附件上传组件递归导致组件失效的问题 * 优化商城模块 DIY 组件 * 重构了 catch-table 的搜索组件,更加简洁 * 修复搜索 select 组件默认值无效 * 修复了登录密码反显加密后的密码问题 * 修复了异常导致认证失败,需重新登录的问题 * 修复了 vite import 组件错误(vite 版本升级导致) ## v025 版本发布 * 接口监控功能 ![catchadmin 专业版 接口监控功能](https://image.catchadmin.com/202407231147599.png) 如果是拉去代码的话,记得使用下面的命令更新 ```shell php artisan catch:db:seed system php artisan catch:migrate system ``` * 接口异常事件监控 * 优化 Schema 功能 * 优化创建表结构 * 优化文件生成 * 优化其他一些小功能 ## v024 版本发布 * 添加后台作业任务 * 新增生成文件历史,支持文件可更改 * 优化表结构创建 * `商城模块`新增快递查询 * `商城模块`新增快递配置 * 优化其他一些小功能 :::info 该版本添加了新的前端依赖, 请使用 `yarn upgrade` 更新依赖 ::: ## v023 版本发布 * 修复选择上级角色后权限没更新 * 优化角色可选权限,限制有父级角色的权限选择范围 * catchTable 支持对象深层级获取 ```javascript // 例如 { prop: 'roles[0].id' } ``` * 优化菜单添加时顶级菜单判断 * 优化大量模型返回时候,时间格式转换导致响应过长的问题 ## v022 版本发布 * 修复短信登录错误 * 商城模块新增了秒杀管理模块 * 商城模块新增了会员充值模块 * 商城模块新增了积分管理模块 * 商城模块新增了满额包邮模块 * 优化了动态配置类型转换 ## v021 版本发布 * 增强 `catch-table` 组件 * 新增一个 `` 组件 * 商城模块新增了优惠券管理模块 * 商城模块新增了商品选择组件`` * 修复了菜单类型断言错误 * 优化了项目断点打印功能,提高了 debug 速度 * 优化了项目整体的异常提示功能 * 优化代码生成,支持路由文件回滚 * 跨域打印功能,本地开发跨域使用断点调试一直很麻烦,不是很直观 ## v020 版本发布 * 新增异常预览功能,不需要通过日志查询 ![异常预览功能](https://image.catchadmin.com/202406021645428.png) * 跨域打印功能,本地开发跨域使用断点调试一直很麻烦,不是很直观 ```php // 使用 catchadmin 断点函数 dd_('断点调试') ``` ![跨域打印功能](https://image.catchadmin.com/202406021646688.png) ## v019 版本发布 * 修复模型删除时未删除对应的关联关系 * 移除默认的认证,需要用户调整一下,算是破坏更新 ```php // 找到 config/catch.php, 将 auth 改为 admin 即可 'auth' => 'admin', ``` * 添加自动[打包命令](./deploy.md),该命令可以将项目打包成 zip 包 ```shell php think catch:build ``` * 添加[回收站功能](../front/catch-table.md#回收站) * 修复 `SQL` 日志记录`%`报错 * 修复时区导致日期错误 * 增强路由,添加 `adminResource` 路由方法 * 修复低版本 Mysql 安装报错 * 添加批量更新方法 ## v018 版本发布 * 新增[导入组件](../server/import.md) * 修改默认 tab 页面 * 修改菜单 action 生成 * 新增阿里 OSS 组件上传 ```vue ``` * 优化上传组件 * 修复底层模型时间错误 * 修复 Query Log 遇到 % 导致无法写入的错误 * 优化了其他功能... ## v017 版本发布 * 后台登录加固,新增最大登录次数限制和密码混淆机密 * 新增 auth 认证便捷脚本 ```php php auth.php 邮箱 密码 ``` * 新增 tagview 导航 * 优化安装,新增前端镜像自动设置,依赖自动安装 * 新增用户模块安装器 * 新增模型批量更新方法(优化数据量大更新较慢) * 新增地区组件,以及自动获取地区数据 * 优化自动生成代码 * 修复 ElementPlus 组件报错 * 优化模块安装, 过滤已安装的模块 * tinymce 编辑器添加视频上传功能 ## v016 版本发布 这个版本主要是进行了 `Laravel11` 项目的迁移,在获取代码之后首先执行 ``` php artisan catch:migrate permissions ``` :::warning 因为权限表的岗位表名和 Laravel 的 Jobs 表名冲突,所以修改了表名,请及时更新 ::: 这次迁移算是很顺利,但是过程中也遇到了一些麻烦,各种错误不断,基本都是由于 `Laravel11` 初始配置与 Laravel10 项目不同导致的。目前已经全部解决,本人也顺利的用上了 Laravel11 的简洁骨架。以后也是在这个 Project 骨架上进行开发。 ### 更新 * 超管支持数组配置 * 打包环境区分本地和正式环境,正式环境将抹除开发工具和模块管理 * 修改 schema 字段 * 用户管理新增加入到权限管理中 * 登录页面的优化 * 修改岗位名称,防止和 Laravel 项目的任务表冲突 * 增加初始化数据 * Request 模板生成错误 * Laravel11 的 migration 兼容更新 * 修复异步任务执行错误 * 修复附件组件上传的内存泄露 * 更改 Element Radis 的 Api,label -> value * 新增两个上传组件,单文件上传和多文件上传 * 优化了后台上传组件 * 后台编辑器支持附件上传组件 * 优化后台的状态组件 * 本地上传将会自动把文件添加到附件列表,供附件组件选择使用 * 优化了上传配置,增加了上传类型限制和大小限制 * 优化了打包类型 * ....等等功能 ## v015 版本发布 ### 新增 * 新增了两个 DashBoard 页面,销售和服务器信息界面 * 新增 Echarts 组件,提供服务器信息查看 * 新增后台路由界面,可以直接通过页面查看路由列表,支持搜索查询 * 新增配置提示,获取相关配置 ### 后端 * 修复后台认证默认 `Guard`, 需要用户重新发布配置文件,使用下面的命令 * 优化数据库报错信息提示 * 优化菜单导出,支持菜单导出多模块 * 优化项目安装,支持在正式环境安装,使用下面的命令 ```sh php artisan catch:install --prod ``` * 修复模型日期错误,框架自带的问题,需要重写方法 * 优化用户超管判断,支持数组配置 * 优化 Schema 字段 * 优化权限相关 ### 前端 * 前端支持区分本地和正式环境打包路由,在正式环境打包,去除开发工具等菜单 * 优化前端项目的权限指令 * 删除用户管理的静态路由,添加到权限管理内,支持可配置权限,需要使用下面的命令同步数据 * 修改后台登录界面 ### 如何更新 在拉取代码之后 ```sh composer update --ignore-paltform-reqs php artisan vendor:publish --tag=catch-config --force php artisan catch:db:seed permissions ``` ## v014 版本发布 * 新增域名管理模块(该功能可用于 Saas 应用,创建 SaaS 应用) * 阿里云域名管理 * 腾讯云域名管理 * 优化代码生成,自动生成前端表单规则 * 优化代码生成,自动生成 radio switch 等组件的选项 * 修复后台管理标签页标题错误 * 修复后台多语言切换 * 修复路由缓存问题 ## v013 版本发布 * 新增会员模块 * 新增 webhook 通知 * 钉钉 webhook 通知 * 飞书 webhook 通知 * 企业微信 webhook 通知 主要用于监听应用消息,例如日志,错误 ## v012 版本发布 * 优化上传组件 * 优化弹窗组件 * 优化后台组件状态切换 * 优化附件选择组件 * 优化代码自动生成,数据库字段直接拖拽 * 新增地区选择组件 * 优化菜单选择组件 * 优化用户信息 * 修复验证码获取 * 修复权限更新报错 * 修复模块创建 ## v011 版本发布 * 新增商城模块(开发中) * 商品模块 * 商品分类模块 * 商品品牌 * 商品服务 * 规格模板 * 商品标签 * 运费模板 * 商城装修 ## v010 版本发布 * 修复了权限管理菜单 tip2 重叠问题 * 修复安装时数据库存在,无法安装的问题 * 修复 Ctrl C 强制终止,无法再次安装的情况(依赖信号,此功能需要安装 pcntl 扩展) * 优化登陆配置,在未配置的情况下,不限时短信和微信登陆 * 优化项目安装,引入 prompts 增强安装体验 * 优化项目安装,增加前端应用配置,无需用户自行配置 * 新增模块自动加载配置,推荐在线上使用 * 新增模块获取信息方法 * 新增 restores 方法,恢复软删除数据 * 新增注解 Form (实验性,开发中) * 更多... ## v004 版本发布 * 优化了代码生成功能,可以更加便捷的生成代码 * 新增了头像显示组件 * 优化了 migration 生成 * 优化了项目安装,默认安装权限模块和系统管理配置模块 * 修复了 action 生成 loading 状态 * 核心包更新,以及修复 bug * 优化模块安装,图形界面安装 ## v003 版本发布 * 新增七牛上传组件 * 新增腾讯 cos 组件 * 新增阿里云短信功能 * 新增腾讯短信功能 * 新增动态配置功能 * 新增手机短信登陆功能 * 新增支持手机号登陆功能 ## V002 版本发布 * 新增 Excel 导入导出组件 * 新增 Wechat 模块 * 新增异步任务组件 * 代码生成功能优化 * 新增修改密码命令,便于服务器管理 * 新增异步任务 task --- --- url: /tenancy/upgrade.md --- # 更新日志 ## v0.1.7 #### ✨ 新特性 * **队列与调度任务监控**:新增对队列和调度任务的实时监控,让您全面掌控系统后台任务的执行状态,提升了系统的可观测性。 #### 🚀 优化与增强 * **增强数据一致性**:强化了创建租户时的数据一致性保障,确保在复杂场景下数据安全可靠。 * **优化缓存隔离**:进一步优化了租户的缓存隔离机制,有效提升了高并发环境下的系统性能与稳定性。 ## v0.0.2 #### ✨ 新特性 * **租户作业生成命令**:新增了 `catch:tenant:job` 命令,开发者可以更便捷地为特定租户创建后台作业。 #### 🚀 优化与增强 * **租户识别机制升级**:租户识别方式全面升级为前缀模式,简化了系统集成与管理复杂度。 * **完善隔离机制**:实现了更彻底的日志隔离与更严格的权限校验,全方位保障租户数据安全。 * **提升上下文切换性能**:深度优化了租户上下文切换逻辑,显著提高了系统的整体响应速度与处理性能。 ## v0.0.1 * 完整支持租户创建与管理功能,实现多租户系统的基础架构 * 引入灵活的租户套餐订阅机制,支持多级别服务方案 * 精细化租户权限配置系统,确保每个租户的资源访问安全 * 实现完全的数据库隔离机制(多库模式),保障数据安全与独立性 * 构建租户日志隔离系统,便于独立的问题跟踪与分析 * 集成租户上传资源隔离功能,确保存储空间的独立与安全 * 实现租户队列隔离机制,避免任务处理相互影响 * 引入租户缓存隔离策略,提升系统响应速度与性能 * 支持租户调度任务隔离,确保定时任务的独立运行环境 --- --- url: /front/permissions.md --- # 权限认证 上面其实也讲到了权限相关的,用户在通过认证之后,后端在用户信息中其实已经加入了该用户所有权限。可以通过 `web/src/store/user/index.ts` 的 `UserStore`获取 ```typescript const userHasPermissions = userStore.getPermissions ``` ## 权限指令 权限指令是使用 `vue`的 `directive` 实现一个前端操作控制的指令,例如新增,更新等等操作。如果你需要页面级别的权限操作,那么这个指令可以很好的帮助你实现该功能 例如控制权限模块的角色更新功能,你可以使用 `v-action` 进行控制,如果登录人员没有改操作权限,那么此操作按钮将不再页面展示。 ```javascript ``` 权限指令要求的格式和后端相似,格式如下 ```javascript module.controller.action or module@controller@action ``` ### 实现方案 众所周知,后端是模块的,为了防止模块之间的路由会发生冲突,所以权限标识是由**模块** + **controller@action** 组合 :::info 后端路由即 controller@action,权限标识也是这样定义 ::: 所以权限检测可以这么写, 伪代码如下 ```typescript function hasPermission(string mark) { // mark 是这样的形式 module + '@' + 'controller@action' // 当然也可以定义其他形式的 const userHasPermissions = userStore.getPermissions userHasPermissions.each(item => { if (permissions === (item.module + '@' + item.permission_mark)) { return true } }) return false } ``` 这样就是检测权限了,那么再将其引入到自定义指令中,这里代码仅提供思路,正确性未知 ```typescript app.directive('permission', (el, binding) => { const hasPermission = hasPermission(binding.value) if (!hasPermission) { el.style.display = none } }) ``` 在项目中这么使用 ```html ``` 具体实现可到前端项目的`directives`目录下的 `action` 查看 ## CatchTable 应用 [catchtable 按钮权限](./catch-table.md#表格按钮权限) --- --- url: /front/style.md --- # 样式 样式存在 `web/src/style` 目录下,结构如下 ```php ├─theme | ├─dark.scss // 暗黑主题 | ├─index.scss | ├─light.css // 默认主题 ├─element.scss // Element 样式 ├─index.scss // scss 入口 ├─tailwind.css // tailwindcss ├─var.scss // 自定义变量 ``` 样式上好像并没有什么可以说的了, `style` 目录的样式都是全局样式。 还有一点就是目前后台的样式是响应式的,基于 `tailwindcss` 做的,`tailwindcss` 还是很方便的。 :::info 其实用到全局样式的地方不是很多,一般还是在 vue 文件中使用 scope 来改样式 ::: --- --- url: /server/config.md --- # 框架配置 ## catchadmin 配置 首先先了解`catchadmin` 的项目相关配置 :::code-group ```php [config/catch.php] return [ /* |-------------------------------------------------------------------------- | catch-admin 超级管理员 |-------------------------------------------------------------------------- | | 可以设置超级管理的用户 ID | 支持数组 | |-------------------------------------------------------------------------- */ 'super_admin' => 1, /* |-------------------------------------------------------------------------- | catch-admin 请求允许 |-------------------------------------------------------------------------- | | 默认允许 GET 请求通过 RBAC 权限 | |-------------------------------------------------------------------------- */ 'request_allowed' => true, /* |-------------------------------------------------------------------------- | catch-admin 模块设置 |-------------------------------------------------------------------------- | | 设置模块根目录 | 设置模块的根命名空间 | 设置模块默认生成的文件夹 | |-------------------------------------------------------------------------- */ 'module' => [ 'root' => 'modules', 'namespace' => 'Modules', /** * 默认启动的模块 */ 'default' => ['develop', 'user', 'common'], 'default_dirs' => [ 'Http'.DIRECTORY_SEPARATOR, 'Http'.DIRECTORY_SEPARATOR.'Requests'.DIRECTORY_SEPARATOR, 'Http'.DIRECTORY_SEPARATOR.'Controllers'.DIRECTORY_SEPARATOR, 'Models'.DIRECTORY_SEPARATOR, ], // 模块存储驱动 // 默认使用文件驱动 'driver' => [ // currently, catchadmin support file and database // the default is driver 'default' => 'file', // use database driver 'table_name' => 'admin_modules', ], /** * 模块路由集合 */ 'routes' => [], /** * 模块是否自动加载 * * 如果设置成 true,模块会自动全部加载 */ 'autoload' => env('CATCH_MODULE_AUTOLOAD', false), ], /* |-------------------------------------------------------------------------- | catch-admin 响应 |-------------------------------------------------------------------------- */ 'response' => [ // JSON 响应, 保证响应数据都是 json 'always_json' => \Catch\Middleware\JsonResponseMiddleware::class, // 响应监听者 // 监听[RequestHandled]事件 'request_handled_listener' => \Catch\Listeners\RequestHandledListener::class, ], /* |-------------------------------------------------------------------------- | 数据库 SQL 日志 |-------------------------------------------------------------------------- */ 'listen_db_log' => env('APP_DEBUG', true), /* |-------------------------------------------------------------------------- | 管理员授权认证模型 |-------------------------------------------------------------------------- */ 'auth_model' => \Modules\User\Models\User::class, /* |-------------------------------------------------------------------------- | 管理员授权 Guard |-------------------------------------------------------------------------- */ 'auth' => 'admin', /* |-------------------------------------------------------------------------- | 路由配置 |-------------------------------------------------------------------------- */ 'route' => [ 'prefix' => 'api', 'middlewares' => [ \Catch\Middleware\AuthMiddleware::class, \Catch\Middleware\JsonResponseMiddleware::class, ], ], /* |-------------------------------------------------------------------------- | 前端 Vue 视图文件夹路径 | | 如果不设置,将不会生成相关的 Vue 文件 |-------------------------------------------------------------------------- */ 'views_path' => base_path('web'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR), /* |-------------------------------------------------------------------------- | 开启系统接口日志分析 | | 接口日志依赖 Redis,提高性能 |-------------------------------------------------------------------------- */ 'system_api_log' => env('CATCH_SYSTEM_API_LOG', false), /* |-------------------------------------------------------------------------- | 图片处理 | | 默认使用 GD |-------------------------------------------------------------------------- */ 'image' => [ 'driver' => env('CATCH_IMAGE_DRIVER', 'gd'), 'options' => [ ], /** * 默认读取磁盘 */ 'read_from' => env('CATCH_IMAGE_READ_FROM', 'uploads'), ], /* |-------------------------------------------------------------------------- | 后台缓存统一管理 | | 配置后台缓存前缀,便于清理后台管理的缓存 |-------------------------------------------------------------------------- */ 'admin_cache_key' => env('CATCH_ADMIN_CACHE_KEY', 'admin_dashboard_'), ]; ``` ::: * `super_admin` 配置 **super admin** 的 **ID**,默认 `1`, 支持数组配置`[1,2,3]` * `module` 模块相关配置 * `root` 配置模块的根目录 * `namespace` 模块根命名空间 * `default` 默认模块,初始化 `develop`, `use`, `common` 三个模块 * `default_dirs` 默认生成默认的目录 * `driver` 模块配置驱动 默认是 file * `routes` 模块路由集合 * `response` 响应配置 * `always_json` 响应输出 `Json` * `request_handled_listener `响应数据格式配置 * `auth` 认证相关配置 * `listen_db_log` 是否监听 **DB SQL** * `route` 路由配置 * `prefix` 路由前缀 * `middlewares` 路由默认路由 * `views_path` 配置前端项目 views 路径 * `system_api_log` 是否开启系统接口日志分析 * `images` 图片处理配置 * `admin_cache_key` 后台缓存 `key` 统一前缀 项目有定制需求,一定要看看这些配置 ## 模块配置 除了整个系统的配置以外,系统还提供了模块化的配置,模块的配置也是相互独立的。如果模块需要配置,那么可以直接在模块目录下添加 `config` 目录,系统会自动加载配置文件。当然非要客制化下,也没问题。只需要在模块的`Provider` 下实现 `configPath` 这个方法即可 ```php title="modules/Test/Providers/TestServiceProvider" namespace Modules\Test\Providers; use Catch\CatchAdmin; use Catch\Providers\CatchModuleServiceProvider; class TestServiceProvider extends CatchModuleServiceProvider { public function confitPath(): string { return config_path; } } ``` 最终模块的配置如下结构 * Permissions * config * one.php * two.php 那么如何获取呢?按照 `Laravel` 的模式,应该还用下面的代码获取配置内容 ```php config('one') ``` 但是因为是模块化独立的,所以获取上一定要加入模块的名称,最终应该是这么获取 ```php config('permissions.one.some_key') ``` --- --- url: /start/client.md --- # 桌面端 基于 Electron 包装的桌面端,再加入代码组织仓库后,到对应的组织下获取对应的桌面的代码。 ## 安装 ```bash yarn install ``` ## 迁移 将专业版的 web 端的后端管理项目的 `src` 整个目录移动到 `src/renderer` 目录中即可。后续 web 端的前端代码更新,可以持续更新到 `src/renderer` 目录即可 ## 配置接口地址 根目录`.env` 文件 ``` VITE_BASE_URL=这个跟你的web admin一样 ``` ## 开发 ```bash yarn dev ``` ### 打包 ```bash # 打包 windwos yarn build:win # 打包 MacOS yarn build:mac # 打包 linux yarn build:linux ``` 打包之后后的安装文件会在 `dist` 目录下 ### 常见问题 如果碰到了该问题 > Could not load c:\project\electron-admin\src\renderer\src/../package.json 那么需要修改 `src\renderer\src\views\dashboard\server\dependencies.vue`的导入 `package.json` 文件,替换成下面的导入路径 ```js // import packages from '@/../package.json' // 换成下面的 import packages from '@/../../../package.json' ``` --- --- url: /develop/sms.md --- # 短信发送 ## 数据结构 后台短信发送功能提供了两个表,一个短信模板表,一个存储验证码的表,结构如下 ### 短信模板表 ```sql CREATE TABLE `system_sms_template` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `identify` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '模版唯一标识', `channel` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'aliyun' COMMENT 'aliyun:阿里云短信,qcloud:腾讯短信', `template_id` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '模版ID', `content` varchar(2000) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '模版内容', `variables` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '模版变量', `creator_id` int unsigned NOT NULL DEFAULT '0' COMMENT '创建人ID', `created_at` int unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', `updated_at` int unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', `deleted_at` int unsigned NOT NULL DEFAULT '0' COMMENT '软删除', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='短信模版'; ``` ### 短信验证码表 ```sql CREATE TABLE `system_sms_code` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `mobile` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号', `code` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '短信验证码', `behavior` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '短信行为表: 1 login 2 register', `channel` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'aliyun' COMMENT 'aliyun:阿里云短信,qcloud:腾讯短信', `status` int NOT NULL DEFAULT '1' COMMENT '状态 1 未使用 2 已使用', `expired_at` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '过期时间', `created_at` int unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', `updated_at` int unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='短信验证码记录'; ``` ## 配置 到后台`系统管理->短信配置`里设置平台,目前支持`阿里云`和`腾讯云`两个平台,如下图配置对应的平台配置 ![CatchAdmin 专业版短信配置](https://image.catchadmin.com/202506151051117.png) 然后配置对应平台的短信模板 :::warning 注意这里需要你先到平台配置对应的模板,需要等模板审核通过后,录入到后台 ::: ![CatchAdmin 专业版短信配置模板](https://image.catchadmin.com/202506151052045.png) ![CatchAdmin 专业版短信配置模板](https://image.catchadmin.com/202506151102971.png) * `模板标识` 字符串,这里的示例是 `login` * `模板ID` 平台获取的模板 ID ## 使用 :::warning 确保已经配置好了模板和平台 ::: 使用起来很简单,例如项目是发送登录短信码 ```php $smsCode = new SmsCode(); $smsCode->login($request->get('mobile')); ``` 具体细节看如下代码 :::code-group ```php [modules/System/Support/Sms/SmsCode.php] class SmsCode { protected Sms $channel; public const LOGIN_BEHAVIOR = 'login'; public const REGISTER_BEHAVIOR = 'register'; public function __construct() { $this->channel = Factory::make(); } /** * 发送登录验证码 * @throws \Throwable */ public function login(string $mobile): bool { try { // 使用存储短信验证码的模型 $smsCodeModel = new SystemSmsCode; // 短信对应的手机号是否有验证码记录,如果验证码没有使用则返回错误 if ($smsCodeModel->hasCode($mobile, self::LOGIN_BEHAVIOR)) { throw new FailedException('验证码未使用或者未过期'); } // 生成新的验证码 $code = $this->getCode(); // 发送验证码 // self::LOGIN_BEHAVIOR 就是模板标识,是 login,用来查找模板,模板变量使用{}包裹,形如{code} // [$code] 这个就是变量数组,里面的值会被系统依次解析,注意需要和模板变量一一对应 $this->channel->send(self::LOGIN_BEHAVIOR, $mobile, [$code]); // 然后保存发送的手机号验证码 $smsCodeModel->store($mobile, self::LOGIN_BEHAVIOR, $code); return true; } catch (NoGatewayAvailableException $exception) { throw new FailedException($exception->getLastException()->getMessage()); } catch (\Throwable|\Exception $e) { throw new FailedException($e->getMessage()); } } /** * @return void */ public function register(string $mobile) { $smsCodeModel = new SystemSmsCode; $smsCodeModel->hasCode($mobile, 'register'); } protected function getCode(): int { return rand(100000, 999999); } } ``` ::: --- --- url: /start/app.md --- # 移动应用端 API 开发指南 CatchAdmin 专业版已提供完整的后台管理系统,现新增移动应用端开发解决方案。`app` 应用端专为前端移动应用和 Web 应用提供标准化 API 接口服务,支持快速构建现代化应用程序。核心功能包括 * 基于 `JWT` 的用户认证 * 统一的枚举管理 * 统一的响应处理 * 统一异常渲染处理 * 身份认证中间件 为了帮助开发者快速掌握 CatchAdmin 专业版 `app` 应用端的标准化开发架构,本文将详细介绍每个功能模块的实现方法和最佳实践,建议在开始 API 开发前完整阅读 :::info 移动应用端 API 路由统一使用 `api/app` 前缀,可通过 `php artisan route:list | grep api/app` 命令查看所有可用接口 ::: :::warning 移动应用端采用独立架构设计,与后台管理系统在业务逻辑上完全分离,仅在数据模型层共享。应用端通过调用各模块的 Eloquent 模型来实现数据访问。 如需更规范的数据访问层管理,建议在 `app` 应用中引入仓库模式(Repository Pattern)。 ::: ## JWT 用户认证系统 CatchAdmin 专业版移动应用端采用 JWT(JSON Web Token)作为身份认证解决方案。相比后台管理使用的 Laravel Sanctum,JWT 更适合移动端和分布式应用场景,主要优势包括 * `santum` 一次身份正常需要查询两次,`JWT` 只有一次 * `JWT` 的 Claim 可以承载更多的信息,无需回表。`sanctum` 需要从数据库查询 * `sanctum` 只适合单体应用,`JWT` 兼容单体,并支持分布式架构 基于性能和扩展性考虑,在高并发移动应用场景下,JWT 认证方案具有明显的性能优势和更好的横向扩展能力,因此 CatchAdmin 专业版应用端选择 JWT 作为标准身份认证方案。 ### 安装 CatchAdmin 专业版 3.2.0+ 版本内置 JWT 认证支持,无需额外配置。低于此版本需要手动安装 JWT 扩展包 :::warning 请确认 CatchAdmin 专业版版本:< 3.2.0 需要执行以下安装步骤,>= 3.2.0 版本可跳过此节。 ::: ```shell composer require "tymon/jwt-auth" ``` 然后发布 `jwt` 配置文件 ```shell php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider" ``` 再生成 `jwt` 密钥 ```shell php artisan jwt:secret ``` `JWT` 配置请查看配置文件 `config/jwt.php` ### 路由 移动应用端当前提供基础的用户认证 API 接口:`登录`和`登出`。完整路由配置位于 `routes/api.php` 文件 ```php Route::prefix('app')->group(function (){ // 登录 Route::post('login', [AuthController::class, 'login']); // 登出 Route::post('logout', [AuthController::class, 'logout'])->middleware('auth:app'); }); ``` ### auth 配置 在 `config/auth.php` 配置文件中,针对移动应用端的身份认证守卫(Guard)配置如下 ```php return [ 'guards' => [ // 前台 app 接口认证 'app' => [ 'driver' => 'jwt', 'provider' => 'app_users', ] ], 'providers' => [ // 前台用户模型 'app_users' => [ 'driver' => 'eloquent', 'model' => \Modules\Member\Models\Members::class // 使用会员表 ], ], ]; ``` ### 认证代码 移动应用端支持多种身份认证方式:`密码登录`、`微信小程序登录`、`小程序手机号快捷登录`。认证控制器位于 `app/Http/Controllers/AuthController.php`,基于 Laravel Auth 门面实现标准化身份验证流程 ```php use App\Services\Auth\UserService; class AuthController extends Controller { /** * 登录 * * @param Request $request * @param UserService $service * @return JsonResponse */ public function login(Request $request, UserService $service): JsonResponse { // 系统封装了 UserService 用于处理用户登录 $user = $service->setAdapterType($request->get('type'))->auth($request->all()); if (! $user) { throw new UnauthorizedAccessException(); } return $this->success($user); } /** * @return JsonResponse */ public function logout(): JsonResponse { // 退出之后将 token 加入黑名单 $this->appGuard()->logout(true); return ApiResponse::success(); } } ``` 用户认证服务的核心实现逻辑,请参考 `app/Services/Auth/UserService.php` 认证服务类 ```php /* @var Login $loginAdapter */ $loginAdapter = app($this->getLoginAdapter($this->type)); if ($res = $loginAdapter->auth($params)) { [$user, $token] = $res; $user->rememberToken($token); return $user->makeHidden([ 'password', 'from', 'creator_id' ]); } return false; ``` 对应的三种登录方式代码 ::: code-group ```php [密码登录 - 支持用户名/手机号登录] // app/Services/Auth/PasswordLogin.php // 自动识别登录账号类型:手机号格式则使用 mobile 字段,否则使用 username 字段 $field = preg_match('/^1[0123456789]\d{10}$/', $params['account']) ? 'mobile' : 'username'; // Auth 认证 $token = Auth::guard('app')->attempt([ $field => $params['account'], 'password' => $params['password'], ]); if ($token) { return [Auth::guard('app')->user(), $token]; } return false; ``` ```php [微信小程序登录 - OAuth2.0 授权登录] // app/Services/Auth/WechatLogin.php try { // 通过微信小程序 code 换取 session_key 和 openid $response = $this->getMiniAppApplication()->getUtils()->codeToSession($params['code']); $user = $this->getAuthModel()->firstOrCreate([ 'miniapp_openid' => $response['openid'], ],[ 'username' => $params['username'], 'from' => 'miniprogram', 'miniapp_openid' => $response['openid'], 'avatar' => $this->storeAvatar(), 'mobile' => '', 'created_at' => time(), 'last_login_at' => time(), 'updated_at' => time(), ]); if (! $user) { return false; } return [$user, JWTAuth::fromUser($user)]; } catch (\Throwable $e) { throw new UnauthorizedAccessException(); } ``` ```php [微信小程序手机号快捷登录 - 一键授权绑定] // app/Services/Auth/WechatByMobileLogin.php try { // 获取微信小程序用户的 openid $openid = $this->getMiniAppOpenId($params['code']); // 检查是否已存在该 openid 的用户记录 $user = $this->getAuthModel()->where('miniapp_openid', $openid)->first(); if ($user) { $user->username = $params['username']; $user->mobile = $this->getMiniAppUserMobile($params['phoneCode']); $user->avatar = $this->storeAvatar(); $user->last_login_at = time(); $user->updated_at = time(); $user->save(); } else { $user = $this->getAuthModel()->firstOrCreate([ 'mobile' => $this->getMiniAppUserMobile($params['phoneCode']), ], [ 'username' => $params['username'], 'from' => 'miniprogram', 'avatar' => $this->storeAvatar(), 'miniapp_openid' => $openid, 'created_at' => time(), 'last_login_at' => time(), 'updated_at' => time(), ]); } if (! $user) { return false; } return [$user, JWTAuth::fromUser($user)]; } catch (\Throwable $e) { throw new UnauthorizedAccessException(); } ``` ::: ## API 认证中间件配置 移动应用端身份认证中间件实现位于 `app/Http/Middleware/AuthMiddleware.php`,负责验证 JWT Token 有效性 ```php class AuthMiddleware { /** * Handle an incoming request. * * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next */ public function handle(Request $request, Closure $next, string $guard): Response { if (! $request->bearerToken()) { throw new TokenMissedException(); } $user = Auth::guard($guard)->user(); if (! $user) { throw new AuthenticationException(); } return $next($request); } } ``` ### 中间件别名 为简化中间件调用,为认证中间件配置别名。在 `bootstrap/app.php` 文件中注册中间件别名 ```php // 在 bootstrap/app.php 文件中注册中间件别名 ->withMiddleware(function (Middleware $middleware) { // $middleware->alias([ // 注册认证中间件别名,支持多种守卫:auth:app(移动端)、auth:web(后台)等 'auth' => AuthMiddleware::class ]); }) ``` 认证中间件通过 Laravel Auth 门面实现多守卫支持,只需传入对应的守卫名称 `$guard` 参数。移动应用端需要身份认证的路由配置示例 ```php // 使用 auth:app 中间件,指定使用移动应用端的 JWT 认证守卫 Route::post('logout', [AuthController::class, 'logout'])->middleware('auth:app'); ``` ## 响应码枚举管理 移动应用端所有枚举类统一管理在 `App\Enums` 目录下,提供类型安全的常量定义。当前内置枚举包括 * Enum 枚举接口 * Code 项目自带的响应码枚举 :::warning 移动应用端开发规范:所有自定义枚举类必须实现 `Enum` 接口,确保代码的一致性和可维护性。 ::: 下面是 `Code` 枚举的具体实现,对于响应的数据的 `code` 可以根据实际需求重新修改 ```php /** * API 响应状态码枚举 - 统一管理移动应用端响应代码 */ enum Code:int implements Enum { case SUCCESS = 10000; case FAILED = 10001; case LOGIN_FAILED = 10002; case AUTH_EXCEPTION = 10003; case TOKEN_EXPIRED = 10004; case TOKEN_INVALID = 10005; case TOKEN_BLACKLIST = 10006; case TOKEN_MISSED = 10007; /** * @return string */ public function message(): string { return match ($this) { self::SUCCESS => 'success', self::FAILED => 'failed', self::LOGIN_FAILED => '登录失败', self::AUTH_EXCEPTION => '认证失败', self::TOKEN_EXPIRED => 'token 过期', self::TOKEN_INVALID => 'token 无效', self::TOKEN_BLACKLIST => 'token 黑名单', self::TOKEN_MISSED => 'token 丢失', }; } /** * @param mixed $value * @return bool */ public function equal(mixed $value): bool { return $this->value === $value; } } ``` ## API 响应格式规范 移动应用端提供统一的 API 响应格式处理类,确保客户端接收到一致的数据结构。可根据项目需求自定义响应格式 ```php /** * 移动应用端统一 API 响应处理类 * 确保所有接口返回一致的数据格式 */ class ApiResponse { /** * 成功响应 - 标准成功数据格式 */ public static function success(mixed $data = [], string $message = 'success', Code $code = Code::SUCCESS): JsonResponse { return response()->json([ 'code' => $code->value, 'message' => $message, 'data' => $data, ]); } /** * 错误响应 - 标准错误信息格式 */ public static function error(string $message = 'api error', int|Code $code = Code::FAILED): JsonResponse { return response()->json([ 'code' => $code instanceof Enum ? $code->value : $code, 'message' => $message, ]); } /** * 分页响应 - 列表数据分页格式 */ public static function paginate(LengthAwarePaginator $paginator, string $message = 'success', Code $code = Code::SUCCESS): JsonResponse { return response()->json([ 'code' => $code->value, 'message' => $message, 'data' => $paginator->items(), 'total' => $paginator->total(), 'limit' => $paginator->perPage(), 'page' => $paginator->currentPage(), ]); } } ``` ## API 异常处理机制 移动应用端在 `App\Exceptions` 目录下提供完整的异常处理机制,支持类型化异常管理和统一错误响应。内置异常类型包括 * `AuthenticationException` 身份认证异常 * `FailedException` 通用的失败异常 * `TokenMissedException` token 修饰异常 * `UnauthorizedAccessException` 登录授权异常 为实现移动应用端异常的统一处理和响应格式标准化,所有自定义异常类必须继承 `ApiAppException` 基类 ```php abstract class ApiAppException extends HttpException { // public function __construct( string $message = '', Code $code = Code::FAILED ) { // 异常所有的 code 都使用枚举值,那么只需要维护 Code 枚举类就可以了 if ($this->code instanceof Enum) { $code = $this->code; $this->message = $this->code->message(); } parent::__construct( $this->statusCode(), $message ?: $this->message, null, [], $code->value ); } /** * @return int */ protected function statusCode(): int { return 500; } } ``` 例如 `AuthenticationException` 异常,只需要这么定义就可以了 ```php use App\Enums\Code; class AuthenticationException extends ApiAppException { protected $code = Code::AUTH_EXCEPTION; } ``` 通过枚举类统一管理异常代码和错误信息,实现了异常处理的类型安全和维护便利性。 ### 统一异常处理 为确保移动应用端获得一致的 JSON 格式错误响应,需要配置全局异常处理器。在 `bootstrap/app.php` 文件的 `withExceptions` 方法中配置 ```php ->withExceptions(function (Exceptions $exceptions) { $exceptions->render(function (Throwable $exception, Request $request) { // 渲染 app 异常,返回错误信息 if ($exception instanceof ApiAppException) { return ApiResponse::error($exception->getMessage(), $exception->getCode()); } // 其他系统异常自行处理, 请根据项目实际情况进行处理 // 如果路由前缀使用 api/app 则返回 api app 异常 if ($request->route()->prefix('api/app')) { return ApiResponse::error($exception->getMessage(), $exception->getCode()); } }); }) ``` ## API 路由组织结构 移动应用端 API 路由按照认证需求进行分组管理,将公开接口和需要身份认证的接口分别组织。在 `routes/api.php` 中按以下结构配置 ```php // 移动应用端 API 路由组织 - 按认证需求分组管理 Route::prefix('app')->group(function (){ // 公开接口:无需身份认证即可访问 Route::post('login', [AuthController::class, 'login']); // 私有接口:需要 JWT Token 认证才能访问 Route::middleware('auth:app')->group(function () { Route::post('logout', [AuthController::class, 'logout']); // 在此添加其他需要认证的 API 接口 }); }); ``` 至此,CatchAdmin 专业版移动应用端开发框架的核心功能已介绍完毕。基于这套标准化的 API 开发架构,您可以快速构建安全、高效的移动应用后端服务。 --- --- url: /front/introduce.md --- # 简介 前端首先由 `Vue3` 构建,所以要想写前端第一步要先看[vue3 文档](https://cn.vuejs.org/)。 第二还需要 [typescript](https://www.tslang.cn/docs/home.html) 的加持 :::info 如果不想用使用 ts,直接在根目录 `tsconfig.json` 关闭即可 ::: 这两个是项目的根基,所以一定要好好看看,其他用到的主要组件 * ElementPlus [官网地址](https://element-plus.org/) * tailwindcss [官网地址](https://tailwindcss.com/) * Pinia [官网地址](https://pinia.vuejs.org/) * icon [hero icon](https://heroicons.com/) ## vite 配置 这里就直接用项目的配置一一说明了,不拆开来看了 ```js const rootPath = resolve(__dirname) // https://vitejs.dev/config/ export default defineConfig(({ command, mode }) => { const env = loadEnv(mode, process.cwd(), '') return { plugins: [ vue({ script: { defineModel: true } }), vueJsx(), alias({ entries: [ { find: '@', replacement: resolve(rootPath, './src') } ] }), { // 监听views文件夹下的vue文件变化,自动重启 configureServer(server) { server.watcher.on('add', (path) => { if (path.includes('views') && path.endsWith('.vue')) { server.restart() } }) } }, AutoImport({ imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'] // resolvers: [ ElementPlusResolver({importStyle: 'sass'}) ] }), Components({ dirs: ['src/components/', 'src/layout/'], extensions: ['vue'], // directoryAsNamespace: true, deep: true, dts: true, include: [/\.vue$/, /\.vue\?vue/], exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/, 'src/components/catchForm/'] // resolvers: [ ElementPlusResolver({ importStyle: 'sass'}) ] }), Icons({ compiler: 'vue3', autoInstall: true }) ], publicDir: './public', define: { BASE_URL: env.BASE_URL }, preprocessorOptions: { scss: { // additionalData: `@use "@/public/assets/styles/element.scss" as *;`, } }, server: { hmr: true, host: '127.0.0.1', port: 10086, open: true, // 自动打开浏览器 cors: true, // 允许跨域 strictPort: true, // 端口占用直接退出 watch: {}, proxy: { // 处理跨域 '/api': { target: env.VITE_BASE_URL, // 后端接口地址 changeOrigin: true, // 修改请求头中的 origin 为目标地址 rewrite: (path) => path.replace(/^\/api/, '') // 可选:移除请求路径中的 /api 前缀 } } }, build: { // 打包文件大小警告的限制(单位kb) chunkSizeWarningLimit: 2000, minify: 'terser', terserOptions: { compress: { drop_console: false, pure_funcs: ['console.log', 'console.info'], drop_debugger: true } }, // 打包输出目录 outDir: '../public/admin', assetsDir: 'assets', rollupOptions: { input: './index.html', output: { chunkFileNames: 'assets/js/[name]-[hash].js', entryFileNames: 'assets/js/[name]-[hash].js', assetFileNames: 'assets/[ext]/[name]-[hash].[ext]' } } } } }) ``` :::info vite 的环境变量都需要在前面加上 `VITE` 前缀,在 **vite.config.js** 使用的时候直接访问 `env.BASE_URL`即可 ::: 但是如果打包的话,还需要添加 `.env.production` 配置文件,里面配置 ```js // api url 接口地址 VITE_BASE_URL = API_URL ``` --- --- url: /server/promise.md --- # 约定 * [约定大于配置](#promise) **约定大于配置**,这句名言相信很多开发者都听过。无论是框架还是软件包,都会遵循一定的约定规范。当我们熟悉并遵循这些约定后,开发工作将变得更加简单高效。`CatchAdmin` 同样遵循这一理念,制定了一系列开发约定。在开始开发前,了解这些约定是非常必要的。 ## 核心约定 ### 目录结构约定 * **后台功能开发**:所有后台功能模块统一放置在 `modules` 目录下 * **前端开发**:`web` 目录作为前端开发的根目录,主要用于公共组件的开发和管理 * **枚举类型**:所有枚举类型必须实现 `Catch\Enums\Enum` 接口 ::: info 版本要求 框架最低版本要求 PHP 8.1,因此可以使用 PHP 8.1 新增的枚举类型 `enum`。 ::: ### 枚举值约定 * **整型枚举起始值**:所有整型枚举值必须从 `1` 开始,不能从 `0` 开始 ::: tip 为什么从 1 开始? 这是因为枚举值集合通常会用于列表查询等场景。由于 PHP 是弱类型语言,在不使用严格比较(`===`)的情况下,`0`、`null` 和空字符串 `""` 会被视为相等,这可能导致逻辑错误。从 `1` 开始可以有效避免这类问题。 ::: ### 公共模块约定 所有公共的请求接口和公共方法都应统一放置在内置的 `Common` 模块中,主要包括: * **文件上传功能**:统一的文件上传处理逻辑 * **前端枚举集合请求**:为前端提供枚举数据的接口 ::: info 参考说明 具体的实现方式和使用方法,请参考 `Common` 模块内的 HTTP 公共请求接口,每个接口都有详细的使用说明。 ::: --- --- url: /front/components.md --- # 组件 文档上无法显示具体组件的效果,所以组件效果在后台展示[组件](https://pro.catchadmin.com/#/components/button) --- --- url: /start/rules.md --- # 规范 CatchAdmin 是一款基于现代化技术栈构建的前后端分离框架,采用模块化架构设计。为了确保代码质量和项目的长期可维护性,制定了一套完整的开发规范体系。 这套规范体系具有以下特点: * **标准化**:严格遵循社区公认的编码标准,具有广泛的适用性 * **模块化**:支持功能模块的独立开发和维护,提高代码复用性 * **可维护性**:通过统一的规范降低代码维护成本和团队协作难度 所有规范均基于行业最佳实践和社区标准制定,而非个人偏好,确保了规范的权威性和普适性。 ## 代码规范 ### PHP 代码规范 项目严格遵循 `PSR-12` 编码标准,这是 PHP 社区广泛认可的代码风格规范,确保代码的一致性和可读性。通过统一的编码风格,团队成员可以更容易地理解和维护彼此的代码。 **代码格式化工具:** 项目集成了 `Laravel Pint` 作为代码格式化工具,它基于 PHP CS Fixer 构建,专为 Laravel 项目优化。配置文件 `pint.json` 位于项目根目录: ```json { "preset": "psr12", "rules": { "braces": false }, "exclude": ["database"], "notName": ["server.php"] } ``` **配置说明:** * `preset`: 使用 PSR-12 预设规则 * `rules.braces`: 禁用大括号格式化规则,保持 Laravel 风格 * `exclude`: 排除 database 目录,避免格式化迁移文件 * `notName`: 排除特定文件名 **IDE 集成:** 建议在 PHPStorm 中配置自动格式化,在保存文件时自动应用代码规范。也可以使用命令行进行批量格式化: ```shell composer pint ``` 该命令会格式化项目中所有符合条件的 PHP 文件,确保代码风格的一致性。 ### 前端代码规范 前端采用现代化的 `Vue 3 + TypeScript` 技术栈,项目文件独立存放在 `web` 目录中。TypeScript 提供了强类型支持,显著提高了代码的健壮性和开发效率,而 Vue 3 的 Composition API 则提供了更好的逻辑复用和代码组织能力。 **代码格式化配置:** 使用 `Prettier` 作为代码格式化工具,确保团队代码风格的统一。VS Code 用户可安装 Prettier 插件实现自动格式化。配置文件 `.prettierrc` 位于 web 目录下: ```json { "semi": false, "printWidth": 200, "tabWidth": 2, "useTabs": false, "singleQuote": true, "arrowParens": "avoid", "trailingComma": "none", "bracketSpacing": true } ``` **配置项说明:** * `semi`: 不使用分号结尾,保持代码简洁 * `printWidth`: 单行最大长度 200 字符,适应现代宽屏显示器 * `tabWidth`: 使用 2 个空格缩进,符合前端社区习惯 * `singleQuote`: 使用单引号,与 Vue 生态保持一致 * `arrowParens`: 箭头函数单参数时省略括号 * `trailingComma`: 不使用尾随逗号,保持代码整洁 ## 项目开发规范 项目采用模块化架构设计,将复杂的业务功能拆分为独立的功能模块,每个模块具有完整的业务闭环和清晰的职责边界。这种设计模式提高了代码的可维护性、可测试性和团队协作效率。 **模块化设计原则:** * **单一职责**:每个模块专注于特定的业务领域 * **高内聚低耦合**:模块内部功能紧密相关,模块间依赖最小化 * **独立部署**:支持模块的独立开发、测试和部署 **标准目录结构:** 以权限模块(`modules/Permissions`)为例,展示标准的模块目录组织: ``` ├─permissions │ ├─database # 数据库相关文件 │ │ ├─migrations # 数据库迁移文件 │ │ └─seeders # 数据填充文件 │ ├─Exceptions # 模块专用异常类 │ ├─Middlewares # 模块中间件 │ ├─Providers # 服务提供者 │ ├─routes # 模块路由定义 │ ├─HTTP # HTTP层业务逻辑 │ │ ├─Controllers # 控制器类 │ │ └─Requests # 表单验证请求类 │ └─Models # 数据模型类 ├─Installer.php # 模块安装器 ``` **架构分层策略:** 项目采用灵活的分层架构,根据模块复杂度选择合适的分层模式,避免过度设计。 **轻量级分层(适用于简单模块):** 对于权限管理等相对简单的模块,采用 `Controller → Model → Request` 的轻量级分层: * **Controller**:处理 HTTP 请求和响应 * **Model**:数据模型和基础业务逻辑 * **Request**:表单验证和数据处理 * **Trait**:公共逻辑的复用机制 这种模式的优势: * 调用链路短,便于调试和维护 * 代码结构清晰,学习成本低 * 避免过度抽象带来的复杂性 **标准分层(适用于复杂模块):** 对于商城管理等复杂业务模块,引入 `Controller → Service → Repository → Model` 分层: * **Service 层**:封装复杂的业务逻辑和数据处理 * **Repository 层**:数据访问抽象,支持多数据源 * **业务聚合**:处理跨模块的业务协调 **分层选择原则:** * **复杂度驱动**:根据业务复杂度选择合适的分层深度 * **团队协作**:考虑团队规模和协作模式 * **维护成本**:平衡代码结构和维护难度 * **性能考量**:避免过深的调用链影响性能 通过这种灵活的分层策略,既保证了代码的可维护性,又避免了不必要的复杂性。 --- --- url: /front/request.md --- # 请求 前端请求默认使用的是 `axios`,但是为了方便,后台提供了 `Http` 对象快速发起请求 ## 请求 ```typescript title="web/src/support/http.ts" import Http from '/admin/support/http' // GET 请求 http.get(path: string, params: object = {}) // POST 请求 http.post(path: string, data: object = {}) // PUT 请求 http.put(path: string, data: object = {}) // DELETE 请求 http.delete(path: string) ``` ### 设置超时 ```typescript Http.timeout(5).get() ``` ### 设置 BASEURL ```typescript Http.setBaseUrl('https://api.com').get() ``` ### 设置 header ```typescript Http.setHeader(key:string, value:string).get() ``` ## 表单请求 表单请求则使用了 `vue3` 的新特性 `hooks`,也称为[组合式函数](https://cn.vuejs.org/guide/reusability/composables.html)。查看 `web/src/composables/curd`,总共提供六个操作。 ### GetList `getList` 请求列表数据 ```typescript const { data, query, search, reset, loading } = useGetList(api) // 接口返回的数据必须 computed 才具备响应 const tableData = computed(() => data.value?.data) ``` * `data` 接口返回的数据 * `query:{}` 查询数据 * `search()` 搜索方法 * `reset()` 重制方法 * `loading:boolean` 列表请求 loading ### Create **create** 其实包含两个操作,创建和更新,当 **props.primary** 是 **null** 的时候,就是创建数据不为空时,则是更新数据 ```typescript const { formData, form, loading, submitForm, close } = useCreate(props.api, props.primary) // 更新的 ID if (props.primary) { useShow(props.api, props.primary, formData) } // 关闭弹窗 const emit = defineEmits(['close']) close(() => emit('close')) ``` * formData 提交的 Form 数据 * form 表单 ref * loading 提交数据表单 loading * submitForm(form) 点击提交表单的方法, 参数就是 `form` * close 关闭弹窗 ### Destroy ```php const { destroy, deleted } = useDestroy() onMounted(() => { // 观察数据是否删除,删除之后刷新列表 deleted(reset) }) ``` * destory(path: string, id: string | number) 删除数据的方法,一般都是用于列表删除数据 * deleted(callback: Function) 观测数据是否删除,参数删除后的回调操作 ### Enabled `enabled` 作用就是请求状态切换 ```php const { enabled, success, loading, afterEnabled } = useEnabled() ``` * enabled(path: string, id: string | number, data: object = {}) 请求切换 * success(callback: Function) 参数成功后的回调函数 * loading 请求时 loading * afterEnabled 请求完成之后的操作, 只有设置成方法才会被调用 ```php afterEnabled.value = () => {} ``` ### Open 打开 `Dialog` 弹窗,一般用于通过`Dialog` **创建/更新**数据的时候 ```php const { open, close, title, visible, id } = useOpen() ``` * open(primary: any = null) 显示`Dialog` * close(callback: Function) 关闭 `Dialog` callback 关闭后的回调方法 * title: string Dialog 标题 * visible: boolean Dialog 状态 * id 数据的 ID ### Show show 方法就是拉取更新时的数据,填充表单 ```php if (props.primary) { useShow(props.api, props.primary, formData) } ``` --- --- url: /start/debug.md --- # 调试 现在框架集成比较多的调试方案,例如 Query log,打印(dd\_)以及异常处理显示。这三个工具在很大程度可以帮助我们调试好程序,但是有时候我们需要看一些更具体的指标,类似于 `laravel debugbar` 这样的插件。偶然间我发现了这么一个非常好用的插件,当然这个插件只能帮助看一些指标,不能打印信息。 ## 介绍 [clockwork](https://underground.works/clockwork/), 想要使用这个插件,需要安装两个部分 ### 安装 package ```shell composer require itsgoingd/clockwork --ignore-platform-reqs --dev ``` ### 安装浏览器扩展 因为本人只使用 chrome 浏览器,其他浏览器可以到他的官方看相关说明。安装主要[clockwork 扩展](https://chromewebstore.google.com/detail/dmggabnehkmmfmdffgajcflpdjlnoemp) 如图,这里我已经安装过 ![CatchAdmin 专业版调试](https://image.catchadmin.com/202409092307255.png) ## 使用 打开 f12, 点击这个栏目,你可以看到每个请求的所有一些信息 ![CatchAdmin 专业版调试](https://image.catchadmin.com/202409092308363.png) * 性能 (performance) * 事件(events) * 模型 (model) * database * cache * Header * Middleware ... 等等信息 例如缓存,请求中命中的缓存,以及缓存的过期事件都会显示出来 ![CatchAdmin 专业版调试](https://image.catchadmin.com/202409092312616.png) 如果你需要更具体信息,那么需要开启 `xdebug`,如下图,开启 xdebug 之后,可以清楚看到每个函数执行的时间,一目了然 ![CatchAdmin 专业版调试](https://image.catchadmin.com/202409092313275.png) 我这里是使用的 `windows` 系统,对于 `macos` 和 `linux` 直接使用 ```shell pecl install xdebug ``` `windows` 系统,需要到 pecl 官网下载,找到对应的 PHP 版本下载 [xdebug 下载](https://pecl.php.net/package/xdebug) [xdebug PHP 对应版本](https://xdebug.org/docs/compat) 安装到对应的 ext 目录之后,到 `php.ini` 文件使用下面的配置 ```php zend_extension=php_xdebug xdebug.mode=profile xdebug.start_with_request=yes ``` --- --- url: /uniapp/route-interceptor.md --- # 路由拦截 在小程序中,需要做类似后台页面的访问权限的拦截,防止用户直接访问某些页面。一般情况下,分登录可访问和非登录可访问的页面。一般可以这么做,例如登录页面,需要在非登录情况下才可以访问。就会有下面的代码 ```ts import { useUserStore } from '@/store/user' // 从 store 中获取当前登录的用户信息 const userStore = useUserStore() // 如果用户登录直接跳转到用户中心页面 if (userStore.isLogined) { toUserCenter() } ``` 如果页面不多的情况,硬编码的话其实还能接受,如果是大量页面,这样的代码就会重复很多,而且维护起来非常麻烦。如果页面不光是这种简单认证权限,还有更复杂的权限控制,比如需要根据用户的角色来决定是否可以访问某个页面,那么就需要使用路由拦截器来实现。 找到对应的路由文件 `interceptors\route.ts` ```ts import { useUserStore } from '@/store' import { getNeedLoginPages, needLoginPages as _needLoginPages } from '@/utils' // TODO Check const loginRoute = '/pages/login/index' const isLogined = () => { const userStore = useUserStore() return userStore.isLogined } const isDev = import.meta.env.DEV // 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录) const navigateToInterceptor = { // 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同 invoke({ url }: { url: string }) { console.log(url) // /pages/route-interceptor/index?name=feige&age=30 const path = url.split('?')[0] let needLoginPages: string[] = [] // 为了防止开发时出现BUG,这里每次都获取一下。生产环境可以移到函数外,性能更好 if (isDev) { // 重点这里获取需要登录的页面,找到 getNeedLoginPages() 方法 needLoginPages = getNeedLoginPages() } else { needLoginPages = _needLoginPages } console.log(needLoginPages.includes(path)) if (needLoginPages.includes(path)) { const isLogin = isLogined() if (isLogin) { return true } const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}` uni.navigateTo({ url: redirectRoute }) return false } return true } } ``` ```ts export const getAllPages = (key = 'needLogin') => { const pages = [ ...pagesJson.pages // 最终在这里 // page 设置的时候需要加上 `needLogin` key .filter((page) => !key || page[key]) .map((page) => ({ ...page, path: `/${page.path}` })) ] ... } ``` ## 配置 找到跟目录的 `pages.json` 文件,在 `pages.json` 文件中找到如下的配置 ```json { "pages": [ { "path": "pages/index/index", "type": "home", "style": { "navigationStyle": "custom", "navigationBarTitleText": "首页" } }, { "path": "pages/login/index", "type": "page", "layout": "default" }, { "path": "pages/user/index", "type": "page", "needLogin": true, // 添加需要登录的标记即可 "style": { "navigationStyle": "custom", "navigationBarTitleText": "我的" } } ] } ``` --- --- url: /tenancy/deploy.md --- # 部署 成功完成上述安装步骤后,接下来需要详细了解如何部署多租户项目。由于本多租户系统主要通过域名来识别和分配不同的租户,因此配置流程是从单应用架构自然演变而来的。首先,我们需要正确配置 `Nginx` 服务器以支持多域名访问: ```json server { listen 443 ssl; http2 on; // 这里需要配置二级泛域名,确保可以捕获所有子域名请求 server_name catchadmin.com *.catchadmin.com; index index.html index.php index.htm default.php default.htm default.html; } ``` ## 配置域名 在 CatchAdmin 专业版中,我们提供了完善的域名管理功能,目前已集成 `阿里云` 和 `腾讯云` 两大主流云服务平台。要开始使用此功能,首先需要启用 `域名管理` 模块: :::warning 如果您使用的是其他云服务平台,请自行前往相应平台的控制台进行域名配置 ::: 导航至 `域名管理` -> `域名配置` 页面: ![catchadmin 多租户域名配置](https://image.catchadmin.com/202506241153197.png) 在此页面中添加相应平台的 API 密钥配置信息,完成后务必点击保存按钮确认更改 接下来,导航至 `域名管理` -> `域名列表` 页面,点击添加按钮来注册您的主域名,操作如下图所示: ![catchadmin 多租户域名配置](https://image.catchadmin.com/202506241156538.png) 域名添加成功后,您将在列表中看到如下所示的域名信息: ![catchadmin 多租户域名配置](https://image.catchadmin.com/202506241157974.png) :::warning 请注意:使用阿里云平台时,系统无法获取域名的过期时间数据,该字段将显示为空 ::: 完成域名添加后,点击操作栏中的 `解析` 按钮进入解析管理页面。在该页面中,点击新增按钮来添加域名解析记录,如下图所示: :::warning 重要提示:所有解析记录必须指向您的实际服务器 IP 地址,否则租户将无法正常访问系统 ::: ![catchadmin 多租户域名配置](https://image.catchadmin.com/202506241159130.png) 域名解析记录添加成功并生效后(通常需要几分钟至几小时,取决于 DNS 传播速度),您就可以继续前往租户列表页面进行下一步的租户配置工作了 ## 新增套餐 在创建租户之前,需要先设置租户可用的套餐方案。请导航至 `多租户` -> `套餐管理` 页面,点击添加按钮创建新的套餐: ![catchadmin 多租户套餐管理](https://image.catchadmin.com/202506241205851.png) 在套餐创建过程中,请务必仔细选择该套餐所包含的权限范围,这将直接决定使用此套餐的租户能够访问的系统功能和资源。权限设置是租户管理的核心环节,请确保权限分配符合您的业务需求 ## 新增租户 完成套餐配置后,您可以开始创建租户账户。请导航至后台管理系统中的 `多租户` -> `租户管理` 页面: ![catchadmin 多租户配置](https://image.catchadmin.com/202506241202421.png) 在添加租户表单中,请认真填写租户的各项信息。特别需要注意的是,所选择的域名必须是已经完成 DNS 解析且解析记录已生效的域名,否则租户将无法正常访问系统。建议在添加租户前,先使用 ping 或其他 DNS 查询工具验证域名解析是否已生效。 --- --- url: /tenancy/config.md --- # 配置 由于目前的租户架构使用域名分配,使用域名来识别对应的租户。所以需要区分中央域和租户域名和租户域名。第一个就需要配置我们的中央域名 ```php TENANT_CENTRAL_DOMAIN=你得中央域名:例如 pro.catchadmin.com,不需要 http 协议 ``` ## 队列监控 在根目录的 `.env` 文件默认使用队列监控的,如果不想监控队列任务,则配置关闭即可 ```php TENANCY_QUEUE_MONITOR=true ``` ## 单独数据库实例 如果需要给租户添加单独的租户,需要在 `config/database.php` 中配置单独数据库链接实例。 ```php // 这里做演示,实例单独实例链接名称需要自己定义 return [ 'connections' => [ 'tenant_connection' => [ 'driver' => 'mysql', 'host' => env('DB_TENANT_HOST', '127.0.0.1'), 'port' => env('DB_TENANT_PORT', '3306'), 'database' => env('DB_TENANT_DATABASE', 'forge'), 'username' => env('DB_TENANT_USERNAME', 'forge'), 'password' => env('DB_TENANT_PASSWORD', ''), 'unix_socket' => env('DB_TENANT_SOCKET', ''), 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'strict' => true, 'engine' => null, ], ], ]; ``` 然后在后台租户管理给对应的租户配置对用的数据库实例,如下图 ![多租户-数据库实例](https://image.catchadmin.com/202507101811825.png) --- --- url: /tenancy/queue.md --- # 队列 在多租户系统中,队列默认驱动使用`database`, 具体配置如下: ```php // config/tenancy.php [ /** * Queue 租户配置 */ 'queue' => [ // Queue 队列 'connections' => [ // 主域 'central' => [ 'driver' => 'database', 'connection' => env('DB_CONNECTION', 'mysql'), 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 90, 'central' => true, // 主域一定要配置这个 ], // 租户 'tenant' => [ 'driver' => 'database', 'table' => 'jobs', 'connection' => env('DB_CONNECTION', 'mysql'), 'queue' => 'tenant', 'retry_after' => 90, ], ], ] ] ``` 然后使用下面的命令创建租户的队列任务 ```php php artisan catch:tenant:job TenancyTest ``` 这个命令会在 `\app\Jobs` 目录创建一个名为 `TenancyTest.php` 得任务文件,内容如下: ```php namespace App\Jobs; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; class TenancyTest implements ShouldQueue { use Queueable; /** * Create a new job instance. */ public function __construct() { $this->onConnection('tenant'); } /** * Execute the job. */ public function handle(): void { // } } ``` 具体业务可以在 `handle`, 租户包会通过任务上下文来识别租户。这个无需关心。 --- --- url: /optimize.md --- # 项目优化 ## 移除 在生产环境需要将后台两个模块移除后再编译 ```js // 找到 web/src/router/index.ts 文件 // 找到这两个路由 { path: '/develop', component: () => import('@/layout/index.vue'), meta: { title: '开发工具', icon: 'wrench-screwdriver' }, children: [ { path: 'modules', name: 'modules', meta: { title: '模块管理' }, component: () => import('@/views/develop/module/index.vue') }, { path: 'schemas', name: 'schemas', meta: { title: 'Schemas' }, component: () => import('@/views/develop/schema/index.vue') }, { path: 'generate/:schema', name: 'generate', meta: { title: '代码生成', hidden: true, active_menu: '/develop/schemas' }, component: () => import('@/views/develop/generate/index.vue') } ] }, { path: '/components', component: () => import('@/layout/index.vue'), meta: { title: '组件演示', icon: 'wrench-screwdriver' }, children: [ { path: 'button', name: 'button', meta: { title: '按钮' }, component: () => import('@/views/components/button/index.vue') }, { path: 'icons', name: 'icons', meta: { title: '图标' }, component: () => import('@/views/components/icons/index.vue') } ] } ``` 将这两个路由注释掉,可以减少项目的大小 ## 后端优化 一定要执行下面几个优化命令 ```shell # 提高类加载速度 composer composer dump-autoload -o --no-dev --classmap-authoritative # 缓存 Laravel 路由表 php artisan route:cache # 缓存 Laravel 配置表 php artisan config:cache ``` --- --- url: /start/deploy.md --- # 项目部署指南 ## CatchAdmin 一键快捷部署 CatchAdmin 提供了一个自动化快速打包部署功能,支持前端 Vue 项目和后端 PHP Laravel 项目的一键打包,只需要运行对应的 Artisan 命令即可完成整个项目的部署准备工作 ```shell php artisan catch:build ``` 该命令将会自动打包前端项目之后,将整个项目压缩成 zip 文件。使用者可以自己上传 zip 文件到服务器解压配置线上环境 如果打包过程遇到前端类型检查的问题,可以选择忽略类型检查 ```shell php artisan catch:build --no-check ``` [更多其他的 CatchAdmin 开发相关的内置命令](../server/consoles.md) :::tip 注意:此命令会将整个 CatchAdmin 项目打包,包含 Composer vendor 依赖文件夹,因此请确保线上生产环境与本地开发环境的 PHP 版本和扩展保持一致 ::: ## 前端 Vue 项目打包部署 Vue 前端项目打包前,需要先配置生产环境 API 接口地址。在前端项目根目录下的 `.env.production` 环境配置文件中(如果没有,请先创建该文件)配置生产环境的 API 基础地址 ``` # base api // 例如 https://api.catchadmin.com/api/ VITE_BASE_URL = '正式环境的 API 地址' ``` Vue 项目环境配置完成后,使用 Yarn 包管理器执行以下命令进行生产环境打包构建 ``` yarn run build ``` ### 打包出现报错 如果打包出现 ts 过多的类型错误,而你对类型又不太敏感的话,对应用没有影响。一个快速的解决办法就是修改 `package.json 文件` build 命令 ```json { "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", // [!code --] "build": "vite build", // [!code ++] "preview": "vite preview" } } ``` 在打包完成之后呢,会在前端项目的根目录生成一个 `dist` 目录,`dist` 目录就是打包后的前端项目,是一个纯 html Js 项目,直接上传到服务器即可。 :::tip 建议在 Nginx 或 Apache 服务器配置中开启 Gzip 压缩,可以显著提升 Vue 前端静态资源的加载速度和用户体验。 ::: ## 后端 PHP Laravel 项目部署 CatchAdmin 后端基于 PHP Laravel 框架开发,你只需要将整个 PHP 项目源码上传至 Linux 服务器即可。对于 Composer 依赖包 `vendor` 文件夹,推荐在服务器端执行 `composer install` 安装依赖,而不是直接上传。如果遇到 Composer 网络安装问题,可以查看 [使用镜像](../faq.md#使用腾讯云镜像) :::warning 如果使用了 CatchAdmin CLI 工具初始化的项目目录结构,在上传 PHP Laravel 后端项目时,一定要排除 `web` 目录,因为该目录包含的是 Vue 前端源码,不需要与后端项目一并上传到服务器。前端部署只需要 Yarn 构建后的 `dist` 目录即可。 ::: ### 上线注意点 * `.env` 环境文件是否配置好? :::tip APP\_URL 一定要和上面前端打包的配置 VITE\_BASE\_URL 相同 还有数据库信息 ::: * 数据库表是否同步? * 数据表的数据是否同步,主要是**权限菜单**表`permissions`里是否同步 * 模块是否开启? 模块如果没有开启,整个项目都会无法正常运行 :::tip 一定要检查线上项目`storage/app/modules.json` 是否存在。如果不存在,要将本地项目`storage/app/modules.json`上传到服务器 ::: * 模块如果正常开启的状态下,路由还是无法正常工作 :::tip * 首先是用 php artisan route:clear * 然后查看路由 php artisan route:list * 最后缓存路由 php artisan route:cache ::: ## 生产环境服务器部署配置 :::warning 如果你使用的是宝塔面板进行服务器管理,请不要完全复制以下 Nginx 配置文件。宝塔面板已预配置了许多项目,如 HTTPS SSL 证书配置、PHP-FPM 管理等,这些无需手动配置。 ::: ### 前后端分离部署(双域名 Nginx 配置) 前后端分离部署是将 Vue 前端项目和 PHP Laravel 后端项目分别部署到不同域名。这里以 `/www` 作为服务器根目录示例,前端 Vue 构建产物部署到 `/www/admin` 目录,后端 PHP Laravel 项目部署到 `/www/api` 目录 :::tip 这里的目录只做展示说明使用,实际部署请按照自身需求设置 ::: * `/www/admin` 上传 `dist` 目录内容到 admin 目录中 * `/www/api` 上传后端项目到 api 目录中 ::: code-group ```nginx [Vue 前端项目 Nginx 配置] server { listen 80; server_name admin.catchadmin.com; return 301 https://admin.catchadmin.com$request_uri; } server { listen 443 ssl http2; server_name admin.catchadmin.com; index.html index.php index.htm default.php default.htm default.html; ssl_certificate # pem文件的路径 ssl_certificate_key # key文件的路径 # HTTPS SSL 证书验证相关配置 ssl_session_timeout 5m; #缓存有效期 ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; root /www/admin; location / { try_files $uri $uri/ /index.html =404; } } ``` ```nginx [PHP Laravel 后端项目 Nginx 配置] server { listen 80; server_name api.catchadmin.com; return 301 https://api.catchadmin.com$request_uri; } server { listen 443 ssl http2; server_name api.catchadmin.com; index index.html index.php index.htm default.php default.htm default.html; root /www/api/public; ssl_certificate /etc/nginx/acme/catchadmin.com/catchadmin.com.cer; # pem文件的路径 ssl_certificate_key /etc/nginx/acme/catchadmin.com/catchadmin.com.key; # key文件的路径 ssl_session_timeout 5m; #缓存有效期 ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=/$1 last; break; } } # PHP Laravel-FPM 支持配置 location ~ \.php$ { try_files $uri /index.php =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } ## Nginx 访问日志和错误日志配置(请根据实际需求配置) access_log; error_log; } ``` ::: ### 前后端合并部署(单域名 Nginx 配置) :::warning 如果使用宝塔部署,请看[宝塔部署章节](./bt-deploy.md) ::: 前后端合并部署指的是在同一个域名下同时提供前端页面和后端 API 服务。继续使用上述 `api` 目录作为示例,该目录部署 PHP Laravel 后端项目,Vue 前端构建产物放置到 `public/admin` 目录下,通过 Nginx 路由规则实现统一域名访问 ```nginx server { listen 80; server_name api.catchadmin.com; return 301 https://api.catchadmin.com$request_uri; } server { listen 443 ssl http2; server_name api.catchadmin.com; index index.html index.php index.htm default.php default.htm default.html; root /www/api/public; ssl_certificate # pem文件的路径 ssl_certificate_key # key文件的路径 ssl_session_timeout 5m; #缓存有效期 ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; # 因为接口都是以 api.catchadmin.com/api 开头,所以可以很好的使用 location # 如果访问 api.catchadmin.com/api 目录 则用 php 解释下 location /api { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=/$1 last; break; } } # 根目录访问直接提供 Vue 前端静态文件 location / { root /www/api/public/admin; try_files $uri $uri/ /index.html; } # CatchAdmin 文件上传静态资源访问配置 location /uploads/ { alias /www/api/public/storage/uploads/; autoindex on; } # PHP Laravel-FPM 处理配置 location ~ \.php$ { try_files $uri /index.php =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } ``` ### Laravel Octane 高性能部署配置 Laravel Octane 是 Laravel 官方提供的高性能应用服务器,支持 Swoole 和 RoadRunner 驱动,可以显著提升 CatchAdmin 后端 API 的并发处理能力 ```php map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name api.catchadmin.com; return 301 https://api.catchadmin.com$request_uri; } server { listen 443 ssl http2; server_name api.catchadmin.com; index index.html index.php index.htm default.php default.htm default.html; root /www/api/public; ssl_certificate # pem文件的路径 ssl_certificate_key # key文件的路径 ssl_session_timeout 5m; #缓存有效期 ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; # 因为接口都是以 api.catchadmin.com/api 开头,所以可以很好的使用 location # 如果访问 api.catchadmin.com/api 目录 则用 php 解释下 location /api { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=/$1 last; break; } } # 根目录访问直接提供 Vue 前端静态文件 location / { root /www/api/public/admin; try_files $uri $uri/ /index.html; } # CatchAdmin 文件上传静态资源访问配置 location /uploads/ { alias /www/api/public/storage/uploads/; autoindex on; } location @octane { set $suffix ""; if ($uri = /index.php) { set $suffix ?$query_string; } proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header Scheme $scheme; proxy_set_header SERVER_PORT $server_port; proxy_set_header REMOTE_ADDR $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_pass http://172.18.0.2:9800$suffix; } } ```