Files
rxthinkcmf-wiki/RXThinkCMF_ThinkPhp6_EleVue/zh/architecture/base-classes.md
T
2026-09-29 09:49:04 +08:00

640 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
:::