21 KiB
4.6 核心基类体系
:::tip 概述 项目有四个核心基类,构成"控制器 → 逻辑层 → 模型"三层架构的骨架。所有业务模块都必须继承对应的基类,通过属性配置 + 生命周期钩子实现声明式 CRUD,子类只需声明"做什么"而不用写"怎么做"。 :::
基类继承关系
:::warning 继承规则
- 系统共享模块(Menu、Dict、Config 等)继承
BaseLogic - 租户隔离模块(User、Role、Article、Dept、Level 等)继承
BaseTenantLogic - 所有模型继承
BaseModel,所有控制器继承BaseController:::
BaseController(控制器基类)
└── 所有 Controller
BaseLogic(逻辑基类 - 系统共享模块)
├── MenuLogic
├── DictLogic
├── ConfigLogic
└── ...
│
└── BaseTenantLogic(租户逻辑基类,继承 BaseLogic)
├── UserLogic
├── RoleLogic
├── ArticleLogic
├── DeptLogic
├── LevelLogic
└── ...
BaseModel(模型基类,继承 ThinkPHP Model)
└── 所有 Model
请求处理全链路
理解基类体系的关键是理解一次请求如何流经各层:
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
方法分类
┌─────────────────────────────────────────────────────────────┐
│ 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最小为 1pageSize限制在 1~100,默认 20
前端传参示例:GET /api/article/page?pageNo=1&pageSize=10&title=测试&status=1
子类实现示例
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)
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)
beforeUpdate($id, $data) ← 钩子
│
▼
validateData($data, 'update', $id) ← 自动参数校验
│
▼
array_camel_to_snake → 文件 → 富文本 → 过滤 → 唯一性(排除自身)
│
▼
Model::find($id)->save($data)
│
▼
afterUpdate($id, $data)
删除流程(delete)
beforeDelete($id) ← 钩子:抛异常可拦截
│
▼
is_delete = 1 ← 软删除
│
▼
afterDelete($id)
查询流程(pageList / allList)
buildQuery($model) ← 构建查询条件(LIKE + 精确匹配 + 数据权限 + 租户隔离)
│
▼
applyOrderBy($model) ← 排序(前端传参优先,否则使用配置)
│
▼
$page / $select ← 分页 / 全量查询
│
▼
processFileFieldsOnReadList ← 文件字段补全域名
│
▼
processSerializeMapsList ← 枚举显示名补全
│
▼
filterFields ← 字段过滤
│
▼
afterPageList(&$records) ← 钩子:批量补充关联数据(引用传递)
自动参数校验(validateClass)
配置 $validateClass 属性后,add/update 时自动调用对应验证器校验参数,无需在 Controller 中手动调用。
class ArticleLogic extends BaseLogic
{
/**
* 关联的模型类
*
* @var string
*/
protected string $modelClass = Article::class;
/**
* 参数验证器类
*
* @var string
*/
protected string $validateClass = \app\validate\ArticleValidate::class;
// ...
}
工作原理:
add()中:beforeAdd→validateData($data, 'add')→ 后续入库update()中:beforeUpdate→validateData($data, 'update', $id)→ 后续入库- 验证器按场景(
add/update)校验,场景不存在时使用默认规则 - update 场景自动注入
id到 data,确保unique规则排除当前记录 - 校验失败抛出
ValidateException,由ExceptionHandle统一返回
覆盖范围: 无论是 Controller 通过 _add/_edit 调用,还是 import 等内部调用都会生效。
向后兼容: $validateClass 默认为空字符串,不设置则不验证。
唯一性校验(uniqueFields)
// 全局唯一
protected array $uniqueFields = ['username', 'mobile'];
// 分组内唯一:code 在同一 pid 下唯一
protected array $uniqueFields = [
'username', // 全局唯一
['code', 'pid'], // code 在同一 pid 下唯一
];
校验时机:add/update 时自动校验(在 checkUnique() 中执行)。错误提示格式:"字段中文名 + 已存在",可通过重写 getFieldLabel() 自定义。
查询条件构建
buildQuery() 按以下顺序叠加条件:
- 额外固定条件(代码层面通过
$where传入) - LIKE 模糊匹配(
pageLikeFields中配置的字段) - 精确匹配(
pageEqFields中配置的字段) - 数据权限过滤(
applyDataScope) - 租户隔离过滤(
applyTenantScope)
子类实现示例
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',从而让所有子类自动获得租户隔离能力。
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 等)
子类示例
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,提供两项横切能力:
- 软删除:基于
is_delete字段,不使用 ThinkPHP 内置的deleteTime机制 - 审计字段自动写入:
create_user/create_time/update_user/update_time
软删除机制
┌─────────────────────────────────────────────────────────┐
│ 软删除机制 │
├─────────────────────────────────────────────────────────┤
│ │
│ 全局查询范围 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 | 更新时间 |
使用示例
// 模型定义 — 最简形式
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:::