# 4.6 核心基类体系 :::tip 概述 项目有四个核心基类,构成"控制器 → 逻辑层 → 模型"三层架构的骨架。所有业务模块都必须继承对应的基类,通过属性配置 + 生命周期钩子实现声明式 CRUD,子类只需声明"做什么"而不用写"怎么做"。 ::: ## 基类继承关系 :::warning 继承规则 - 系统共享模块(Menu、Dict、Config 等)继承 `BaseLogic` - 租户隔离模块(User、Role、Article、Dept、Level 等)继承 `BaseTenantLogic` - 所有模型继承 `BaseModel`,所有控制器继承 `BaseController` ::: ```text BaseController(控制器基类) └── 所有 Controller BaseLogic(逻辑基类 - 系统共享模块) ├── MenuLogic ├── DictLogic ├── ConfigLogic └── ... │ └── BaseTenantLogic(租户逻辑基类,继承 BaseLogic) ├── UserLogic ├── RoleLogic ├── ArticleLogic ├── DeptLogic ├── LevelLogic └── ... BaseModel(模型基类,继承 ThinkPHP Model) └── 所有 Model ``` ## 请求处理全链路 理解基类体系的关键是理解一次请求如何流经各层: ```text HTTP 请求 │ ▼ BaseController(参数获取、响应封装) │ getJsonBody() / getParams() │ success() / fail() / _page() │ ▼ BaseLogic(业务编排、属性驱动) │ validateData() → 自动参数校验 │ beforeAdd() → 生命周期钩子 │ add() → 文件处理、唯一性校验、写入 │ afterAdd() → 关联表保存 │ ▼ BaseModel(数据持久化) │ onBeforeInsert → 审计字段自动填充 │ soft_delete → 全局过滤已删除数据 │ ▼ 数据库 ``` --- ## BaseController **文件**:`app/BaseController.php` ### 设计要点 - 通用 CRUD 方法以 `_` 前缀命名,避免与子类路由方法冲突 - 子类只需实现 `initialize()` 并实例化自己的 Logic 层 - 登录用户信息来自 `AuthMiddleware` 注入的 `$request->userInfo` ### 方法分类 ```text ┌─────────────────────────────────────────────────────────────┐ │ BaseController │ ├─────────────────────────────────────────────────────────────┤ │ 统一响应 │ │ success($data, $msg, $raw) 成功响应 │ │ _page($result) 分页响应 │ │ fail($msg, $code, $data) 失败响应 │ ├─────────────────────────────────────────────────────────────┤ │ 参数获取 │ │ getParam($name, $default) 获取单个参数 │ │ getParams() 获取所有参数 │ │ getPageParams() 获取分页参数(含边界限制) │ │ getJsonBody() 获取 JSON 请求体 │ ├─────────────────────────────────────────────────────────────┤ │ 登录用户 │ │ getLoginUser() 获取用户信息对象 │ │ getLoginUserId() 获取用户 ID(未登录返回 0) │ │ getLoginUsername() 获取用户名(未登录返回 '') │ ├─────────────────────────────────────────────────────────────┤ │ 通用 CRUD(子类通过 parent:: 调用) │ │ _index($logic, $where, $with) 分页查询 │ │ _list($logic, $where) 全量列表 │ │ _treeList($logic, $where) 树形列表 │ │ _detail($logic, $id) 详情查询 │ │ _add($logic, $data) 新增 │ │ _edit($logic, $id, $data) 修改 │ │ _remove($logic, $id) 删除 │ │ _batchRemove($logic, $ids) 批量删除 │ ├─────────────────────────────────────────────────────────────┤ │ 手动验证(特殊场景) │ │ validate($data, $validate) 数据验证 │ └─────────────────────────────────────────────────────────────┘ ``` ### 通用 CRUD 方法与 Logic 层的对应关系 | Controller 方法 | Logic 方法 | 说明 | |---|---|---| | `_index($logic)` | `$logic->pageList()` | 分页查询 | | `_list($logic)` | `$logic->allList()` | 全量列表 | | `_treeList($logic)` | `$logic->treeList()` | 树形列表 | | `_detail($logic, $id)` | `$logic->detail($id)` | 详情查询 | | `_add($logic, $data)` | `$logic->add($data)` | 新增 | | `_edit($logic, $id, $data)` | `$logic->update($id, $data)` | 修改 | | `_remove($logic, $id)` | `$logic->delete($id)` | 删除 | | `_batchRemove($logic, $ids)` | `$logic->batchDelete($ids)` | 批量删除 | ### 分页参数 `getPageParams()` 返回 `['pageNo' => int, 'pageSize' => int]`,带边界限制: - `pageNo` 最小为 1 - `pageSize` 限制在 1~100,默认 20 前端传参示例:`GET /api/article/page?pageNo=1&pageSize=10&title=测试&status=1` ### 子类实现示例 ```php class ArticleController extends BaseController { protected ArticleLogic $logic; protected function initialize(): void { $this->logic = new ArticleLogic(); } // 分页查询 — 一行搞定 #[Permission('sys:article:list', '文章列表')] public function page(): Json { return parent::_index($this->logic); } // 详情 #[Permission('sys:article:detail', '文章详情')] public function detail(int $id): Json { return parent::_detail($this->logic, $id); } // 新增(使用 _add 快捷方法) #[Log('文章管理-新增', Log::TYPE_ADD, '新增文章:{title}')] #[Permission('sys:article:add', '添加文章')] public function add(): Json { return parent::_add($this->logic, $this->getJsonBody()); } // 删除 — 一行搞定 #[Permission('sys:article:delete', '删除文章')] public function delete(int $id): Json { return parent::_remove($this->logic, $id); } } ``` --- ## BaseLogic **文件**:`app/BaseLogic.php` BaseLogic 是整个项目的核心,通过"属性配置 + 生命周期钩子"实现声明式 CRUD。子类只需配置属性和按需重写钩子,即可获得完整的增删改查能力。 ### 属性配置清单 | 属性 | 类型 | 说明 | 示例 | |---|---|---|---| | `modelClass` | string | 关联模型类名(**必须**) | `Article::class` | | `validateClass` | string | 验证器类名(设置后 add/update 自动校验) | `ArticleValidate::class` | | `fileFields` | array | 单文件字段列表 | `['cover', 'avatar']` | | `multiFileFields` | array | 多文件字段列表(逗号分隔的多个URL) | `['images']` | | `fileSaveDir` | string | 文件保存子目录 | `'article'` | | `pageLikeFields` | array | LIKE 模糊匹配字段 | `['title', 'author']` | | `pageEqFields` | array | 精确匹配字段 | `['status', 'category_id']` | | `pageOrderBy` | array | 默认排序规则 | `['field'=>'sort','type'=>'asc']` | | `uniqueFields` | array | 唯一性校验字段 | `['username', ['code','pid']]` | | `serializeMaps` | array | 枚举显示名映射 | `['status'=>'article_status']` | | `contentFields` | array | 富文本字段 | `['content']` | | `dataScopeUserField` | string | 数据权限:归属字段 | `'create_user'` | | `dataScopeDeptField` | string | 数据权限:部门字段 | `'dept_id'` | | `tenantScopeField` | string | 租户隔离字段 | `'tenant_id'` | | `treeParentField` | string | 树形结构父级字段 | `'parent_id'` | | `treeLikeField` | string | 树形列表模糊搜索字段 | `'name'` | ### 生命周期钩子 | 钩子 | 时机 | 用途 | 可拦截 | |---|---|---|---| | `beforeAdd($data)` | 新增前 | 修改数据、预处理,**返回处理后的 data** | ✅ 抛异常 | | `afterAdd($id, $data)` | 新增后 | 保存关联表、发送通知 | ❌ | | `beforeUpdate($id, $data)` | 修改前 | 修改数据、预处理,**返回处理后的 data** | ✅ 抛异常 | | `afterUpdate($id, $data)` | 修改后 | 更新关联表 | ❌ | | `beforeDelete($id)` | 删除前 | 检查子级、业务约束 | ✅ 抛异常 | | `afterDelete($id)` | 删除后 | 清理关联数据 | ❌ | | `beforeBatchDelete($ids)` | 批量删除前 | 整体校验 | ✅ 抛异常 | | `afterBatchDelete($ids)` | 批量删除后 | 批量清理 | ❌ | | `afterDetail($id, &$data)` | 详情查询后 | 补充关联数据、格式化输出(引用传递) | ❌ | | `afterPageList(&$records, $params)` | 列表查询后 | 批量补充关联数据,分页 pageList 与全量 allList 均触发(引用传递) | ❌ | ### 新增流程(add) ```text beforeAdd($data) ← 钩子:可修改数据,抛异常可拦截 │ ▼ validateData($data, 'add') ← 自动参数校验(配置了 validateClass 时) │ ▼ array_camel_to_snake($data) ← 键名 camelCase → snake_case │ ▼ processTenantId($data) ← 自动填充 tenant_id │ ▼ processFileFieldsOnSave ← 文件字段:临时文件 → 正式目录 │ ▼ processContentFieldsOnSave ← 富文本:迁移媒体文件 │ ▼ filterTableFields($data) ← 过滤非数据库字段 │ ▼ checkUnique($data) ← 唯一性校验 │ ▼ Model::create($data) ← 写入数据库 │ ▼ afterAdd($id, $data) ← 钩子:保存关联表等副作用 ``` ### 修改流程(update) ```text beforeUpdate($id, $data) ← 钩子 │ ▼ validateData($data, 'update', $id) ← 自动参数校验 │ ▼ array_camel_to_snake → 文件 → 富文本 → 过滤 → 唯一性(排除自身) │ ▼ Model::find($id)->save($data) │ ▼ afterUpdate($id, $data) ``` ### 删除流程(delete) ```text beforeDelete($id) ← 钩子:抛异常可拦截 │ ▼ is_delete = 1 ← 软删除 │ ▼ afterDelete($id) ``` ### 查询流程(pageList / allList) ```text buildQuery($model) ← 构建查询条件(LIKE + 精确匹配 + 数据权限 + 租户隔离) │ ▼ applyOrderBy($model) ← 排序(前端传参优先,否则使用配置) │ ▼ $page / $select ← 分页 / 全量查询 │ ▼ processFileFieldsOnReadList ← 文件字段补全域名 │ ▼ processSerializeMapsList ← 枚举显示名补全 │ ▼ filterFields ← 字段过滤 │ ▼ afterPageList(&$records) ← 钩子:批量补充关联数据(引用传递) ``` ### 自动参数校验(validateClass) 配置 `$validateClass` 属性后,`add`/`update` 时自动调用对应验证器校验参数,无需在 Controller 中手动调用。 ```php class ArticleLogic extends BaseLogic { /** * 关联的模型类 * * @var string */ protected string $modelClass = Article::class; /** * 参数验证器类 * * @var string */ protected string $validateClass = \app\validate\ArticleValidate::class; // ... } ``` **工作原理:** 1. `add()` 中:`beforeAdd` → `validateData($data, 'add')` → 后续入库 2. `update()` 中:`beforeUpdate` → `validateData($data, 'update', $id)` → 后续入库 3. 验证器按场景(`add`/`update`)校验,场景不存在时使用默认规则 4. update 场景自动注入 `id` 到 data,确保 `unique` 规则排除当前记录 5. 校验失败抛出 `ValidateException`,由 `ExceptionHandle` 统一返回 **覆盖范围:** 无论是 Controller 通过 `_add`/`_edit` 调用,还是 `import` 等内部调用都会生效。 **向后兼容:** `$validateClass` 默认为空字符串,不设置则不验证。 ### 唯一性校验(uniqueFields) ```php // 全局唯一 protected array $uniqueFields = ['username', 'mobile']; // 分组内唯一:code 在同一 pid 下唯一 protected array $uniqueFields = [ 'username', // 全局唯一 ['code', 'pid'], // code 在同一 pid 下唯一 ]; ``` 校验时机:add/update 时自动校验(在 `checkUnique()` 中执行)。错误提示格式:"字段中文名 + 已存在",可通过重写 `getFieldLabel()` 自定义。 ### 查询条件构建 `buildQuery()` 按以下顺序叠加条件: 1. 额外固定条件(代码层面通过 `$where` 传入) 2. LIKE 模糊匹配(`pageLikeFields` 中配置的字段) 3. 精确匹配(`pageEqFields` 中配置的字段) 4. 数据权限过滤(`applyDataScope`) 5. 租户隔离过滤(`applyTenantScope`) ### 子类实现示例 ```php class ArticleLogic extends BaseLogic { /** * 关联的模型类 * * @var string */ protected string $modelClass = Article::class; /** * 参数验证器类 * * @var string */ protected string $validateClass = \app\validate\ArticleValidate::class; /** * 需要自动处理的文件上传字段 * * @var array */ protected array $fileFields = ['cover']; /** * 文件保存的子目录 * * @var string */ protected string $fileSaveDir = 'article'; /** * 富文本字段 * * @var array */ protected array $contentFields = ['content']; /** * LIKE 模糊匹配字段 * * @var array */ protected array $pageLikeFields = ['title']; /** * 精确匹配字段 * * @var array */ protected array $pageEqFields = ['status', 'category_id']; /** * 默认排序规则 * * @var array */ protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc']; /** * 唯一性校验字段 * * @var array */ protected array $uniqueFields = ['title']; /** * 枚举显示名映射 * * @var array */ protected array $serializeMaps = ['status' => 'article_status']; /** * 数据权限:归属字段名(用于按创建人过滤) * * @var string */ protected string $dataScopeUserField = 'create_user'; /** * 新增前处理:设置默认排序值 * * @param array $data 待新增的数据 * @return array 处理后的数据 */ protected function beforeAdd(array $data): array { $data['sort'] = $data['sort'] ?? 0; return $data; } } ``` --- ## BaseTenantLogic **文件**:`app/BaseTenantLogic.php` ### 设计说明 BaseTenantLogic 继承 BaseLogic,**仅做一件事**:将 `tenantScopeField` 设为 `'tenant_id'`,从而让所有子类自动获得租户隔离能力。 ```php abstract class BaseTenantLogic extends BaseLogic { /** * 租户隔离字段名 * * @var string */ protected string $tenantScopeField = 'tenant_id'; } ``` ### 隔离机制(由 BaseLogic 实现) | 场景 | 行为 | |---|---| | **查询时** | `applyTenantScope` 自动叠加 `WHERE tenant_id = ?` 条件 | | **新增时** | `processTenantId` 自动填充 `tenant_id` 字段 | | **超级管理员** | `tenantId = 0` 时不过滤,可跨租户查看数据 | ### 租户上下文来源 `TenantMiddleware` 解析并注入 `$request->tenantId`。 ### 使用约定 - 需要租户隔离的模块继承此基类(如 User、Article、Dept、Level 等) - 系统共享表不继承此基类,直接继承 BaseLogic(如 Menu、Role、Dict、Config 等) ### 子类示例 ```php class ArticleLogic extends BaseTenantLogic // 注意:继承 BaseTenantLogic { /** * 关联的模型类 * * @var string */ protected string $modelClass = Article::class; /** * 参数验证器类 * * @var string */ protected string $validateClass = \app\validate\ArticleValidate::class; // tenantScopeField 已由基类设为 'tenant_id',无需再声明 // ... } ``` --- ## BaseModel **文件**:`app/BaseModel.php` ### 设计说明 BaseModel 继承 ThinkPHP 的 Model,提供两项横切能力: 1. **软删除**:基于 `is_delete` 字段,不使用 ThinkPHP 内置的 `deleteTime` 机制 2. **审计字段自动写入**:`create_user` / `create_time` / `update_user` / `update_time` ### 软删除机制 ```text ┌─────────────────────────────────────────────────────────┐ │ 软删除机制 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 全局查询范围 soft_delete: │ │ 所有查询自动叠加 WHERE is_delete = 0 │ │ → 已删除数据默认不可见 │ │ │ │ 软删除操作: │ │ $model->softDelete() → UPDATE SET is_delete = 1 │ │ BaseModel::batchSoftDelete($ids) → 批量软删除 │ │ │ │ 查询已删除数据: │ │ Model::withoutGlobalScope(['soft_delete'])->... │ │ │ └─────────────────────────────────────────────────────────┘ ``` **方法列表:** | 方法 | 类型 | 说明 | |---|---|---| | `softDelete()` | 实例方法 | 设置 `is_delete = 1` | | `batchSoftDelete($ids)` | 静态方法 | 批量软删除,先关闭全局范围再 whereIn 更新 | | `scopeSoftDelete($query)` | 全局范围 | 自动过滤 `is_delete = 1` 的数据 | ### 审计字段自动写入 通过模型事件自动填充,**无需手动处理**: | 事件 | 自动填充 | 说明 | |---|---|---| | `onBeforeInsert`(创建前) | `create_user`、`create_time` | 若调用方已显式传入,则不覆盖 | | `onBeforeUpdate`(更新前) | `update_user`、`update_time` | `update_time` 始终覆盖 | 用户来源:`AuthMiddleware` 注入的 `$request->userInfo->username`。若无登录用户(如控制台任务),则跳过用户字段,时间字段仍写入。 ### 数据库字段规范 所有表建议包含以下公共字段: | 字段名 | 类型 | 说明 | |---|---|---| | `id` | int | 主键,自增 | | `is_delete` | tinyint(1) | 软删除标记,0=正常 1=已删除 | | `create_user` | varchar(50) | 创建人 | | `create_time` | datetime | 创建时间 | | `update_user` | varchar(50) | 更新人 | | `update_time` | datetime | 更新时间 | ### 使用示例 ```php // 模型定义 — 最简形式 class Article extends BaseModel { /** * 对应数据库表名(不含前缀) * * @var string */ protected $name = 'article'; } // 软删除 $article = Article::find(1); $article->softDelete(); // UPDATE article SET is_delete = 1 WHERE id = 1 // 批量软删除 Article::batchSoftDelete([1, 2, 3]); // 查询已删除数据(临时关闭全局范围) $all = Article::withoutGlobalScope(['soft_delete'])->select(); // 审计字段自动写入(无需手动处理) Article::create(['title' => '测试']); // 自动填充: create_user='admin', create_time='2026-09-22 12:00:00' ``` --- ## 基类选择指南 | 场景 | 继承 | 原因 | |---|---|---| | 系统级共享数据(菜单、字典、配置) | `BaseLogic` | 不需要租户隔离 | | 租户业务数据(用户、角色、文章、部门、职级) | `BaseTenantLogic` | 需要按租户隔离数据 | | 所有数据模型 | `BaseModel` | 获得软删除 + 审计字段 | | 所有控制器 | `BaseController` | 获得统一响应 + 通用 CRUD | :::tip 快速判断 问自己:这个模块的数据是否需要按租户隔离? - **是** → 继承 `BaseTenantLogic` - **否** → 继承 `BaseLogic` :::