签入版本

This commit is contained in:
2026-09-29 10:14:04 +08:00
parent 3e5fc01124
commit d6b552103d
1664 changed files with 298944 additions and 0 deletions
@@ -0,0 +1,429 @@
# 4.3 后端分层架构
:::tip 概述
后端采用"三层主干 + 三层支撑"架构:Controller → Logic → Model 构成主干链路,Validate、Service、Middleware 分别提供参数校验、通用能力、请求拦截等支撑。各层职责明确,层间通过方法调用串联,不越层访问。
:::
## 架构全景
```text
┌─────────────────────────────────────────────────────────────────────┐
│ 支撑层 │
│ │
│ Validate(参数校验) Service(通用能力) Middleware(请求拦截) │
│ app/validate/*.php app/service/*.php app/middleware/*.php │
└──────────────────────────────┬──────────────────────────────────────┘
│ 被主干层调用
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 主干链路 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Controller(控制器层) │ │
│ │ 接收请求 → 获取参数 → 调用 Logic → 封装响应 │ │
│ │ 基类:BaseController | 文件:app/controller/*.php │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ 调用 Logic 方法 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Logic(逻辑层) │ │
│ │ 属性配置 → 参数校验 → 生命周期钩子 → 横切处理 → 调用 Model │ │
│ │ 基类:BaseLogic / BaseTenantLogic | 文件:app/logic/*.php │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ 调用 Model ORM │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Model(模型层) │ │
│ │ 表映射 → 软删除 → 审计字段 → ORM → SQL │ │
│ │ 基类:BaseModel | 文件:app/model/*.php │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Database(数据库) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## 各层职责详解
### Controller 层
**文件**:`app/controller/{Module}Controller.php`
**基类**:`BaseController`
**职责**:HTTP 请求入口,只做"搬运"不做"判断"
| 职责 | 说明 | 示例 |
|------|------|------|
| 参数获取 | 从请求中提取参数 | `$this->getJsonBody()`、`$this->getParams()` |
| 调用 Logic | 委托业务逻辑 | `$this->logic->add($data)` |
| 响应封装 | 统一 JSON 格式返回 | `$this->success()`、`$this->fail()` |
| 注解标注 | 声明权限和日志 | `#[Permission]`、`#[Log]` |
**Controller 不应该做:** 业务判断、数据库操作、文件处理、参数校验(特殊场景除外)。
```php
class ArticleController extends BaseController
{
protected ArticleLogic $logic;
protected function initialize(): void
{
$this->logic = new ArticleLogic();
}
// 标准 CRUD — 一行调用
#[Permission('sys:article:page', '文章分页')]
public function page(): Json
{
return parent::_index($this->logic);
}
#[Permission('sys:article:add', '添加文章')]
public function add(): Json
{
return parent::_add($this->logic, $this->getJsonBody());
}
}
```
### Logic 层
**文件**:`app/logic/{Module}Logic.php`
**基类**:`BaseLogic`(系统共享)/ `BaseTenantLogic`(租户隔离)
**职责**:业务核心,通过"属性配置 + 生命周期钩子"声明式 CRUD
| 职责 | 方式 | 说明 |
|------|------|------|
| 行为定义 | 属性配置 | `$pageLikeFields`、`$uniqueFields`、`$validateClass` 等 |
| 业务定制 | 生命周期钩子 | `beforeAdd`、`afterUpdate`、`beforeDelete` 等 |
| 自动校验 | `$validateClass` | add/update 时自动调用验证器 |
| 横切处理 | 属性驱动 | 文件字段、富文本、枚举映射、数据权限、租户隔离 |
**Logic 不应该做:** 直接 HTTP 交互、直接返回 JSON 响应。
```php
class ArticleLogic extends BaseTenantLogic
{
/**
* 关联的模型类
*
* @var string
*/
protected string $modelClass = Article::class;
/**
* 参数验证器类
*
* @var string
*/
protected string $validateClass = ArticleValidate::class;
/**
* 需要自动处理的文件上传字段
*
* @var array
*/
protected array $fileFields = ['cover'];
/**
* LIKE 模糊匹配字段
*
* @var array
*/
protected array $pageLikeFields = ['title'];
/**
* 精确匹配字段
*
* @var array
*/
protected array $pageEqFields = ['status', 'category_id'];
/**
* 唯一性校验字段
*
* @var array
*/
protected array $uniqueFields = ['title'];
/**
* 枚举显示名映射
*
* @var array
*/
protected array $serializeMaps = ['status' => 'article_status'];
/**
* 新增前处理:设置默认排序值
*
* @param array $data 待新增的数据
* @return array 处理后的数据
*/
protected function beforeAdd(array $data): array
{
$data['sort'] = $data['sort'] ?? 0;
return $data;
}
}
```
### Model 层
**文件**:`app/model/{TableName}.php`
**基类**:`BaseModel`
**职责**:数据持久化,只做表映射和自动维护
| 职责 | 说明 |
|------|------|
| 表映射 | `protected $name = 'article'` |
| 软删除 | 全局查询范围自动过滤 `is_delete=1` |
| 审计字段 | `onBeforeInsert`/`onBeforeUpdate` 自动填充 |
| 关联关系 | `hasMany`、`belongsTo` 等 |
**Model 不应该做:** 业务判断、复杂查询逻辑(交给 Logic)。
```php
class Article extends BaseModel
{
protected $name = 'article';
// 关联关系(按需定义)
public function category()
{
return $this->belongsTo(Category::class, 'category_id');
}
}
```
### Validate 层
**文件**:`app/validate/{Module}Validate.php`
**基类**:`think\Validate`
**职责**:参数格式校验,定义规则 + 场景
```php
class ArticleValidate extends Validate
{
protected $rule = [
'title' => 'require|max:200',
];
protected $message = [
'title.require' => '文章标题不能为空',
];
protected $scene = [
'add' => ['title'],
'update' => ['title'],
];
}
```
**触发方式:** Logic 配置 `$validateClass` 后,add/update 自动调用。
### Service 层
**文件**:`app/service/{Name}Service.php`
**职责**:跨模块通用能力,无状态、无副作用
| 服务 | 能力 |
|------|------|
| `DictService` | 字典查询(两级缓存) |
| `PasswordService` | 密码加密(bcrypt 双重加盐) |
| `JwtService` | Token 生成/刷新/解析 |
| `ExcelService` | Excel 导入导出 |
| `CaptchaService` | 图片验证码 |
**Service 不应该做:** 持有状态、直接操作数据库(通过 Model 静态方法例外)。
### Middleware 层
**文件**:`app/middleware/{Name}Middleware.php`
**职责**:请求拦截,在 Controller 前后执行
```text
CorsMiddleware → AuthMiddleware → TenantMiddleware → DemoMiddleware → LogMiddleware → Controller
```
| 中间件 | 作用 | 注入数据 |
|--------|------|----------|
| `AuthMiddleware` | JWT 认证 + 权限校验 | `$request->userInfo` |
| `TenantMiddleware` | 租户上下文 | `$request->tenantId` |
| `DemoMiddleware` | 演示环境拦截 | — |
| `LogMiddleware` | 操作日志记录 | — |
## 各层方法对照表
### 分页查询链路
| 层 | 方法 | 输入 | 输出 |
|----|------|------|------|
| Controller | `_index($logic)` | HTTP 请求参数 | Json 响应 |
| Logic | `pageList($params, $where, $with)` | 参数数组 | `['records'=>[], 'total'=>int, ...]` |
| Model | `Model::page($current, $size)->select()` | 分页条件 | Collection |
### 新增链路
| 层 | 方法 | 输入 | 输出 |
|----|------|------|------|
| Controller | `_add($logic, $data)` 或 `$this->logic->add($data)` | Json 请求体 | Json 响应 |
| Logic | `add($data)` | 关联数组 | `int`(新记录 ID) |
| Model | `Model::create($data)` | 关联数组 | Model 实例 |
**Logic::add 内部流程:**
`beforeAdd` → `validateData` → `camel_to_snake` → `processTenantId` → `processFile` → `processContent` → `filterTableFields` → `checkUnique` → `Model::create` → `afterAdd`
### 修改链路
| 层 | 方法 | 输入 | 输出 |
|----|------|------|------|
| Controller | `_edit($logic, $id, $data)` | id + Json 请求体 | Json 响应 |
| Logic | `update($id, $data)` | id + 关联数组 | `bool` |
| Model | `Model::find($id)->save($data)` | 模型实例 + 关联数组 | `bool` |
### 删除链路
| 层 | 方法 | 输入 | 输出 |
|----|------|------|------|
| Controller | `_remove($logic, $id)` | id | Json 响应 |
| Logic | `delete($id)` | id | `bool` |
| Model | `$model->save(['is_delete' => 1])` | 软删除标记 | `bool` |
## 数据流向
### 请求方向(前端 → 后端 → 数据库)
```text
前端请求(camelCase JSON)
│
▼
BaseController::getJsonBody() ← 获取原始参数
│
▼
BaseLogic::beforeAdd($data) ← 钩子:预处理
│
▼
BaseLogic::validateData($data) ← 自动参数校验
│
▼
BaseLogic::convertQueryParams() ← camelCase → snake_case
│
▼
BaseLogic::processFileFieldsOnSave ← 文件字段处理
│
▼
BaseLogic::filterTableFields() ← 过滤非数据库字段
│
▼
BaseLogic::checkUnique() ← 唯一性校验
│
▼
BaseModel::create() ← 审计字段自动填充
│
▼
Database(snake_case)
```
### 响应方向(数据库 → 后端 → 前端)
```text
Database(snake_case 数据)
│
▼
BaseModel ← ORM 查询
│
▼
BaseLogic 处理
│ processFileFieldsOnRead() ← 文件字段补全域名
│ processContentFieldsOnRead() ← 富文本占位符替换
│ processSerializeMaps() ← 枚举显示名补全
│ afterDetail() ← 钩子:补充关联数据
▼
Result::convertData() ← snake_case → camelCase
│
▼
JSON 响应 → 前端
```
## 异常处理传播
```text
Model 层异常(如数据库连接失败)
│
▼ 向上抛出
Logic 层异常(如 beforeDelete 抛出业务异常、validateData 抛出 ValidateException)
│
▼ 向上抛出
Controller 层
│ 方式一:try-catch 捕获,返回 fail()
│ 方式二:不捕获,由 ExceptionHandle 统一处理
▼
ExceptionHandle::render()
│ ValidateException → {"code":1, "msg":"参数验证失败", "data":"..."}
│ ModelNotFoundException → {"code":1, "msg":"数据不存在"}
│ RuntimeException(401) → {"code":1, "msg":"未授权"}
│ 其他异常 → {"code":1, "msg":"操作失败"}
▼
JSON 响应
```
**推荐做法:**
- Controller 通过 `_add`/`_edit`/`_remove` 调用时,不捕获异常,交给 ExceptionHandle
- Controller 直接调用 `$this->logic->add()` 时,try-catch 捕获并返回 `$this->fail()`
- Logic 层的业务异常(如 beforeDelete 校验)直接 `throw new \Exception`
## 完整请求示例
以"新增文章"为例,展示一个请求从进入到返回的完整链路:
```text
POST /api/article/add
Content-Type: application/json
Authorization: Bearer <token>
{"title": "新文章", "categoryId": 1, "status": 0}
```
```text
1. CorsMiddleware → 放行(设置跨域头)
2. AuthMiddleware → 校验 Token → 注入 $request->userInfo
3. TenantMiddleware → 解析租户 → 注入 $request->tenantId
4. LogMiddleware → 记录日志(#[Log] 注解)
5. ArticleController::add()
│ getJsonBody() → ["title"=>"新文章", "categoryId"=>1, "status"=>0]
│
▼
6. ArticleLogic::add($data)
│ beforeAdd($data) → 设置默认 sort=0
│ validateData($data, 'add') → 校验 title 必填 ✓
│ camel_to_snake($data) → ["title"=>"新文章", "category_id"=>1, "status"=>0]
│ processTenantId($data) → 加入 tenant_id
│ processFileFieldsOnSave → cover 无值,跳过
│ filterTableFields($data) → 过滤非表字段
│ checkUnique($data) → title 唯一 ✓
│
▼
7. Article::create($data)
│ onBeforeInsert → 自动填充 create_user, create_time
│
▼
8. Database INSERT
9. ArticleLogic::afterAdd($id, $data) → 无额外操作
10. 返回 {"code":0, "ok":true, "msg":"添加成功", "data":{"id":42}}
```
## 层间通信规范
| 规则 | 说明 |
|------|------|
| Controller → Logic | ✅ 允许,标准调用 |
| Controller → Model | ❌ 禁止,越层 |
| Logic → Model | ✅ 允许,标准调用 |
| Logic → Service | ✅ 允许,调用通用能力 |
| Logic → Controller | ❌ 禁止,反向依赖 |
| Model → Logic | ❌ 禁止,反向依赖 |
| Model → Service | ⚠️ 慎用,仅静态工具方法 |
| Middleware → Controller | ✅ 通过 `$request` 注入数据 |
| Validate → Logic | ❌ 禁止,只做格式校验 |
@@ -0,0 +1,639 @@
# 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`
:::
@@ -0,0 +1,90 @@
# 4.8 缓存架构设计
:::tip 概述
项目采用两级缓存架构:请求级缓存(内存)+ 持久缓存(file/Redis),大幅减少数据库查询。
:::
## 两级缓存架构
```text
┌─────────────────────────────────────────────────────────┐
│ 第一级:请求级缓存(static 变量) │
│ │
│ 生命周期:单次 HTTP 请求 │
│ 存储位置:PHP 内存(static $cache) │
│ 速度:极快(纳秒级) │
│ │
│ 同一请求内多次查询同一数据,只查一次库或缓存 │
└──────────────────────────┬──────────────────────────────┘
│ 未命中
▼
┌─────────────────────────────────────────────────────────┐
│ 第二级:持久缓存(cache() 门面) │
│ │
│ 生命周期:跨请求复用 │
│ 存储位置:file 或 Redis │
│ 速度:快(毫秒级) │
│ 有效期:1 小时(3600 秒) │
│ │
│ 跨请求复用,减少数据库压力 │
└──────────────────────────┬──────────────────────────────┘
│ 未命中
▼
┌─────────────────────────────────────────────────────────┐
│ 数据库查询 │
│ │
│ 查询后同时写入两级缓存 │
└─────────────────────────────────────────────────────────┘
```
## 使用两级缓存的服务
| 服务 | 缓存键格式 | 说明 |
|------|-----------|------|
| `DictService` | `dict_{code}` | 字典项映射 |
| `DictService` | `dict_full_{code}` | 字典项完整数据 |
| `ParamService` | `param_{code}` | 系统参数值 |
## 缓存驱动配置
`.env` 配置:
```ini
; 文件缓存(默认)
[CACHE]
DRIVER = file
; Redis 缓存(推荐生产环境)
[CACHE]
DRIVER = redis
REDIS_HOST = 127.0.0.1
REDIS_PORT = 6379
REDIS_PASSWORD =
REDIS_DB = 0
```
:::warning 生产环境建议使用 Redis
文件缓存在多进程/多服务器环境下无法共享,生产环境建议配置 Redis 缓存驱动。使用 Redis 前需安装 `php redis` 扩展。
:::
## 缓存清除
```php
// 清除指定字典缓存
DictService::clearCache('gender');
// 清除所有字典缓存
DictService::clearAllCache();
// 清除指定参数缓存
ParamService::clearCache('DEFAULT_PASSWORD');
// 清除所有参数缓存
ParamService::clearAllCache();
```
对应接口:
```text
GET /api/dict/refreshCache 刷新字典缓存
```
@@ -0,0 +1,536 @@
# 4.7 横切机制
:::tip 概述
横切机制是 BaseLogic/BaseModel 内置的自动处理能力,开发者通过属性配置启用,无需手动编写处理代码。项目共有 9 种横切机制,覆盖数据转换、文件处理、校验、权限、隔离等维度。
:::
## 横切机制总览
| 机制 | 配置位置 | 配置属性 | 触发时机 |
|------|---------|---------|---------|
| 驼峰/下划线转换 | `config/api.php` | `camel_snake_convert` | 请求入、响应出 |
| 文件字段处理 | Logic | `fileFields` + `fileSaveDir` | add/update 写入,detail/page 读取 |
| 多文件字段处理 | Logic | `multiFileFields` + `fileSaveDir` | add/update 写入,detail/page 读取 |
| 富文本占位符 | Logic | `contentFields` | add/update 写入,detail/page 读取 |
| 枚举显示名翻译 | Logic | `serializeMaps` | detail/page 查询后 |
| 自动参数校验 | Logic | `validateClass` | add/update 前 |
| 唯一性校验 | Logic | `uniqueFields` | add/update 时 |
| 数据权限过滤 | Logic | `dataScopeUserField` / `dataScopeDeptField` | 查询时 |
| 租户隔离 | Logic(继承 BaseTenantLogic) | `tenantScopeField` | 查询时 + 新增时 |
| 审计字段 + 软删除 | BaseModel | 自动生效 | 所有 CRUD |
## 1. 驼峰/下划线自动转换
前端使用 camelCase,数据库使用 snake_case,框架自动完成双向转换。
### 转换流程
```text
请求方向(前端 → 数据库):
前端传参:{ userName: "admin", createTime: "2026-01-01" }
│
▼ BaseLogic::convertQueryParams()
│ array_camel_to_snake($params, $except)
▼
数据库操作:WHERE user_name = 'admin' AND create_time = '2026-01-01'
响应方向(数据库 → 前端):
数据库数据:{ user_name: "admin", create_time: "2026-01-01" }
│
▼ Result::convertData()
│ array_snake_to_camel($data)
▼
前端接收:{ userName: "admin", createTime: "2026-01-01" }
```
### 排除项(不参与转换)
以下参数保持 camelCase,不转为 snake_case:
- `pageNo`、`pageSize` — 分页参数
- `orderField`、`orderType` — 排序参数
- `fields` — 字段过滤参数
### 配置开关
```php
// config/api.php
'camel_snake_convert' => env('api.camel_snake_convert', true),
```
```ini
# .env
API.CAMEL_SNAKE_CONVERT = true # 开启(默认)
API.CAMEL_SNAKE_CONVERT = false # 关闭(请求保持驼峰,响应保持下划线)
```
:::tip 开发者无需手动处理
驼峰/下划线转换完全自动,开发者只需在前端用 camelCase,在数据库用 snake_case,框架自动处理双向转换。
:::
## 2. 文件字段自动处理
**配置方式**:Logic 中设置 `fileFields` 和 `fileSaveDir`
```php
class ArticleLogic extends BaseTenantLogic
{
protected array $fileFields = ['cover', 'attachment'];
protected string $fileSaveDir = 'article';
}
```
### 写入时(add/update)
```text
前端传入:cover = "https://temp.example.com/tmp/abc123.jpg"
│
▼ processFileFieldsOnSave()
│ 1. 检测字段值是否为临时文件 URL
│ 2. 调用 save_file() 将临时文件迁移到正式目录
│ 3. 去掉域名,只存储相对路径
▼
入库存储:cover = "/uploads/article/20260922/abc123.jpg"
```
### 读取时(detail/pageList/allList)
```text
数据库读取:cover = "/uploads/article/20260922/abc123.jpg"
│
▼ processFileFieldsOnRead()
│ 调用 get_file_url() 补全域名
▼
前端接收:cover = "https://cdn.example.com/uploads/article/20260922/abc123.jpg"
```
### 多个单文件字段
```php
protected array $fileFields = ['cover', 'avatar', 'attachment'];
// 三个字段都会自动处理
```
## 3. 多文件字段自动处理
当一个字段存储多个文件URL(逗号分隔)时,使用 `multiFileFields`:
**配置方式**:Logic 中设置 `multiFileFields`(复用 `fileSaveDir`)
```php
class ArticleLogic extends BaseTenantLogic
{
protected array $multiFileFields = ['images', 'attachments'];
protected string $fileSaveDir = 'article';
}
```
**写入时(add/update)**
```text
前端传入:images = "http://example.com/temp/a.png,http://example.com/temp/b.png"
│
▼ processMultiFileFieldsOnSave()
│ 1. 按逗号拆分为数组
│ 2. 逐个调用 save_file() 迁移临时文件并去掉域名
│ 3. 重新用逗号拼接
▼
入库存储:images = "article/20260922/a.png,article/20260922/b.png"
```
**读取时(detail/pageList/allList)**
```text
数据库读取:images = "article/20260922/a.png,article/20260922/b.png"
│
▼ processMultiFileFieldsOnRead()
│ 1. 按逗号拆分为数组
│ 2. 逐个调用 get_file_url() 补全域名
│ 3. 重新用逗号拼接
▼
前端接收:images = "http://cdn.example.com/article/20260922/a.png,http://cdn.example.com/article/20260922/b.png"
```
## 4. 富文本占位符机制
**配置方式**:Logic 中设置 `contentFields`
```php
class ArticleLogic extends BaseTenantLogic
{
protected array $contentFields = ['content'];
}
```
### 写入时
```text
前端传入:content = '<p>文章内容</p><img src="https://temp.example.com/tmp/img001.jpg">'
│
▼ processContentFieldsOnSave()
│ 1. 解析 HTML 中的 <img>/<video>/<a> 标签
│ 2. 将临时文件迁移到正式目录
│ 3. 替换为 [IMG_URL]/相对路径 占位符
▼
入库存储:content = '<p>文章内容</p><img src="[IMG_URL]/uploads/article/img001.jpg">'
```
### 读取时
```text
数据库读取:content = '<p>文章内容</p><img src="[IMG_URL]/uploads/article/img001.jpg">'
│
▼ processContentFieldsOnRead()
│ 将 [IMG_URL] 替换为配置的实际域名
▼
前端接收:content = '<p>文章内容</p><img src="https://cdn.example.com/uploads/article/img001.jpg">'
```
### 好处
域名变更时只需修改 `config/file.php` 中的 `domain_url`,已存储的内容无需逐条更新。
## 5. 枚举显示名翻译
**配置方式**:Logic 中设置 `serializeMaps`
```php
class ArticleLogic extends BaseTenantLogic
{
/**
* 枚举显示名映射
*
* @var array
*/
protected array $serializeMaps = [
'status' => 'article_status',
'type' => 'article_type',
];
}
```
### 工作原理
```text
数据库查询:{ status: 1, type: 0 }
│
▼ processSerializeMaps()
│ DictService::getText('article_status', 1) → '已发布'
│ DictService::getText('article_type', 0) → '原创'
▼
前端接收:{ status: 1, statusText: '已发布', type: 0, typeText: '原创' }
```
### 字段名映射
当字典编码与字段名不同时,可指定映射:
```php
/**
* 枚举显示名映射
*
* @var array
*/
protected array $serializeMaps = [
'gender' => 'gender',
'status' => 'user_status',
];
```
## 6. 自动参数校验
**配置方式**:Logic 中设置 `validateClass`
```php
class ArticleLogic extends BaseTenantLogic
{
protected string $validateClass = \app\validate\ArticleValidate::class;
}
```
### 工作原理
```text
add() 流程:
beforeAdd($data) → validateData($data, 'add') → 后续入库
update() 流程:
beforeUpdate($id, $data) → validateData($data, 'update', $id) → 后续入库
```
### 验证器示例
```php
class ArticleValidate extends Validate
{
protected $rule = [
'title' => 'require|max:200',
];
protected $message = [
'title.require' => '文章标题不能为空',
];
protected $scene = [
'add' => ['title'],
'update' => ['title'],
];
}
```
校验失败抛出 `ValidateException`,由 `ExceptionHandle` 统一返回 `{"code":1, "msg":"参数验证失败", "data":"文章标题不能为空"}`。
### 覆盖范围
无论是 Controller 通过 `_add`/`_edit` 调用,还是 `import` 等内部调用都会触发校验。
## 7. 唯一性校验
**配置方式**:Logic 中设置 `uniqueFields`
```php
class UserLogic extends BaseTenantLogic
{
// 全局唯一
protected array $uniqueFields = ['username', 'mobile'];
}
```
### 两种规则
```php
// 字符串 — 全局唯一
protected array $uniqueFields = ['username', 'mobile'];
// 数组 — 分组内唯一
protected array $uniqueFields = [
'username', // username 全局唯一
['code', 'pid'], // code 在同一 pid 下唯一
];
```
### 工作原理
```text
add() 时:
checkUnique($data)
→ SELECT * FROM think_user WHERE username = 'admin'
→ 存在 → throw new \Exception('用户名已存在')
update() 时:
checkUnique($data, $excludeId)
→ SELECT * FROM think_user WHERE username = 'admin' AND id <> 42
→ 存在 → throw new \Exception('用户名已存在')
```
### 错误提示自定义
默认提示格式:"字段中文名 + 已存在"。可通过重写 `getFieldLabel()` 自定义:
```php
protected function getFieldLabel(string $field): string
{
$map = [
'username' => '用户名',
'mobile' => '手机号',
'code' => '编码',
];
return $map[$field] ?? $field;
}
```
## 8. 数据权限过滤
**配置方式**:Logic 中设置 `dataScopeUserField` 和/或 `dataScopeDeptField`
```php
class ArticleLogic extends BaseTenantLogic
{
/**
* 数据权限:归属字段名(用于按创建人过滤)
*
* @var string
*/
protected string $dataScopeUserField = 'create_user';
/**
* 数据权限:部门关联字段名(用于按部门过滤)
*
* @var string
*/
protected string $dataScopeDeptField = 'dept_id';
}
```
### 三种权限范围
由角色表 `think_role` 的 `data_scope` 字段决定:
```text
┌─────────────────────────────────────────────────────────┐
│ data_scope = 1 → 全部数据(不过滤) │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ A │ │ B │ │ C │ │ D │ │ E │ ← 所有部门所有人的数据 │
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
├─────────────────────────────────────────────────────────┤
│ data_scope = 2 → 本部门数据 │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ A │ │ B │ │███│ │ D │ │ E │ ← 只看到本部门数据 │
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
├─────────────────────────────────────────────────────────┤
│ data_scope = 3 → 仅本人数据 │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ A │ │ B │ │███│ │ D │ │ E │ ← 只看到自己创建的数据 │
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
└─────────────────────────────────────────────────────────┘
```
### 工作原理
```text
查询时 applyDataScope():
1. 获取当前用户角色 ID
2. 查询角色的 data_scope
3. scope=1 → 不过滤
4. scope=2 → WHERE dept_id = 当前用户部门ID
5. scope=3 → WHERE create_user = 当前用户名
```
:::warning 无角色用户
无角色时默认按"仅本人"(scope=3)处理。
:::
## 9. 租户隔离
**配置方式**:继承 `BaseTenantLogic`(自动设置 `tenantScopeField = 'tenant_id'`)
```php
class ArticleLogic extends BaseTenantLogic // 继承 BaseTenantLogic 即启用
{
// tenantScopeField 已由基类设为 'tenant_id',无需再声明
}
```
### 隔离行为
| 场景 | 行为 | 实现方法 |
|------|------|---------|
| **查询时** | 自动叠加 `WHERE tenant_id = 当前租户ID` | `applyTenantScope()` |
| **新增时** | 自动填充 `tenant_id` 字段 | `processTenantId()` |
| **超级管理员**(tenantId=0) | 查询不过滤,可跨租户查看 | 条件判断跳过 |
| **超级管理员新增** | 自动代入默认租户(tenant_id=1) | 读取配置 `tenant.default_tenant_id` |
### 租户上下文来源
`TenantMiddleware` 解析 JWT 中的租户信息,注入 `$request->tenantId`。
### 适用范围
- 需要租户隔离的模块:继承 `BaseTenantLogic`(User、Article、Dept、Level 等)
- 系统共享数据:继承 `BaseLogic`(Menu、Role、Dict、Config 等)
## 10. 审计字段 + 软删除(BaseModel)
**配置方式**:继承 `BaseModel` 即自动生效,无需额外配置。
### 软删除
```text
全局查询范围 soft_delete:
所有查询自动叠加 WHERE is_delete = 0
→ 已删除数据默认不可见
软删除操作:
$model->softDelete() → UPDATE SET is_delete = 1
BaseModel::batchSoftDelete($ids) → 批量软删除
查询已删除数据:
Model::withoutGlobalScope(['soft_delete'])->...
```
### 审计字段自动写入
| 事件 | 自动填充字段 | 数据来源 |
|------|------------|---------|
| `onBeforeInsert`(创建前) | `create_user` | `$request->userInfo->username` |
| `onBeforeInsert`(创建前) | `create_time` | `date('Y-m-d H:i:s')` |
| `onBeforeUpdate`(更新前) | `update_user` | `$request->userInfo->username` |
| `onBeforeUpdate`(更新前) | `update_time` | `date('Y-m-d H:i:s')`(始终覆盖) |
:::tip 无用户场景
控制台任务、定时任务等无登录用户的场景下,跳过用户字段,时间字段仍写入。
:::
### 数据库公共字段
所有业务表**必须**包含以下字段:
```sql
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
is_delete TINYINT(1) DEFAULT 0 COMMENT '软删除 0=正常 1=已删除',
create_user VARCHAR(50) DEFAULT '' COMMENT '创建人',
create_time DATETIME COMMENT '创建时间',
update_user VARCHAR(50) DEFAULT '' COMMENT '更新人',
update_time DATETIME COMMENT '更新时间'
```
## 横切机制的组合使用
一个模块可以同时启用多种横切机制,它们互不干扰:
```php
class ArticleLogic extends BaseTenantLogic
{
protected string $modelClass = Article::class;
// 自动参数校验
protected string $validateClass = ArticleValidate::class;
// 文件字段处理
protected array $fileFields = ['cover'];
protected string $fileSaveDir = 'article';
// 多文件字段处理
protected array $multiFileFields = ['images'];
// 富文本占位符
protected array $contentFields = ['content'];
// 枚举显示名翻译
protected array $serializeMaps = ['status' => 'article_status'];
// 唯一性校验
protected array $uniqueFields = ['title'];
// 数据权限
protected string $dataScopeUserField = 'create_user';
// 租户隔离(继承 BaseTenantLogic 自动启用)
// 审计字段 + 软删除(继承 BaseModel 自动生效)
}
```
执行顺序(add 流程):
```text
beforeAdd ← 钩子
▼
validateData ← 自动参数校验
▼
camel_to_snake ← 驼峰转下划线
▼
processTenantId ← 租户ID填充
▼
processFileFields ← 文件字段处理(单值)
▼
processMultiFileFields ← 多文件字段处理(逗号分隔多值)
▼
processContentFields ← 富文本处理
▼
filterTableFields ← 过滤非数据库字段
▼
checkUnique ← 唯一性校验
▼
Model::create ← 审计字段自动填充
▼
afterAdd ← 钩子
```
@@ -0,0 +1,749 @@
# 4.9 数据库设计
:::tip 概述
项目支持 MySQL、PostgreSQL、SQL Server、Oracle、SQLite 五种数据库,通过 `.env` 的 `DATABASE.TYPE` 一键切换。所有数据库共用同一套代码,无需修改业务逻辑。本章涵盖命名规范、表结构设计、字段说明、索引策略和多数据库适配。
:::
## 命名规范
| 对象 | 规范 | 示例 |
|------|------|------|
| 表名 | `think_` 前缀 + 小写下划线 | `think_user`、`think_article` |
| 字段名 | 小写下划线 | `create_time`、`user_name` |
| 主键 | 统一 `id`,INT 自增 | `id INT PRIMARY KEY` |
| 外键 | `表名_id` | `user_id`、`role_id` |
| 索引 | `idx_字段名` | `idx_status`、`idx_tenant_id` |
| 唯一索引 | `uk_字段名` | `uk_username` |
## 公共字段
所有业务表**必须**包含以下公共字段,由 `BaseModel` 自动维护:
```sql
-- MySQL 格式
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
is_delete TINYINT(1) DEFAULT 0 COMMENT '软删除 0=正常 1=已删除',
create_user VARCHAR(50) DEFAULT '' COMMENT '创建人',
create_time DATETIME COMMENT '创建时间',
update_user VARCHAR(50) DEFAULT '' COMMENT '更新人',
update_time DATETIME COMMENT '更新时间'
```
| 字段 | 自动维护方 | 说明 |
|------|-----------|------|
| `is_delete` | BaseModel 软删除机制 | `softDelete()` 设置为 1,全局查询范围自动过滤 |
| `create_user` | BaseModel `onBeforeInsert` | 取当前登录用户名,无用户时留空 |
| `create_time` | BaseModel `onBeforeInsert` | 取当前时间,已传入则不覆盖 |
| `update_user` | BaseModel `onBeforeUpdate` | 取当前登录用户名 |
| `update_time` | BaseModel `onBeforeUpdate` | 始终覆盖为当前时间 |
## 核心表清单
### 用户与权限(6 张表)
| 表名 | 说明 | Logic 基类 |
|------|------|-----------|
| `think_user` | 用户表 | BaseTenantLogic |
| `think_user_role` | 用户角色关联表(N:N 中间表) | — |
| `think_role` | 角色表 | BaseLogic |
| `think_role_menu` | 角色菜单关联表(N:N 中间表) | — |
| `think_menu` | 菜单/权限表 | BaseLogic |
| `think_dept` | 部门表(树形) | BaseTenantLogic |
**关联关系:**
```text
think_user ──┐
│ N:N (think_user_role)
think_role ──┤
│ N:N (think_role_menu)
think_menu ──┘
think_dept ←── think_user.dept_id
think_tenant ←── think_user.tenant_id
```
### 组织架构(3 张表)
| 表名 | 说明 | Logic 基类 |
|------|------|-----------|
| `think_dept` | 部门表(树形,parent_id) | BaseTenantLogic |
| `think_position` | 岗位表 | BaseTenantLogic |
| `think_level` | 职级表 | BaseTenantLogic |
### 租户(1 张表)
| 表名 | 说明 | Logic 基类 |
|------|------|-----------|
| `think_tenant` | 租户表 | BaseLogic |
### 系统配置(5 张表)
| 表名 | 说明 | Logic 基类 |
|------|------|-----------|
| `think_dict` | 字典表 | BaseLogic |
| `think_dict_item` | 字典项表 | BaseLogic |
| `think_config` | 配置组表 | BaseLogic |
| `think_config_item` | 配置项表 | BaseLogic |
| `think_param` | 系统参数表 | BaseLogic |
**关联关系:**
```text
think_dict ←── think_dict_item.dict_id
think_config ←── think_config_item.config_id
```
### 内容管理(4 张表)
| 表名 | 说明 | Logic 基类 |
|------|------|-----------|
| `think_article` | 文章表 | BaseTenantLogic |
| `think_category` | 文章分类表(树形) | BaseTenantLogic |
| `think_link` | 友情链接表 | BaseTenantLogic |
| `think_notice` | 通知公告表 | BaseTenantLogic |
### 日志审计(2 张表)
| 表名 | 说明 | Logic 基类 |
|------|------|-----------|
| `think_login_log` | 登录日志表 | BaseLogic |
| `think_operation_log` | 操作日志表 | BaseLogic |
### 定时任务(2 张表)
| 表名 | 说明 | Logic 基类 |
|------|------|-----------|
| `think_job` | 定时任务表 | BaseLogic |
| `think_job_log` | 定时任务执行日志表 | BaseLogic |
**关联关系:**
```text
think_job ←── think_job_log.job_id
```
### 基础数据(2 张表)
| 表名 | 说明 | Logic 基类 |
|------|------|-----------|
| `think_city` | 城市行政区划表(树形,pid) | BaseLogic |
| `think_file_template` | 文件模板表 | BaseLogic |
**共计 25 张业务表**(不含中间表)。
## 关键表字段说明
### think_user(用户表)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INT | 主键 |
| `username` | VARCHAR(50) | 用户名(登录账号) |
| `password` | VARCHAR(255) | 密码(bcrypt 加密) |
| `salt` | VARCHAR(50) | 密码盐值 |
| `realname` | VARCHAR(50) | 姓名 |
| `gender` | TINYINT | 性别(字典 gender) |
| `mobile` | VARCHAR(20) | 手机号 |
| `email` | VARCHAR(100) | 邮箱 |
| `avatar` | VARCHAR(500) | 头像路径 |
| `status` | TINYINT | 状态(字典 user_status) |
| `dept_id` | INT | 部门 ID |
| `level_id` | INT | 职级 ID |
| `position_id` | INT | 岗位 ID |
| `tenant_id` | INT | 租户 ID |
| `province_code` | VARCHAR(20) | 省编码 |
| `city_code` | VARCHAR(20) | 市编码 |
| `district_code` | VARCHAR(20) | 区编码 |
| `street_code` | VARCHAR(20) | 街道编码 |
| `city_info` | VARCHAR(200) | 城市名称拼接 |
| `is_delete` | TINYINT(1) | 软删除 |
| `create_user` | VARCHAR(50) | 创建人 |
| `create_time` | DATETIME | 创建时间 |
| `update_user` | VARCHAR(50) | 更新人 |
| `update_time` | DATETIME | 更新时间 |
### think_role(角色表)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INT | 主键 |
| `name` | VARCHAR(50) | 角色名称 |
| `code` | VARCHAR(50) | 角色编码 |
| `data_scope` | TINYINT | 数据权限范围:1=全部 2=本部门 3=仅本人 |
| `status` | TINYINT | 状态 |
| `remark` | VARCHAR(500) | 备注 |
| `sort` | INT | 排序 |
| `...` | | 公共字段 |
### think_menu(菜单/权限表)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INT | 主键 |
| `parent_id` | INT | 父级 ID(0=顶级) |
| `name` | VARCHAR(50) | 菜单名称 |
| `permission` | VARCHAR(100) | 权限标识(如 `sys:user:add`) |
| `type` | TINYINT | 类型:0=目录 1=菜单 2=按钮 |
| `path` | VARCHAR(200) | 路由路径 |
| `icon` | VARCHAR(100) | 图标 |
| `sort` | INT | 排序 |
| `status` | TINYINT | 状态 |
| `remark` | VARCHAR(500) | 备注 |
| `...` | | 公共字段 |
### think_tenant(租户表)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INT | 主键 |
| `name` | VARCHAR(100) | 租户名称 |
| `code` | VARCHAR(50) | 租户编码(系统自动生成:T+4位ID) |
| `status` | TINYINT | 状态(字典 tenant_status):0=禁用 1=启用 |
| `expire_time` | DATETIME | 过期时间 |
| `max_user_num` | INT | 最大用户数限制 |
| `contact_name` | VARCHAR(50) | 联系人 |
| `contact_phone` | VARCHAR(20) | 联系电话 |
| `remark` | VARCHAR(500) | 备注 |
| `...` | | 公共字段 |
### think_article(文章表)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INT | 主键 |
| `title` | VARCHAR(200) | 标题 |
| `category_id` | INT | 分类 ID |
| `cover` | VARCHAR(500) | 封面图路径 |
| `content` | TEXT | 内容(富文本,含 [IMG_URL] 占位符) |
| `author` | VARCHAR(50) | 作者 |
| `status` | TINYINT | 状态(字典 article_status):0=正常 1=下架 |
| `sort` | INT | 排序 |
| `tenant_id` | INT | 租户 ID |
| `...` | | 公共字段 |
## 索引策略
### 推荐索引
```sql
-- 用户表:高频查询字段
ALTER TABLE think_user ADD INDEX idx_username (username);
ALTER TABLE think_user ADD INDEX idx_tenant_id (tenant_id);
ALTER TABLE think_user ADD INDEX idx_dept_id (dept_id);
ALTER TABLE think_user ADD INDEX idx_status (status);
-- 角色关联表:外键索引
ALTER TABLE think_user_role ADD INDEX idx_user_id (user_id);
ALTER TABLE think_user_role ADD INDEX idx_role_id (role_id);
ALTER TABLE think_role_menu ADD INDEX idx_role_id (role_id);
ALTER TABLE think_role_menu ADD INDEX idx_menu_id (menu_id);
-- 文章表:常用过滤字段
ALTER TABLE think_article ADD INDEX idx_category_id (category_id);
ALTER TABLE think_article ADD INDEX idx_status (status);
ALTER TABLE think_article ADD INDEX idx_tenant_id (tenant_id);
-- 日志表:按时间查询
ALTER TABLE think_login_log ADD INDEX idx_create_time (create_time);
ALTER TABLE think_operation_log ADD INDEX idx_create_time (create_time);
ALTER TABLE think_job_log ADD INDEX idx_job_id (job_id);
-- 菜单表:树形查询
ALTER TABLE think_menu ADD INDEX idx_parent_id (parent_id);
-- 字典项表:按字典 ID 查询
ALTER TABLE think_dict_item ADD INDEX idx_dict_id (dict_id);
ALTER TABLE think_config_item ADD INDEX idx_config_id (config_id);
-- 城市表:按编码查询
ALTER TABLE think_city ADD INDEX idx_area_code (area_code);
ALTER TABLE think_city ADD INDEX idx_parent_code (parent_code);
ALTER TABLE think_city ADD INDEX idx_pid (pid);
```
### 索引设计原则
| 原则 | 说明 |
|------|------|
| 外键必加 | 所有 `_id` 外键字段都应加索引 |
| 过滤字段 | `pageEqFields` 中配置的精确匹配字段应加索引 |
| 模糊搜索 | `pageLikeFields` 中的字段可考虑全文索引,但 LIKE '%xx%' 无法用普通索引 |
| 排序字段 | `pageOrderBy` 中的默认排序字段可加索引 |
| 时间字段 | 日志表的 `create_time` 必加索引(按时间范围查询) |
## 多数据库支持
### 支持的数据库
| 数据库 | type 值 | 默认端口 | 默认用户 | PHP 扩展 |
|--------|---------|----------|----------|----------|
| MySQL | `mysql` | 3306 | root | `pdo_mysql` |
| PostgreSQL | `pgsql` | 5432 | postgres | `pdo_pgsql` |
| SQL Server | `sqlsrv` | 1433 | sa | `pdo_sqlsrv` |
| Oracle | `oracle` | 1521 | - | `pdo_oci` |
| SQLite | `sqlite` | - | - | `pdo_sqlite` |
### 切换数据库
只需修改 `.env` 文件,无需改动任何代码:
::: code-group
```ini [MySQL(默认)]
[DATABASE]
TYPE = mysql
HOSTNAME = 127.0.0.1
DATABASE = rxthinkcmf.thinkphp6.elevue
USERNAME = root
PASSWORD = root
HOSTPORT = 3306
```
```ini [PostgreSQL]
[DATABASE]
TYPE = pgsql
HOSTNAME = 127.0.0.1
DATABASE = rxthinkcmf.thinkphp6.elevue
USERNAME = postgres
PASSWORD = root
HOSTPORT = 5432
```
```ini [SQL Server]
[DATABASE]
TYPE = sqlsrv
HOSTNAME = 127.0.0.1
DATABASE = rxthinkcmf.thinkphp6.elevue
USERNAME = sa
PASSWORD = root
HOSTPORT = 1433
```
```ini [Oracle]
[DATABASE]
TYPE = oracle
HOSTNAME = 127.0.0.1
DATABASE = rxthinkcmf.thinkphp6.elevue
USERNAME = root
PASSWORD = root
HOSTPORT = 1521
```
```ini [SQLite]
[DATABASE]
TYPE = sqlite
DATABASE = /www/api/runtime/database.sqlite
```
:::
### 配置文件结构
```php
// config/database.php
return [
'default' => env('database.type', 'mysql'),
'connections' => [
'mysql' => [...],
'pgsql' => [...],
'sqlsrv' => [...],
'oracle' => [...],
'sqlite' => [...],
],
];
```
### 字段类型映射
| 通用类型 | MySQL | PostgreSQL | SQL Server | Oracle | SQLite |
|---------|-------|------------|------------|--------|--------|
| 自增主键 | `INT AUTO_INCREMENT` | `SERIAL` | `INT IDENTITY` | `NUMBER` + 序列 | `INTEGER PRIMARY KEY` |
| 字符串 | `VARCHAR(n)` | `VARCHAR(n)` | `NVARCHAR(n)` | `VARCHAR2(n)` | `TEXT` |
| 文本 | `TEXT` | `TEXT` | `NVARCHAR(MAX)` | `CLOB` | `TEXT` |
| 布尔 | `TINYINT(1)` | `BOOLEAN` | `BIT` | `NUMBER(1)` | `INTEGER` |
| 日期时间 | `DATETIME` | `TIMESTAMP` | `DATETIME2` | `TIMESTAMP` | `TEXT` |
| 小数 | `DECIMAL(m,n)` | `NUMERIC(m,n)` | `DECIMAL(m,n)` | `NUMBER(m,n)` | `REAL` |
:::tip 业务代码零修改
ThinkPHP 的 ORM 层会自动处理大部分 SQL 差异,切换数据库只需修改 `.env` 中的 `DATABASE.TYPE`,业务代码无需任何修改。
:::
### 各数据库安装与配置
项目支持五种数据库,按需选择一种即可。
| 数据库 | 推荐场景 | 难度 | XAMPP 自带 | 说明 |
|--------|---------|------|-----------|------|
| **MySQL** | 日常开发、中小项目 | ⭐ 低 | ✅ 是 | 默认推荐,开箱即用 |
| **PostgreSQL** | 需要 JSONB、全文检索 | ⭐⭐ 中 | ❌ 否 | 功能丰富,需单独安装 |
| **SQLite** | 快速演示、单元测试 | ⭐ 低 | ✅ 是 | 无需数据库服务,零配置 |
| **SQL Server** | 企业 Windows 环境 | ⭐⭐ 中 | 驱动自带 | 需单独安装数据库服务 |
| **Oracle** | 大型企业项目 | ⭐⭐⭐ 高 | 驱动自带 | 需安装 Instant Client |
::: tip 新手推荐
XAMPP 已集成 MySQL 数据库服务,安装 XAMPP 后无需再单独安装数据库。
:::
#### MySQL 数据库
MySQL 是项目默认推荐的关系型数据库,XAMPP 已自带,开箱即用。
::: code-group
```sh [Windows]
# XAMPP 已自带 MySQL,无需额外安装
# 在 XAMPP 控制面板启动 MySQL 即可
# 确认扩展已启用(编辑 D:\xampp8.2\php\php.ini):
# extension=pdo_mysql
# 若前面有分号则去掉,保存后重启 XAMPP
# 创建数据库
D:\xampp8.2\mysql\bin\mysql.exe -u root -proot -e "CREATE DATABASE `rxthinkcmf.thinkphp6.elevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
```
```sh [macOS]
brew install mysql
brew services start mysql
sudo mysql_secure_installation
# 创建数据库
mysql -u root -proot -e "CREATE DATABASE `rxthinkcmf.thinkphp6.elevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
```
```sh [Ubuntu / Debian]
sudo apt install mysql-server
sudo systemctl start mysql
sudo systemctl enable mysql
sudo mysql_secure_installation
# 创建数据库
mysql -u root -proot -e "CREATE DATABASE `rxthinkcmf.thinkphp6.elevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
```
```sh [Rocky Linux / CentOS]
sudo dnf install mysql-server
sudo systemctl start mysqld
sudo systemctl enable mysqld
sudo mysql_secure_installation
# 创建数据库
mysql -u root -proot -e "CREATE DATABASE `rxthinkcmf.thinkphp6.elevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
```
:::
#### PostgreSQL 数据库
PostgreSQL 功能强大,支持 JSONB、全文检索等高级特性,需单独安装。
::: code-group
```sh [Windows]
# 安装 PostgreSQL
# 访问 https://www.postgresql.org/download/windows/ 下载安装包
# 安装过程中记住设置的密码
# 确认扩展已启用(编辑 D:\xampp8.2\php\php.ini):
# extension=pdo_pgsql
# extension=pgsql
# 若前面有分号则去掉,保存后重启 XAMPP
# 设置 postgres 密码(首次)
psql -U postgres -c "ALTER USER postgres WITH PASSWORD 'root';"
# 创建数据库
psql -U postgres -c "CREATE DATABASE \"rxthinkcmf.thinkphp6.elevue\" OWNER postgres ENCODING 'UTF8';"
```
```sh [macOS]
brew install postgresql
brew services start postgresql
# 设置 postgres 密码
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'root';"
# 创建数据库
sudo -u postgres psql -c "CREATE DATABASE \"rxthinkcmf.thinkphp6.elevue\" OWNER postgres ENCODING 'UTF8';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE \"rxthinkcmf.thinkphp6.elevue\" TO postgres;"
```
```sh [Ubuntu / Debian]
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql
# 设置 postgres 密码
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'root';"
# 创建数据库
sudo -u postgres psql -c "CREATE DATABASE \"rxthinkcmf.thinkphp6.elevue\" OWNER postgres ENCODING 'UTF8';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE \"rxthinkcmf.thinkphp6.elevue\" TO postgres;"
```
```sh [Rocky Linux / CentOS]
sudo dnf install postgresql-server
sudo postgresql-setup --initdb
sudo systemctl start postgresql
# 设置 postgres 密码
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'root';"
# 创建数据库
sudo -u postgres psql -c "CREATE DATABASE \"rxthinkcmf.thinkphp6.elevue\" OWNER postgres ENCODING 'UTF8';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE \"rxthinkcmf.thinkphp6.elevue\" TO postgres;"
```
:::
#### SQLite 数据库
SQLite 无需安装数据库服务器,数据存储在单个文件中,适合开发环境和小型项目。
::: code-group
```sh [Windows]
# XAMPP 已自带 SQLite 扩展,无需额外安装
# 只需创建数据库文件
mkdir D:\xampp\htdocs\runtime
echo. > D:\xampp\htdocs\runtime\database.sqlite
# 确认扩展已启用(编辑 D:\xampp8.2\php\php.ini):
# extension=pdo_sqlite
# extension=sqlite3
# 若前面有分号则去掉,保存后重启 XAMPP
# .env 配置
# DATABASE.TYPE = sqlite
# DATABASE.DATABASE = D:/xampp8.2/htdocs/runtime/database.sqlite
```
```sh [macOS]
brew install php sqlite
# 创建数据库文件
mkdir -p /www/api/runtime
touch /www/api/runtime/database.sqlite
```
```sh [Ubuntu / Debian]
sudo apt install php8.2-sqlite3
# 创建数据库文件
mkdir -p /www/api/runtime
touch /www/api/runtime/database.sqlite
```
```sh [Rocky Linux / CentOS]
sudo dnf install php-sqlite3
# 创建数据库文件
mkdir -p /www/api/runtime
touch /www/api/runtime/database.sqlite
```
:::
#### SQL Server 数据库
SQL Server 是微软商业数据库,XAMPP 已自带 pdo_sqlsrv 驱动,Windows 下开箱即用。
::: code-group
```sh [Windows]
# SQL Server 需单独安装(XAMPP 不自带数据库服务,但自带 PHP 驱动)
# 1. 下载安装 SQL Server Express:https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads
# 2. XAMPP 已自带 pdo_sqlsrv 扩展,在 php.ini 中启用:
# extension=php_sqlsrv_82_ts_x64.dll
# extension=php_pdo_sqlsrv_82_ts_x64.dll
# 创建数据库
sqlcmd -S localhost -U sa -P root -Q "CREATE DATABASE [rxthinkcmf.thinkphp6.elevue] COLLATE Chinese_PRC_CI_AS"
```
```sh [macOS]
# macOS 不原生支持 SQL Server,建议使用 Docker
docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=root" \
-p 1433:1433 --name sqlserver \
-d mcr.microsoft.com/mssql/server:2022-latest
# 安装 PHP 扩展
pecl install pdo_sqlsrv
# 创建数据库
sqlcmd -S localhost -U sa -P root -Q "CREATE DATABASE [rxthinkcmf.thinkphp6.elevue] COLLATE Chinese_PRC_CI_AS"
```
```sh [Ubuntu / Debian]
# 1. 安装 SQL Server
curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
sudo add-apt-repository "$(wget -qO- https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list)"
sudo apt update
sudo apt install -y mssql-server
sudo /opt/mssql/bin/mssql-conf setup
# 2. 安装命令行工具
sudo apt install -y mssql-tools18
echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bashrc
source ~/.bashrc
# 3. 安装 PHP 扩展
sudo pecl install pdo_sqlsrv
echo "extension=pdo_sqlsrv.so" | sudo tee /etc/php/8.2/mods-available/pdo_sqlsrv.ini
sudo phpenmod pdo_sqlsrv
# 创建数据库
sqlcmd -S localhost -U sa -P root -Q "CREATE DATABASE [rxthinkcmf.thinkphp6.elevue] COLLATE Chinese_PRC_CI_AS"
```
```sh [Rocky Linux / CentOS]
# 1. 安装 SQL Server
sudo curl -o /etc/yum.repos.d/mssql-server.repo https://packages.microsoft.com/config/rhel/8/mssql-server-2022.repo
sudo dnf install -y mssql-server
sudo /opt/mssql/bin/mssql-conf setup
# 2. 安装命令行工具
sudo curl -o /etc/yum.repos.d/msprod.repo https://packages.microsoft.com/config/rhel/8/prod.repo
sudo dnf install -y mssql-tools18
echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bashrc
source ~/.bashrc
# 3. 安装 PHP 扩展
sudo pecl install pdo_sqlsrv
echo "extension=pdo_sqlsrv.so" | sudo tee /etc/php.d/pdo_sqlsrv.ini
# 创建数据库
sqlcmd -S localhost -U sa -P root -Q "CREATE DATABASE [rxthinkcmf.thinkphp6.elevue] COLLATE Chinese_PRC_CI_AS"
```
:::
#### Oracle 数据库
Oracle 是企业级商业数据库,需手动安装 Instant Client 和 pdo_oci 扩展。
::: code-group
```sh [Windows]
# XAMPP 已自带 php_pdo_oci.dll,只需安装 Oracle Instant Client 并在 php.ini 中启用
# 1. 安装 Oracle Instant Client
# 下载地址:https://www.oracle.com/database/technologies/instant-client/downloads.html
# 选择 Basic Package,x64,推荐 19c
# 解压到 C:\oracle\instantclient_19_20(⚠️ 避免空格与中文)
# 2. 配置系统环境变量
# 将 C:\oracle\instantclient_19_20 添加到系统 PATH
# 3. 安装 VC++ 运行库(必装)
# 下载:https://aka.ms/vc14/vc_redist.x64.exe
# 4. 启用扩展
# 编辑 D:\xampp8.2\php\php.ini,去掉分号:
# extension=pdo_oci
# 5. 重启 XAMPP(完全关闭再启动)
# .env 配置
# DATABASE.TYPE = oracle
# DATABASE.HOSTNAME = 127.0.0.1
# DATABASE.DATABASE = rxthinkcmf.thinkphp6.elevue
# DATABASE.USERNAME = root
# DATABASE.PASSWORD = root
# DATABASE.HOSTPORT = 1521
```
```sh [macOS]
# Oracle 不原生支持 macOS,建议使用 Docker
docker run -d --name oracle-xe -p 1521:1521 \
-e ORACLE_PWD=root \
gvenzl/oracle-xe:21-slim
# 安装 PHP 扩展
pecl install pdo_oci
```
```sh [Ubuntu / Debian]
# 1. 安装 Oracle Database
sudo dpkg -i oracle-database-xe-21c-1.0-1.x86_64.deb
sudo /etc/init.d/oracle-xe-21c configure
# 2. 安装 Oracle Instant Client
sudo dpkg -i oracle-instantclient-basic-*.deb
sudo dpkg -i oracle-instantclient-devel-*.deb
# 3. 安装 PHP 扩展
sudo pecl install pdo_oci
echo "extension=pdo_oci.so" | sudo tee /etc/php/8.2/mods-available/pdo_oci.ini
sudo phpenmod pdo_oci
# 4. 创建表空间和用户
sqlplus system/root@localhost:1521/XE <<EOF
CREATE TABLESPACE "rxthinkcmf_thinkphp6_elevue" DATAFILE '/opt/oracle/oradata/XE/rxthinkcmf_thinkphp6_elevue.dbf' SIZE 100M AUTOEXTEND ON NEXT 100M;
CREATE USER "root" IDENTIFIED BY "root" DEFAULT TABLESPACE "rxthinkcmf_thinkphp6_elevue";
GRANT CONNECT, RESOURCE, CREATE VIEW TO "root";
GRANT UNLIMITED TABLESPACE TO "root";
EXIT;
EOF
```
```sh [Rocky Linux / CentOS]
# 1. 安装 Oracle Database
sudo rpm -ivh oracle-database-xe-21c-1.0-1.x86_64.rpm
sudo /etc/init.d/oracle-xe-21c configure
# 2. 安装 Oracle Instant Client
sudo rpm -ivh oracle-instantclient-basic-*.rpm
sudo rpm -ivh oracle-instantclient-devel-*.rpm
# 3. 安装 PHP 扩展
sudo pecl install pdo_oci
echo "extension=pdo_oci.so" | sudo tee /etc/php/8.2/mods-available/pdo_oci.ini
sudo phpenmod pdo_oci
# 4. 创建表空间和用户
sqlplus system/root@localhost:1521/XE <<EOF
CREATE TABLESPACE "rxthinkcmf_thinkphp6_elevue" DATAFILE '/opt/oracle/oradata/XE/rxthinkcmf_thinkphp6_elevue.dbf' SIZE 100M AUTOEXTEND ON NEXT 100M;
CREATE USER "root" IDENTIFIED BY "root" DEFAULT TABLESPACE "rxthinkcmf_thinkphp6_elevue";
GRANT CONNECT, RESOURCE, CREATE VIEW TO "root";
GRANT UNLIMITED TABLESPACE TO "root";
EXIT;
EOF
```
:::
## SQL 脚本
项目为每种数据库提供了独立的 SQL 脚本,存放在 `document/` 目录下按数据库类型分组:
```text
document/
├── mysql/
│ └── rxthinkcmf.thinkphp6.elevue.sql # MySQL 版本
├── postgresql/
│ └── rxthinkcmf.thinkphp6.elevue.sql # PostgreSQL 版本
├── sqlserver/
│ └── rxthinkcmf.thinkphp6.elevue.sql # SQL Server 版本
├── oracle/
│ └── rxthinkcmf.thinkphp6.elevue.sql # Oracle 版本
└── sqlite/
└── rxthinkcmf.thinkphp6.elevue.sql # SQLite 版本
```
各数据库脚本包含完整的表结构定义和初始数据(用户、角色、菜单、字典、城市行政区划等),已针对各数据库的 SQL 语法做了适配,可直接导入使用。
导入方法详见 [2.4 数据库初始化](/zh/quickstart/database-init.html)。
## 慢查询排查
```ini
# MySQL my.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
```
:::warning 注意
项目所有查询都通过 ThinkPHP ORM 构建,不会出现原生 SQL 注入。如需排查慢查询,建议开启数据库慢查询日志,而非在代码层面埋点。
:::
@@ -0,0 +1,270 @@
# 4.5 目录结构设计
:::tip 概述
目录结构遵循"按职责分层、按模块分组"的原则。后端以 `app/` 为核心,按 Controller → Logic → Model → Validate 四层组织;前端以 `src/` 为核心,按 API → Views → Store 分层。两侧通过路由和 API 接口对接。
:::
## 后端整体结构
```text
项目根目录/
├── app/ ← 应用代码(核心)
├── config/ ← 配置文件
├── extend/ ← 扩展类库(非 Composer)
├── public/ ← Web 入口 + 静态资源
├── route/ ← 路由定义
├── templates/ ← 代码生成器模板
├── runtime/ ← 运行时缓存(自动生成)
├── vendor/ ← Composer 依赖
├── wiki/ ← 文档站点(VitePress)
├── document/ ← 开发参考手册(txt)
├── .env ← 环境变量配置
└── think ← ThinkPHP 命令行入口
```
## app/ 目录详解
```text
app/
├── BaseController.php 控制器基类
├── BaseModel.php 模型基类
├── BaseLogic.php 业务逻辑基类(系统共享模块)
├── BaseTenantLogic.php 租户逻辑基类(租户隔离模块)
├── ExceptionHandle.php 统一异常处理(JSON 响应)
├── Request.php 请求类扩展
├── common.php 公共函数库
├── event.php 事件定义
├── middleware.php 全局中间件注册
│
├── controller/ 控制器层(28 个)
├── logic/ 业务逻辑层(26 个)
├── model/ 数据模型层(25 个)
├── validate/ 验证器(21 个)
├── service/ 通用服务层(11 个)
├── middleware/ 中间件(5 个)
├── attribute/ 注解定义(3 个)
├── command/ 命令行命令(4 个)
└── task/ 定时任务处理器(3 个)
```
### 各目录职责与命名规范
| 目录 | 职责 | 命名规则 | 数量 | 说明 |
|------|------|----------|------|------|
| `controller/` | HTTP 请求入口 | `{Module}Controller.php` | 28 | 一个模块一个文件,注解标注权限和日志 |
| `logic/` | 业务逻辑 | `{Module}Logic.php` | 26 | 属性配置 + 生命周期钩子,核心业务层 |
| `model/` | 数据库映射 | `{TableName}.php`(大驼峰) | 25 | 继承 BaseModel,获软删除 + 审计字段 |
| `validate/` | 参数校验 | `{Module}Validate.php` | 21 | 定义规则 + 场景,Logic 层自动调用 |
| `service/` | 通用服务 | `{Name}Service.php` | 11 | 跨模块复用的无状态工具类 |
| `middleware/` | 中间件 | `{Name}Middleware.php` | 5 | 请求拦截:认证、租户、日志等 |
| `attribute/` | 注解类 | `{Name}.php` | 3 | PHP 8 注解:Log、Permission、DemoAllow |
| `command/` | 命令行 | `{Name}Command.php` | 4 | CLI 命令:定时任务、代码生成、迁移 |
| `task/` | 任务处理器 | `{Name}Task.php` | 3 | 定时任务的具体执行逻辑 |
### 模块文件对应关系
以"用户管理"模块为例,一个完整模块包含 4 个文件:
```text
app/
├── controller/UserController.php ← HTTP 入口(路由指向这里)
├── logic/UserLogic.php ← 业务逻辑(属性配置 + 钩子)
├── model/User.php ← 数据模型(表映射)
└── validate/UserValidate.php ← 参数校验(规则 + 场景)
```
请求流转:`Controller → Logic → Model → 数据库`
### 基类与公共文件
| 文件 | 说明 |
|------|------|
| `BaseController.php` | 控制器基类:统一响应、参数获取、通用 CRUD 快捷方法 |
| `BaseLogic.php` | 逻辑基类:声明式 CRUD、生命周期钩子、自动参数校验、文件/富文本/枚举/权限/租户处理 |
| `BaseTenantLogic.php` | 租户逻辑基类:继承 BaseLogic,自动启用 `tenant_id` 隔离 |
| `BaseModel.php` | 模型基类:软删除(`is_delete`)、审计字段自动写入 |
| `ExceptionHandle.php` | 异常处理:所有异常统一渲染为 JSON 响应 |
| `common.php` | 公共函数:字符串处理、加密、文件操作、驼峰转换等 |
| `event.php` | 事件定义:全局事件监听 |
| `middleware.php` | 全局中间件注册:CORS 等 |
### service/ 服务层清单
服务层提供跨模块复用的无状态能力,按单一职责拆分:
| 服务 | 文件 | 说明 |
|------|------|------|
| JWT 认证 | `JwtService.php` | 登录、刷新、解析 Token |
| 密码加密 | `PasswordService.php` | bcrypt 双重加盐 |
| 字典服务 | `DictService.php` | 字典缓存与查询(两级缓存) |
| 系统参数 | `ParamService.php` | 系统参数服务(两级缓存) |
| 验证码 | `CaptchaService.php` | 图片验证码生成与校验 |
| 请求信息 | `RequestInfoService.php` | 客户端 IP、UA、归属地解析 |
| 注解读取 | `AttributeService.php` | PHP 8 注解反射读取 + 缓存 |
| 代码生成 | `GeneratorService.php` | 根据数据库表生成 CRUD 代码 |
| 数据库迁移 | `DbMigrateService.php` | 跨数据库迁移引擎 |
| DDL 生成 | `DbSchemaBuilder.php` | 跨库类型映射的 DDL 生成器 |
| Excel | `ExcelService.php` | 导入导出(支持大数据量分批) |
### middleware/ 中间件清单
| 中间件 | 文件 | 作用域 | 说明 |
|--------|------|--------|------|
| 跨域 | `CorsMiddleware.php` | 全局 | CORS 头设置 |
| 认证权限 | `AuthMiddleware.php` | 路由级 | JWT 校验 + 权限校验 |
| 租户上下文 | `TenantMiddleware.php` | 路由级 | 解析注入 tenantId |
| 演示环境 | `DemoMiddleware.php` | 路由级 | 演示环境写操作拦截 |
| 操作日志 | `LogMiddleware.php` | 路由级 | 基于注解记录操作日志 |
### command/ 命令行清单
| 命令 | 文件 | 说明 |
|------|------|------|
| 定时任务执行 | `JobRunCommand.php` | 执行一次定时任务 |
| 定时任务守护 | `JobDaemonCommand.php` | 常驻进程循环扫描执行 |
| 代码生成 | `GeneratorCommand.php` | CLI 方式生成 CRUD 代码 |
| 数据库迁移 | `DbMigrateCommand.php` | 跨数据库迁移 CLI 工具 |
## config/ 配置文件
```text
config/
├── api.php API 配置(驼峰转换开关)
├── app.php 应用配置(调试模式、异常处理)
├── cache.php 缓存配置(支持 Redis)
├── console.php 命令行配置
├── cors.php 跨域配置
├── database.php 数据库配置
├── file.php 文件上传配置(域名、大小限制)
├── jwt.php JWT 配置(密钥、过期时间)
├── middleware.php 中间件排除列表
└── route.php 路由配置
```
| 配置文件 | 关键配置项 | 说明 |
|----------|-----------|------|
| `api.php` | `camel_snake_convert` | 驼峰/下划线自动转换开关 |
| `database.php` | `type` / `hostname` / `database` | 数据库连接(支持 MySQL/PG/SqlServer/Oracle/SQLite) |
| `file.php` | `domain_url` / `max_size` | 文件上传域名和大小限制 |
| `jwt.php` | `secret` / `access_ttl` / `refresh_ttl` | JWT 密钥和令牌有效期 |
| `cache.php` | `type` / `host` | 缓存驱动(file/redis) |
## extend/ 扩展类库
存放不通过 Composer 管理的自定义类库,按 PSR-4 自动加载:
```text
extend/
├── jwt/
│ └── Jwt.php JWT 工具类(封装 firebase/php-jwt)
├── response/
│ └── Result.php 统一响应类(success/fail/page 等)
└── (无自定义扩展类)
```
使用方式:直接通过命名空间引用,如 `\response\Result::success()`、`\jwt\Jwt::getTokenFromHeader()`。
## route/ 路由定义
```text
route/
└── app.php 应用路由定义(所有 API 路由)
```
路由按模块分组,示例:
```php
// 公开接口(无需认证)
Route::group('login', function () {
Route::post('login', 'LoginController/login');
Route::get('captcha', 'LoginController/captcha');
});
// 需认证接口(AuthMiddleware)
Route::group('article', function () {
Route::get('page', 'ArticleController/page');
Route::post('add', 'ArticleController/add');
Route::put('update', 'ArticleController/update');
Route::delete('delete/:id', 'ArticleController/delete');
})->middleware(AuthMiddleware::class);
```
## templates/ 代码生成模板
```text
templates/
├── controller.php.tpl 控制器模板
├── logic.php.tpl 业务逻辑模板
├── model.php.tpl 模型模板
├── validate.php.tpl 验证器模板
├── ui/ 普通列表前端模板
└── ui2/ 树形结构前端模板
```
由 `GeneratorService` 渲染,通过代码生成器 CLI 或 API 调用。
## 前端目录设计
```text
ui/src/
├── api/ ← API 接口(按业务域分组)
├── views/ ← 页面视图(按业务域分组)
├── components/ ← 公共组件(跨页面复用)
├── store/ ← 状态管理(Pinia)
├── router/ ← 路由配置
├── hooks/ ← 组合式函数
├── directives/ ← 自定义指令
├── enums/ ← 枚举常量
├── styles/ ← 全局样式
├── utils/ ← 工具函数
├── layout/ ← 布局组件
├── plugins/ ← 插件
├── settings/ ← 项目配置
└── assets/ ← 静态资源
```
## 前后端目录对应关系
| 后端 | 前端 | 说明 |
|------|------|------|
| `app/controller/UserController.php` | `src/api/system/user.ts` | API 接口函数 |
| `app/logic/UserLogic.php` | `src/views/system/user/` | 业务页面 |
| `app/model/User.php` | — | 前端不直接操作模型 |
| `app/validate/UserValidate.php` | — | 前端校验由表单组件处理 |
| `route/app.php` | `src/api/**/*.ts` | 路由 ↔ API 函数映射 |
| `config/dict*.php` | `src/enums/` | 字典 ↔ 枚举常量 |
## 设计原则
### 按职责分层
每层只做一件事,层间通过方法调用串联:
| 层 | 职责 | 不应该做 |
|----|------|----------|
| Controller | 参数获取、响应封装、注解标注 | 业务逻辑、数据库操作 |
| Logic | 业务编排、钩子处理、校验 | 直接 HTTP 交互 |
| Model | 表映射、关联关系、软删除 | 复杂业务判断 |
| Validate | 参数格式校验 | 业务规则校验 |
### 按模块分组
同一业务模块的 4 个文件(Controller / Logic / Model / Validate)分散在各自目录中,通过命名前缀关联。这种"水平分层 + 垂直命名"的方式:
- 便于按职责查找(所有控制器在一起)
- 便于代码生成器按模板批量生成
- 便于基类统一处理(如 BaseLogic 的属性驱动)
### 文件命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 控制器 | 大驼峰 + Controller | `ArticleController.php` |
| 逻辑层 | 大驼峰 + Logic | `ArticleLogic.php` |
| 模型 | 大驼峰(表名) | `Article.php` |
| 验证器 | 大驼峰 + Validate | `ArticleValidate.php` |
| 服务 | 大驼峰 + Service | `DictService.php` |
| 中间件 | 大驼峰 + Middleware | `AuthMiddleware.php` |
| 注解 | 大驼峰 | `Log.php`、`Permission.php` |
| 命令 | 大驼峰 + Command | `GeneratorCommand.php` |
| 任务 | 大驼峰 + Task | `SendSmsTask.php` |
@@ -0,0 +1,612 @@
# 4.11 扩展性设计
:::tip 概述
系统通过六种机制保证良好的扩展性:属性配置、生命周期钩子、自动参数校验、中间件链、注解系统、服务层复用。新增模块无需修改框架代码,只需继承基类 + 配置属性 + 按需重写钩子。
:::
## 扩展机制总览
| 机制 | 位置 | 作用 | 扩展方式 |
|------|------|------|----------|
| 属性配置 | Logic 子类 | 声明式行为定义 | 设置属性值 |
| 生命周期钩子 | Logic 子类 | 注入定制逻辑 | 重写钩子方法 |
| 自动参数校验 | Validate + Logic | 参数格式校验 | 创建 Validate + 配置 `$validateClass` |
| 中间件链 | middleware/ | 请求拦截处理 | 创建中间件 + 注册 |
| 注解系统 | attribute/ | 声明式元数据 | 创建注解 + 中间件读取 |
| 服务层复用 | service/ | 跨模块通用能力 | 创建 Service 类 |
## 1. 属性配置模式
Logic 层通过属性配置声明行为,新增模块只需设置属性,无需编写 CRUD 代码。
### 完整属性清单
```php
class NewModuleLogic extends BaseLogic
{
/**
* 关联的模型类
*
* 所有增删改查操作都会基于此模型执行,子类必须指定
*
* @var string
*/
protected string $modelClass = NewModule::class;
/**
* 参数验证器类
*
* 配置后,新增 / 修改等操作会自动调用该验证器进行参数校验,
* 校验不通过会直接抛出异常,无需在业务代码中手动验证
*
* @var string
*/
protected string $validateClass = NewModuleValidate::class;
/**
* 需要自动处理的文件上传字段
*
* 提交数据中若包含这些字段,框架会自动完成文件上传、存储与路径回填
*
* @var array
*/
protected array $fileFields = ['cover', 'avatar'];
/**
* 文件保存的子目录
*
* 上传的文件会保存在该子目录下(相对于文件存储根目录)
*
* @var string
*/
protected string $fileSaveDir = 'new_module';
/**
* 富文本字段
*
* 这些字段会做富文本特殊处理(如 XSS 过滤、图片路径修正等)
*
* @var array
*/
protected array $contentFields = ['content'];
/**
* 列表查询中需要 LIKE 模糊匹配的字段
*
* 请求参数中若带有这些字段,会自动以 LIKE '%value%' 的方式参与查询
*
* @var array
*/
protected array $pageLikeFields = ['name', 'title'];
/**
* 列表查询中需要精确匹配的字段
*
* 请求参数中若带有这些字段,会自动以 = 的方式参与查询
*
* @var array
*/
protected array $pageEqFields = ['status', 'type'];
/**
* 列表查询的默认排序规则
*
* field:排序字段;type:排序方式(asc / desc)
*
* @var array
*/
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
/**
* 唯一性校验字段
*
* 支持两种写法:
* - 字符串:单字段全局唯一,如 'code'
* - 数组:多字段组合唯一,如 ['name', 'pid'] 表示同层级下 name 唯一
*
* 新增 / 修改时会自动校验,冲突则抛出异常
*
* @var array
*/
protected array $uniqueFields = ['code', ['name', 'pid']];
/**
* 枚举字段与显示名的映射
*
* 键为数据表字段名,值为对应的枚举字典 key,
* 返回数据时会自动附加对应的枚举显示名(如 status_text)
*
* @var array
*/
protected array $serializeMaps = ['status' => 'module_status'];
/**
* 数据权限-按创建人过滤的字段
*
* 配置后,非管理员用户只能看到自己创建的数据
*
* @var string
*/
protected string $dataScopeUserField = 'create_user';
/**
* 数据权限-按部门过滤的字段
*
* 配置后,非管理员用户只能看到本部门(及子部门)的数据
*
* @var string
*/
protected string $dataScopeDeptField = 'dept_id';
/**
* 租户隔离字段
*
* 继承 BaseTenantLogic 时自动启用,所有查询会自动追加 tenant_id 条件,
* 保证多租户之间的数据完全隔离
*
* @var string
*/
// protected string $tenantScopeField = 'tenant_id';
/**
* 树形结构的父级字段
*
* 配置后,列表接口会自动将平铺数据组装成树形结构
*
* @var string
*/
protected string $treeParentField = 'parent_id';
/**
* 树形结构的搜索字段
*
* 树形搜索时对该字段做模糊匹配,命中节点会自动保留其父链
*
* @var string
*/
protected string $treeLikeField = 'name';
}
```
### 属性驱动的自动行为
| 属性 | 触发的自动行为 |
|------|---------------|
| `validateClass` | add/update 时自动调用验证器校验参数 |
| `fileFields` | add/update 时迁移临时文件,查询时补全域名 |
| `contentFields` | add/update 时处理富文本媒体文件,查询时替换占位符 |
| `pageLikeFields` | pageList 时自动构建 LIKE 查询 |
| `pageEqFields` | pageList 时自动构建精确查询 |
| `pageOrderBy` | pageList/allList 时自动应用默认排序 |
| `uniqueFields` | add/update 时自动校验唯一性 |
| `serializeMaps` | 查询时自动补全 `{字段名}Text` 枚举显示名 |
| `dataScopeUserField` | 查询时自动按创建人过滤 |
| `dataScopeDeptField` | 查询时自动按部门过滤 |
| `tenantScopeField` | 查询时自动叠加 tenant_id 条件,新增时自动填充 |
## 2. 生命周期钩子
通过重写钩子注入定制逻辑,无需修改基类。钩子方法在基类中定义为空实现或默认返回,子类按需覆盖。
### 钩子清单
| 钩子 | 时机 | 参数 | 返回值 | 可拦截 |
|------|------|------|--------|--------|
| `beforeAdd` | 新增前 | `$data` | 处理后的 `$data` | ✅ 抛异常 |
| `afterAdd` | 新增后 | `$id, $data` | void | ❌ |
| `beforeUpdate` | 修改前 | `$id, $data` | 处理后的 `$data` | ✅ 抛异常 |
| `afterUpdate` | 修改后 | `$id, $data` | void | ❌ |
| `beforeDelete` | 删除前 | `$id` | void | ✅ 抛异常 |
| `afterDelete` | 删除后 | `$id` | void | ❌ |
| `beforeBatchDelete` | 批量删除前 | `$ids` | void | ✅ 抛异常 |
| `afterBatchDelete` | 批量删除后 | `$ids` | void | ❌ |
| `afterDetail` | 详情查询后 | `$id, &$data` | void(引用传递) | ❌ |
| `afterPageList` | 列表查询后 | `&$records, $params` | void(引用传递,分页与全量列表均触发) | ❌ |
### 典型用法
```php
class ArticleLogic extends BaseTenantLogic
{
/**
* 新增前处理:设置默认值
*
* @param array $data 待新增的数据
* @return array 处理后的数据
*/
protected function beforeAdd(array $data): array
{
// sort:排序字段,若未传则默认为 0
$data['sort'] = $data['sort'] ?? 0;
// author:作者字段,若未传则尝试从当前登录用户信息中取用户名
$data['author'] = $data['author'] ?? request()->userInfo->username ?? '';
return $data;
}
/**
* 新增后处理:保存关联数据
*
* @param int $id 新增文章的主键 ID
* @param array $data 新增时提交的原始数据
* @return void
*/
protected function afterAdd(int $id, array $data): void
{
// 若提交数据中带有 tags(标签数组),则批量写入文章-标签关联表
if (!empty($data['tags'])) {
ArticleTag::insertAll(array_map(fn($tag) => [
'article_id' => $id, 'tag' => $tag
], $data['tags']));
}
}
/**
* 修改前处理:过滤不可修改字段
*
* @param int $id 文章 ID
* @param array $data 待更新的数据
* @return array 过滤后的数据
*/
protected function beforeUpdate(int $id, array $data): array
{
// author(作者)字段不允许通过更新接口修改,直接剔除
unset($data['author']);
return $data;
}
/**
* 删除前处理:业务约束校验
*
* @param int $id 文章 ID
* @return void
* @throws \Exception 文章下存在评论时抛出
*/
protected function beforeDelete(int $id): void
{
// 若该文章下存在评论,则不允许删除
$commentCount = Comment::where('article_id', $id)->count();
if ($commentCount > 0) {
throw new \Exception('该文章下存在评论,不可删除');
}
}
/**
* 详情查询后处理:补充单条记录的关联数据
*
* @param int $id 文章 ID
* @param array &$data 详情数据(引用传递)
* @return void
*/
protected function afterDetail(int $id, array &$data): void
{
$data['tags'] = ArticleTag::where('article_id', $id)->column('tag');
$data['commentCount'] = Comment::where('article_id', $id)->count();
}
/**
* 列表查询后处理:批量补充关联数据
*
* @param array &$records 记录列表(引用传递)
* @param array $params 查询参数
* @return void
*/
protected function afterPageList(array &$records, array $params): void
{
if (empty($records)) return;
// 提取当前页所有文章 ID
$ids = array_column($records, 'id');
// 一次性统计每篇文章的评论数,返回 [article_id => count] 结构
$counts = Comment::whereIn('article_id', $ids)
->group('article_id')
->column('count(*)', 'article_id');
// 将统计结果回填到每条记录
foreach ($records as &$record) {
$record['commentCount'] = $counts[$record['id']] ?? 0;
}
}
}
```
### 执行顺序
**add 流程:**
`beforeAdd` → `validateData` → `camel_to_snake` → `processTenantId` → `processFile` → `processContent` → `filterTableFields` → `checkUnique` → `Model::create` → `afterAdd`
**update 流程:**
`beforeUpdate` → `validateData` → `camel_to_snake` → `processFile` → `processContent` → `filterTableFields` → `checkUnique` → `Model::find + save` → `afterUpdate`
**delete 流程:**
`beforeDelete` → `is_delete = 1` → `afterDelete`
**pageList/allList 流程:**
`buildQuery` → `排序` → `分页` → `processFile` → `processContent` → `processSerializeMaps` → `filterFields` → `afterPageList`
## 3. 自动参数校验
通过在 Logic 中配置 `$validateClass`,add/update 时自动调用验证器,无需手动调用。
### 扩展方式
```php
// 1. 创建验证器
// app/validate/NewModuleValidate.php
class NewModuleValidate extends Validate
{
protected $rule = [
'name' => 'require|max:100',
'code' => 'require|unique:new_module',
];
protected $message = [
'name.require' => '名称不能为空',
'code.unique' => '编码已存在',
];
protected $scene = [
'add' => ['name', 'code'],
'update' => ['name'],
];
}
// 2. 在 Logic 中配置
class NewModuleLogic extends BaseLogic
{
protected string $validateClass = NewModuleValidate::class;
// ...
}
```
详见 [3.4 验证器开发](/zh/guide/validator.html)。
## 4. 中间件机制
通过中间件链式处理请求,可灵活添加新的拦截逻辑。
### 现有中间件
```text
请求进入
│
▼
CorsMiddleware(全局) ← OPTIONS 预检 + 跨域响应头
│
▼
AuthMiddleware(路由级) ← JWT 认证 + 权限校验 → 401/403
│ 注入 userInfo
▼
TenantMiddleware(路由级) ← 租户上下文注入 → 403
│ 注入 tenantId
▼
DemoMiddleware(路由级) ← 演示环境拦截写操作 → 403
│
▼
LogMiddleware(路由级) ← 操作日志记录
│
▼
控制器方法
```
### 创建自定义中间件
```php
// 1. 创建中间件类
// app/middleware/RateLimitMiddleware.php
namespace app\middleware;
use think\facade\Cache;
class RateLimitMiddleware
{
public function handle($request, \Closure $next)
{
$ip = $request->ip();
$key = 'rate_limit_' . $ip;
$count = Cache::get($key, 0);
if ($count > 100) {
return json(['code' => 1, 'msg' => '请求过于频繁'], 429);
}
Cache::set($key, $count + 1, 60);
return $next($request);
}
}
// 2. 注册中间件
// 方式一:全局注册(app/middleware.php)
return [
\app\middleware\RateLimitMiddleware::class,
];
// 方式二:路由级注册(route/app.php)
Route::group('api', function () {
// ...
})->middleware(\app\middleware\RateLimitMiddleware::class);
// 方式三:控制器级注册
class SomeController extends BaseController
{
protected $middleware = [
\app\middleware\RateLimitMiddleware::class,
];
}
```
## 5. 注解系统
通过 PHP 8 注解声明元数据,中间件通过反射读取,业务代码零侵入。
### 现有注解
| 注解 | 目标 | 说明 | 读取方 |
|------|------|------|--------|
| `#[Log]` | 方法 | 操作日志(标题、类型、描述) | LogMiddleware |
| `#[Permission]` | 方法/类 | 权限校验(权限编码) | AuthMiddleware |
| `#[DemoAllow]` | 方法 | 演示环境放行标记 | DemoMiddleware |
### 使用示例
```php
// 操作日志
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
// 权限校验
#[Permission('sys:user:add', '添加用户')]
// 演示环境放行(允许在演示环境执行写操作)
#[DemoAllow]
// 组合使用
#[Log('文章管理-删除记录', Log::TYPE_DELETE, '删除文章ID:{id}')]
#[Permission('sys:article:delete', '删除文章')]
public function delete(int $id): Json
{
return parent::_remove($this->logic, $id);
}
```
### 创建自定义注解
```php
// 1. 定义注解类
// app/attribute/Cacheable.php
namespace app\attribute;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Cacheable
{
public function __construct(
public string $key = '',
public int $ttl = 3600,
) {}
}
// 2. 在控制器方法上使用
#[Cacheable(key: 'article_list', ttl: 1800)]
public function list(): Json { ... }
// 3. 在中间件中读取
// 通过 AttributeService 或反射获取注解实例
$ref = new \ReflectionMethod($controller, $method);
$attrs = $ref->getAttributes(Cacheable::class);
if (!empty($attrs)) {
$cacheable = $attrs[0]->newInstance();
// $cacheable->key, $cacheable->ttl
}
```
### 注解读取服务
`AttributeService` 提供统一的注解读取接口(带缓存):
```php
AttributeService::getLog($class, $method); // 获取 Log 注解
AttributeService::getPermission($class, $method); // 获取 Permission 注解
```
## 6. 服务层复用
通用能力封装为 Service,跨模块复用,无状态、无副作用。
### 现有服务清单
| 服务 | 文件 | 能力 |
|------|------|------|
| `JwtService` | `app/service/JwtService.php` | 登录、刷新、解析 Token |
| `PasswordService` | `app/service/PasswordService.php` | bcrypt 双重加盐加密 |
| `DictService` | `app/service/DictService.php` | 字典查询(两级缓存) |
| `ParamService` | `app/service/ParamService.php` | 系统参数查询(两级缓存) |
| `CaptchaService` | `app/service/CaptchaService.php` | 图片验证码生成与校验 |
| `RequestInfoService` | `app/service/RequestInfoService.php` | 客户端 IP、UA、归属地解析 |
| `AttributeService` | `app/service/AttributeService.php` | 注解反射读取 + 缓存 |
| `GeneratorService` | `app/service/GeneratorService.php` | 代码生成 |
| `ExcelService` | `app/service/ExcelService.php` | Excel 导入导出 |
| `DbMigrateService` | `app/service/DbMigrateService.php` | 跨数据库迁移引擎 |
| `DbSchemaBuilder` | `app/service/DbSchemaBuilder.php` | DDL 生成器(跨库映射) |
### 使用示例
```php
// 字典服务
DictService::getText('gender', 1); // → '男'
DictService::getValue('user_status', '启用'); // → 1
DictService::getOptions('gender'); // → [['value'=>0,'label'=>'女'], ...]
// 密码服务
$encrypted = PasswordService::encrypt('123456'); // → ['password'=>'...', 'salt'=>'...']
$default = PasswordService::getDefaultPasswordPlain(); // → '123456'
// Excel 服务
ExcelService::importWithMap($filePath, $headerMap);
ExcelService::chunkExport($headers, $callback, '导出.xlsx', 500);
// 请求信息
$requestInfo = RequestInfoService::create(request());
$ip = $requestInfo->getIp(); // 客户端 IP(经 ProxyMiddleware 还原真实 IP)
$location = $requestInfo->getIpLocation(); // IP 归属地(基于 zoujingli/ip2region)
$os = $requestInfo->getOs(); // 操作系统
$browser = $requestInfo->getBrowser(); // 浏览器
```
### 创建自定义服务
```php
// app/service/NotificationService.php
namespace app\service;
class NotificationService
{
// 发送站内通知
public static function send(int $userId, string $title, string $content): void
{
\app\model\Notice::create([
'user_id' => $userId,
'title' => $title,
'content' => $content,
'status' => 0,
]);
}
// 批量发送
public static function batchSend(array $userIds, string $title, string $content): void
{
foreach ($userIds as $userId) {
self::send($userId, $title, $content);
}
}
}
// 使用
NotificationService::send($userId, '系统通知', '您有一条新消息');
```
## 扩展性设计原则
### 开闭原则(OCP)
| 扩展场景 | 做法 | 不应该做 |
|----------|------|----------|
| 新增 CRUD 模块 | 创建 Controller/Logic/Model/Validate | 修改基类 |
| 新增查询条件 | 设置 `pageLikeFields`/`pageEqFields` | 重写 `buildQuery` |
| 新增文件处理 | 设置 `fileFields` | 手动调用 `save_file` |
| 新增校验规则 | 创建 Validate + 配置 `validateClass` | 在 Controller 手动 validate |
| 新增请求拦截 | 创建中间件 | 修改现有中间件 |
| 新增操作日志 | 添加 `#[Log]` 注解 | 在方法内手动记录日志 |
| 新增权限节点 | 添加 `#[Permission]` 注解 | 在方法内手动校验权限 |
| 新增通用能力 | 创建 Service | 在 Logic 中写静态方法 |
### 最小改动原则
新增一个完整的业务模块,只需要:
1. 建表(含公共字段)
2. 创建 Model(继承 BaseModel,声明表名)
3. 创建 Validate(定义规则 + 场景)
4. 创建 Logic(继承 BaseLogic/BaseTenantLogic,配置属性)
5. 创建 Controller(继承 BaseController,初始化 Logic,CRUD 一行调用)
6. 注册路由
**不需要修改的文件:** 基类、中间件、配置文件、服务层。
@@ -0,0 +1,425 @@
# 4.4 前端分层架构
:::tip 概述
前端采用 Vue 3 + TypeScript 组件化架构,分为 View(视图)→ API(接口)→ Store(状态)→ HTTP(请求)四层。辅以 Router(路由)、Directives(指令)、Hooks(组合式函数)、Utils(工具)等支撑模块。
:::
## 架构全景
```text
┌─────────────────────────────────────────────────────────────────────┐
│ 支撑模块 │
│ │
│ Router(路由) Directives(指令) Hooks(组合式函数) │
│ src/router/ src/directives/ src/hooks/ │
│ │
│ Utils(工具) Enums(枚举) Styles(样式) │
│ src/utils/ src/enums/ src/styles/ │
└──────────────────────────────┬──────────────────────────────────────┘
│ 被主干层引用
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 主干链路 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ View(视图层) │ │
│ │ 页面渲染、用户交互、调用 API 和 Store │ │
│ │ 文件:src/views/**/*.vue │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ 调用 API 函数 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ API(接口层) │ │
│ │ 封装后端 API 调用,定义请求参数和响应类型 │ │
│ │ 文件:src/api/**/*.ts │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ 调用 http.request() │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ HTTP(请求层) │ │
│ │ Axios 封装:拦截器、Token 携带、错误处理 │ │
│ │ 文件:src/utils/http/axios/ │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 后端 API(RESTful JSON) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Store(状态层) │ │
│ │ 全局状态管理:Token、用户信息、权限列表、动态路由 │ │
│ │ 文件:src/store/modules/*.ts │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## 各层职责详解
### View 层(视图)
**文件**:`src/views/**/*.vue`
**职责**:页面渲染、用户交互、调用 API 获取数据
```text
src/views/
├── system/ 系统管理
│ ├── user/ 用户管理
│ │ ├── index.vue 列表页
│ │ └── edit.vue 编辑弹窗
│ ├── role/ 角色管理
│ └── menu/ 菜单管理
├── content/ 内容管理
│ ├── article/ 文章管理
│ └── category/ 分类管理
├── login.vue 登录页
├── dashboard/ 仪表盘
└── ...
```
**典型页面结构:**
```vue
<template>
<!-- 搜索区 -->
<el-form :model="searchForm" inline>
<el-form-item label="名称">
<el-input v-model="searchForm.name" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchData">搜索</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<el-button v-perm="'sys:example:add'" type="primary" @click="handleAdd">新增</el-button>
<!-- 数据表格 -->
<el-table :data="tableData">
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="statusText" label="状态" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button v-perm="'sys:example:update'" @click="handleEdit(row)">编辑</el-button>
<el-button v-perm="'sys:example:delete'" type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize"
:total="total" @change="fetchData" />
</template>
<script setup lang="ts">
import { getExamplePage, exampleDelete } from '@/api/tool/example';
const tableData = ref([]);
const pageNo = ref(1);
const pageSize = ref(20);
const total = ref(0);
const fetchData = async () => {
const res = await getExamplePage({ pageNo: pageNo.value, pageSize: pageSize.value });
tableData.value = res.records;
total.value = res.total;
};
onMounted(() => fetchData());
</script>
```
### API 层(接口)
**文件**:`src/api/**/*.ts`
**职责**:封装后端 API 调用,一个后端模块对应一个 API 文件
**命名规范:**
| 操作 | 函数名 | HTTP 方法 | 参数传递 |
|------|--------|----------|----------|
| 分页查询 | `get{Module}Page` | GET | `params` |
| 全量列表 | `get{Module}List` | GET | `params` |
| 详情 | `get{Module}Detail` | GET | URL 参数 |
| 新增 | `{module}Add` | POST | `data` |
| 修改 | `{module}Update` | PUT | `data` |
| 删除 | `{module}Delete` | DELETE | URL 参数 |
| 批量删除 | `{module}BatchDelete` | DELETE | `data` |
| 导入 | `{module}Import` | POST | FormData |
| 导出 | `{module}Export` | GET | `params` |
**完整示例:**
```typescript
// src/api/system/user.ts
import { http } from '@/utils/http/axios';
// 分页查询
export function getUserPage(params?) {
return http.request({ url: '/user/page', method: 'GET', params });
}
// 详情
export function getUserDetail(id) {
return http.request({ url: '/user/detail/' + id, method: 'GET' });
}
// 新增
export function userAdd(data: any) {
return http.request({ url: '/user/add', method: 'POST', data });
}
// 修改
export function userUpdate(data: any) {
return http.request({ url: '/user/update', method: 'PUT', data });
}
// 删除
export function userDelete(id) {
return http.request({ url: '/user/delete/' + id, method: 'DELETE' });
}
// 批量删除
export function userBatchDelete(data: any) {
return http.request({ url: '/user/batchDelete', method: 'DELETE', data });
}
```
### HTTP 层(请求)
**文件**:`src/utils/http/axios/`
**职责**:Axios 封装,自动处理 Token、响应码、错误提示
```text
src/utils/http/axios/
├── index.ts Axios 实例配置和拦截器
├── Axios.ts VAxios 类封装
├── axiosTransform.ts 数据转换接口
├── checkStatus.ts HTTP 状态码处理
├── helper.ts 辅助函数
└── types.ts 类型定义
```
**核心拦截器:**
```text
请求拦截器:
┌──────────────────────────────────────────┐
│ 1. 从 Store 获取 Token │
│ 2. 自动添加 Authorization: Bearer <token> │
└──────────────────────────────────────────┘
响应拦截器:
┌──────────────────────────────────────────┐
│ code === 0 → 成功,返回 data │
│ code === 401 → Token 过期,跳转登录页 │
│ code === 403 → 权限不足,显示错误提示 │
│ 其它 code → 业务失败,显示 msg │
└──────────────────────────────────────────┘
```
### Store 层(状态)
**文件**:`src/store/modules/*.ts`
**职责**:全局状态管理(Pinia)
| Store | 文件 | 管理的状态 |
|-------|------|-----------|
| userStore | `user.ts` | Token、用户信息、权限列表、登录/登出 |
| asyncRouteStore | `asyncRoute.ts` | 动态路由、keep-alive 缓存列表 |
| projectSettingStore | `projectSetting.ts` | 项目配置(主题、布局等) |
| tabsViewStore | `tabsView.ts` | 标签页导航状态 |
| designSettingStore | `designSetting.ts` | 主题设计配置 |
| ossConfigStore | `ossConfig.ts` | 云存储配置 |
| lockscreenStore | `lockscreen.ts` | 锁屏状态 |
**userStore 核心流程:**
```text
登录:
userStore.login(userInfo)
→ POST /login → 获取 access_token
→ Storage.set(ACCESS_TOKEN, token)
→ this.setToken(token)
获取用户信息:
userStore.GetInfo()
→ GET /index/getUserInfo
→ this.setPermissions(res.permissions)
→ this.setUserInfo(res)
登出:
userStore.logout()
→ POST /logout
→ 清除 Token、用户信息、权限列表
→ 跳转登录页
```
## 路由与动态菜单
### 路由架构
```text
静态路由(固定,src/router/)
├── /login 登录页
├── / 根路径,重定向到首页
├── /redirect/:path 重定向路由
└── /:path(.*)* 404 错误页
动态路由(从后端菜单生成)
├── /dashboard 仪表盘
├── /system/user 用户管理
├── /system/role 角色管理
├── /content/article 文章管理
└── ...
```
### 动态路由生成流程
```text
1. 用户登录 → 获取 Token → 存入 Store
2. 路由守卫(router.beforeEach)检测到 Token
3. 调用 GET /index/getMenus 获取菜单树
4. generator-routers.ts 将菜单树转换为路由表
5. router.addRoute() 动态注入路由
6. 渲染侧边栏菜单
```
### 布局系统
```text
┌─────────────────────────────────────────────────────────────┐
│ 顶部导航栏 │
│ [Logo] [面包屑] [搜索] [通知] [头像] │
├──────────┬──────────────────────────────────────────────────┤
│ │ │
│ 侧边栏 │ 内容区域 │
│ 菜单 │ │
│ │ ┌──────────────────────────────────────────┐ │
│ ──────── │ │ 标签页导航 │ │
│ 系统管理 │ ├──────────────────────────────────────────┤ │
│ ├ 用户 │ │ │ │
│ ├ 角色 │ │ 页面内容(keep-alive 缓存) │ │
│ └ 菜单 │ │ │ │
│ │ └──────────────────────────────────────────┘ │
└──────────┴──────────────────────────────────────────────────┘
```
## 权限控制
### 按钮级权限指令
```vue
<!-- v-perm:单权限 -->
<el-button v-perm="'sys:user:add'" type="primary">新增</el-button>
<!-- v-perms:多权限(满足任一即可) -->
<el-button v-perms="['sys:user:add', 'sys:user:update']">操作</el-button>
```
无权限时元素直接从 DOM 移除,而非仅隐藏。
:::tip 超级管理员
用户权限列表包含 `*:*:*` 时,表示超级管理员,拥有所有权限,`v-perm`/`v-perms` 指令始终放行。
:::
## 支撑模块
### Hooks(组合式函数)
| Hook | 文件 | 说明 |
|------|------|------|
| `usePermission` | `hooks/web/usePermission.ts` | 权限判断(hasPermission、hasSomePermission) |
| `useTable` | `hooks/web/useTable.ts` | 表格通用逻辑(分页、搜索、刷新) |
| `useForm` | `hooks/web/useForm.ts` | 表单通用逻辑(提交、重置、校验) |
### Directives(自定义指令)
| 指令 | 说明 |
|------|------|
| `v-perm` | 单权限控制 |
| `v-perms` | 多权限控制(满足任一) |
### Utils(工具函数)
| 工具 | 文件 | 说明 |
|------|------|------|
| HTTP | `utils/http/axios/` | Axios 封装 |
| Storage | `utils/Storage.ts` | localStorage 封装 |
| Dict | `utils/dict.ts` | 字典工具(从 Store 获取字典值) |
## 数据流向
### 完整请求链路
```text
用户操作(点击搜索按钮)
│
▼
View: fetchData()
│ 调用 API 函数
▼
API: getExamplePage(params)
│ 调用 http.request()
▼
HTTP: 请求拦截器添加 Token
│ 发送 GET /example/page?pageNo=1&pageSize=20
▼
后端: Controller → Logic → Model → Database
│
▼
HTTP: 响应拦截器判断 code
│ code === 0 → 返回 data
▼
API: 返回 data
│
▼
View: tableData.value = res.records; total.value = res.total
│
▼
Vue 响应式 → 表格自动刷新
```
### Store 数据流
```text
登录成功
│
▼
userStore.setToken(token) → 响应式更新
userStore.setUserInfo(info) → 响应式更新
userStore.setPermissions(perm) → 响应式更新
│
▼
其他组件通过 userStore.username / userStore.permissions 读取
│
▼
v-perm 指令读取 permissions 列表 → 控制元素显隐
```
## 前后端对应关系
| 后端 | 前端 | 说明 |
|------|------|------|
| `app/controller/UserController.php` | `src/api/system/user.ts` | API 函数映射 |
| `app/logic/UserLogic.php` | `src/views/system/user/` | 业务页面 |
| `app/model/User.php` | — | 前端不直接操作模型 |
| `app/validate/UserValidate.php` | — | 前端校验由表单组件处理 |
| `app/validate/*Validate.php` 的场景 | 表单 `rules` | 前端表单校验规则 |
| `route/app.php` | `src/api/**/*.ts` | 路由 ↔ API 函数 |
| 后端字典 `DictService` | `src/enums/` + 字典 Store | 枚举常量 |
| 后端 `#[Permission]` 注解 | `v-perm` 指令 | 权限码对应 |
## 层间通信规范
| 调用方向 | 是否允许 | 说明 |
|----------|---------|------|
| View → API | ✅ | 标准调用 |
| View → Store | ✅ | 读取/修改全局状态 |
| View → Hooks | ✅ | 复用逻辑 |
| API → HTTP | ✅ | 调用 http.request() |
| API → Store | ❌ | API 层不应操作状态 |
| Store → API | ✅ | Store actions 中调用 API |
| HTTP → Store | ⚠️ | 仅在拦截器中读取 Token |
| Hooks → API | ✅ | Hooks 中可调用 API |
@@ -0,0 +1,329 @@
# 4.1 整体架构概览
:::tip 概述
本章从全局视角阐述系统的整体技术架构,涵盖前端、后端、数据库及基础设施等核心组成部分,旨在帮助开发者快速建立对系统全貌的清晰认知。
:::
## 架构总览
```text
┌─────────────────────────────────────────────────────────────────┐
│ 浏览器(Vue3 + ElementPlus) │
│ Axios HTTP 请求 / Vite 开发代理 │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Nginx(反向代理) │
│ 静态资源托管 / SSL 终止 / 负载均衡 / gzip 压缩 │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ThinkPHP 6.1 Application │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 中间件链(按顺序执行) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────┐ ┌───────┐ │ │
│ │ │ CORS │→│ Auth │→│ Tenant │→│ Demo │→│ Log │ │ │
│ │ │ 跨域处理 │ │JWT+权限 │ │ 租户上下文│ │演示拦截│ │操作日志│ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └──────┘ └───────┘ │ │
│ └───────────────────────────────┬───────────────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 控制器层(Controller) │ │
│ │ BaseController → 统一响应 · 参数获取 · 通用 CRUD │ │
│ │ #[Permission] 权限注解 · #[Log] 日志注解 │ │
│ └───────────────────────────────┬───────────────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 逻辑层(Logic) │ │
│ │ BaseLogic / BaseTenantLogic │ │
│ │ 属性配置 · 生命周期钩子 · 文件处理 · 枚举翻译 │ │
│ │ 唯一性校验 · 数据权限 · 租户隔离 · 事务管理 │ │
│ └───────────────────────────────┬───────────────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 模型层(Model) │ │
│ │ BaseModel │ │
│ │ 软删除(is_delete)· 审计字段自动写入 │ │
│ └───────────────────────────────┬───────────────────────────┘ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ MySQL │ │ Redis │ │ 文件系统 │ │
│ │ 数据库 │ │ 缓存 │ │ uploads/ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## 架构特征
:::tip 核心设计原则
遵循以下架构设计原则,确保系统在长期演进中保持高质量和可维护性:
:::
### 1. 前后端分离
前端 Vue3 应用通过 HTTP API 调用后端 ThinkPHP 服务,两者独立开发、独立部署。前端开发阶段通过 Vite 的 `proxy` 配置将 `/api` 请求代理到后端 `http://localhost:8000`,生产环境由 Nginx 统一反向代理。
### 2. 分层架构
后端严格分为三层,每层职责单一、依赖单向:
| 层次 | 目录 | 职责 |
|------|------|------|
| 控制器层 | `app/controller/` | 路由入口、权限注解、参数获取、响应返回 |
| 逻辑层 | `app/logic/` | 业务逻辑、属性配置、生命周期钩子、横切处理 |
| 模型层 | `app/model/` | 数据库表映射、软删除、审计字段 |
### 3. 模板方法模式(声明式 CRUD)
通过 `BaseLogic` 基类,将通用 CRUD 流程固化为模板方法。子类只需声明差异点(过滤字段、排序规则、唯一性校验等),即可获得完整的增删改查能力,极大减少重复代码。
```php
// 子类只需声明差异点,即可获得完整 CRUD 能力
class ExampleLogic extends BaseLogic
{
/**
* 关联的模型类
*
* @var string
*/
protected string $modelClass = Example::class;
/**
* LIKE 模糊匹配字段
*
* @var array
*/
protected array $pageLikeFields = ['name'];
/**
* 精确匹配字段
*
* @var array
*/
protected array $pageEqFields = ['status'];
/**
* 默认排序规则
*
* @var array
*/
protected array $pageOrderBy = ['field'=>'sort', 'type'=>'asc'];
/**
* 唯一性校验字段
*
* @var array
*/
protected array $uniqueFields = ['name'];
/**
* 枚举显示名映射
*
* @var array
*/
protected array $serializeMaps = ['status'=>'example_status'];
}
```
### 4. 中间件链式处理
请求处理采用中间件链,按注册顺序依次执行:
```text
请求进入 → CORS → Auth → Tenant → Demo → Log → 控制器
│
响应返回 ← CORS ← Auth ← Tenant ← Demo ← Log ←───┘
```
| 中间件 | 职责 | 配置方式 |
|--------|------|----------|
| CorsMiddleware | 跨域处理 | 全局,config/cors.php |
| AuthMiddleware | JWT 认证 + RBAC 鉴权 | 路由级,config/middleware.php |
| TenantMiddleware | 租户上下文注入 | 路由级 |
| DemoMiddleware | 演示环境拦截 | 路由级,.env [APP] DEMO |
| LogMiddleware | 操作日志记录 | 路由级,config/middleware.php |
### 5. 全局异常处理
`ExceptionHandle` 统一捕获所有异常并返回标准 JSON 响应:
| 来源 | 异常类型 | 触发场景 | 响应 |
|------|---------|---------|------|
| ExceptionHandle | ValidateException | 参数验证失败 | `{code:1, msg:"参数验证失败", data:{错误明细}}` |
| ExceptionHandle | ModelNotFoundException | 数据不存在 | `{code:1, msg:"数据不存在"}` |
| ExceptionHandle | HttpException | HTTP 错误 | `{code:1, msg:"请求错误"}` |
| ExceptionHandle | Exception | 未捕获异常 | `{code:1, msg:"操作失败"}` |
| AuthMiddleware | — | JWT 缺失/过期/无效、token 类型错误 | `{code:401, msg:"未提供认证令牌"}` 等 |
| PermissionMiddleware | — | 无接口权限 | `{code:403, msg:"没有权限"}` |
### 6. 统一响应格式
所有 API 端点返回统一的 JSON 结构:
```json
{
"code": 0,
"ok": true,
"msg": "操作成功",
"data": {}
}
```
成功响应 `code=0`,失败响应 `code=1`(ExceptionHandle 统一处理)。`code=401`(AuthMiddleware 直接返回)和 `code=403`(PermissionMiddleware 直接返回)由中间件在请求到达控制器之前拦截,不经过 ExceptionHandle。前端无需针对不同接口做差异化解析。
### 7. 软删除策略
:::warning 禁止物理删除
所有业务数据必须使用软删除(`is_delete` 字段),禁止物理删除。物理删除仅用于日志清理等特殊场景。
:::
所有业务表通过 `is_delete` 字段实现软删除(0=正常,1=已删除)。`BaseModel` 的全局查询范围自动追加 `is_delete=0` 过滤,业务代码无需关心已删除数据。
```php
// 自动过滤已删除数据
$users = User::select(); // WHERE is_delete = 0
// 查询已删除数据
$deleted = User::withoutGlobalScope(['soft_delete'])->select();
// 软删除
$user->softDelete(); // UPDATE SET is_delete = 1
// 批量软删除
User::batchSoftDelete([1, 2, 3]);
```
### 8. 审计字段自动写入
通过 BaseModel 的模型事件自动填充审计字段:
```php
// 创建前自动填充
public static function onBeforeInsert($model): void
{
$model->create_user = request()->userInfo->username ?? '';
$model->create_time = date('Y-m-d H:i:s');
}
// 更新前自动填充
public static function onBeforeUpdate($model): void
{
$model->update_user = request()->userInfo->username ?? '';
$model->update_time = date('Y-m-d H:i:s');
}
```
### 9. 多数据库支持
通过 `DATABASE.TYPE` 环境变量切换数据库驱动(MySQL / PostgreSQL / SQL Server / Oracle / SQLite),ThinkPHP ORM 的方言层屏蔽了底层差异,业务代码无需修改。
### 10. 两级缓存架构
```text
请求级缓存(static 变量) ← 同一请求内不重复查库
│ 未命中
▼
持久缓存(file / Redis) ← 跨请求复用,1小时过期
│ 未命中
▼
数据库查询 ← 查询后写入两级缓存
```
DictService 和 ParamService 均采用此架构,大幅减少数据库查询。
## 技术分层图
```text
┌──────────────────────────────────────────────────────────────┐
│ 前端(Vue3 + ElementPlus) │
│ Pinia · Vue Router · Axios · TinyMce · ECharts │
└──────────────────────────┬───────────────────────────────────┘
│ HTTP / JSON
┌──────────────────────────┴───────────────────────────────────┐
│ ThinkPHP 6.1 Application │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ 中间件 │ │ 路由 │ │ 异常处理器 │ │
│ │ ───────── │ │ ───────── │ │ ───────────────── │ │
│ │ CORS │ │ route/ │ │ ValidateException │ │
│ │ Auth │ │ app.php │ │ ModelNotFound │ │
│ │ Tenant │ │ │ │ RuntimeException │ │
│ │ Demo │ │ │ │ HttpException │ │
│ │ Log │ │ │ │ Exception │ │
│ └─────────────┘ └─────────────┘ └──────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 控制器层(app/controller/) │ │
│ │ BaseController · #[Permission] · #[Log] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 逻辑层(app/logic/) │ │
│ │ BaseLogic / BaseTenantLogic │ │
│ │ 属性配置 · 钩子 · 文件处理 · 枚举翻译 · 数据权限 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 模型层(app/model/) │ │
│ │ BaseModel · 软删除 · 审计字段 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ 服务层 │ │ 验证器 │ │ 注解 │ │
│ │ ────────────── │ │ ───────── │ │ ───────────────── │ │
│ │ JwtService │ │ Validate │ │ #[Log] │ │
│ │ PasswordService │ │ │ │ #[Permission] │ │
│ │ DictService │ │ │ │ #[DemoAllow] │ │
│ │ ParamService │ │ │ │ │ │
│ │ ExcelService │ │ │ │ │ │
│ │ CaptchaService │ │ │ │ │ │
│ │ GeneratorService│ │ │ │ │ │
│ │ AttributeService│ │ │ │ │ │
│ │ DbMigrateService│ │ │ │ │ │
│ │ DbSchemaBuilder │ │ │ │ │ │
│ │ RequestInfoServ │ │ │ │ │ │
│ └──────────────────┘ └─────────────┘ └──────────────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ MySQL │ │ Redis │
│ PostgreSQL │ │ 缓存 │
│ SQL Server │ └─────────────┘
│ Oracle │
│ SQLite │
└─────────────┘
```
## 模块化设计
每个业务模块遵循统一的文件结构:
```text
app/controller/UserController.php # HTTP 入口
app/logic/UserLogic.php # 业务逻辑
app/model/User.php # 数据模型
app/validate/UserValidate.php # 参数验证
```
| 文件 | 职责 | 基类 |
|------|------|------|
| Controller | 接收请求、调用 Logic、返回响应 | BaseController |
| Logic | 业务逻辑、属性配置、钩子处理 | BaseLogic / BaseTenantLogic |
| Model | 数据库表映射 | BaseModel |
| Validate | 参数校验规则 | ThinkPHP Validate |
## 关键设计决策
| 决策 | 选择 | 原因 |
|------|------|------|
| 认证方案 | JWT 双令牌 | 无状态,适合分布式部署 |
| 删除策略 | 软删除(is_delete) | 数据可恢复,避免误删 |
| 字段命名 | 数据库 snake_case / 前端 camelCase | 各端遵循各自惯例 |
| 缓存策略 | 两级缓存(请求级 + 持久缓存) | 减少数据库查询 |
| 数据库 | 多数据库支持 | 适配不同客户需求 |
| 权限模型 | RBAC(用户→角色→菜单) | 灵活的权限配置 |
@@ -0,0 +1,532 @@
# 4.2 请求处理链路
:::tip 链路概览
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS 中间件 → Auth 中间件(JWT 认证 + RBAC 鉴权)→ Tenant 中间件 → Demo 中间件 → Log 中间件 → 控制器 → 逻辑层 → 模型层 → 数据库 → 响应序列化 → 浏览器
:::
本节以 `POST /api/example/add` 添加案例为例,逐步拆解完整链路。
## 第一阶段:网络层
### 1. 浏览器发起请求
```typescript
// 前端 Axios 调用(ui/src/api/tool/example.ts)
export function exampleAdd(data: any) {
return http.request({
url: '/example/add',
method: 'POST',
data, // { name: '测试案例', status: 1, sort: 0 }
});
}
```
前端通过 Axios 发起 HTTP POST 请求,携带:
- `Authorization: Bearer <JWT Token>` 请求头(由请求拦截器自动添加)
- `Content-Type: application/json` 请求头
- JSON 请求体
### 2. Nginx 反向代理(生产环境)
```nginx
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
开发环境下,Vite 开发服务器将 `/api` 请求代理到后端:
```typescript
// ui/vite.config.ts
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
}
```
## 第二阶段:路由解析
### 3. ThinkPHP 入口
```text
public/index.php → 引导框架 → 路由解析 route/app.php
```
### 4. 路由匹配
```php
// route/app.php
// 公开接口(无需认证)
Route::group('api', function () {
Route::post('login', 'LoginController/login');
Route::get('captcha', 'LoginController/captcha');
});
// 需认证接口
Route::group('api', function () {
Route::group('example', function () {
Route::post('add', 'ExampleController/add'); // ← 匹配到此处
});
})->middleware([
\app\middleware\AuthMiddleware::class, // JWT 认证 + 权限校验
\app\middleware\TenantMiddleware::class, // 租户上下文
\app\middleware\DemoMiddleware::class, // 演示环境拦截
\app\middleware\LogMiddleware::class, // 操作日志
]);
```
路由匹配到 `POST api/example/add` → `ExampleController::add()`,同时绑定四个中间件。
## 第三阶段:中间件链(按顺序执行)
### 5. CorsMiddleware(全局中间件)
```php
// app/middleware/CorsMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
// 处理 OPTIONS 预检请求
if ($request->isOptions()) {
$response = Response::create('', 'html', 204);
} else {
$response = $next($request);
}
// 设置跨域响应头
$response->header([
'Access-Control-Allow-Origin' => $origin,
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, ...',
'Access-Control-Allow-Credentials' => 'true',
]);
return $response;
}
```
- 每个请求都经过(全局中间件)
- OPTIONS 预检请求直接返回 204
- 普通请求附加 CORS 响应头
### 6. AuthMiddleware(JWT 认证 + 权限校验)
```php
// app/middleware/AuthMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
// 1. 检查排除路由(login/captcha 等直接放行)
$path = $request->pathinfo();
foreach ($exceptList as $except) {
if (strpos($path, $except) !== false) {
return $next($request);
}
}
// 2. 从请求头获取 Token
$token = Jwt::getTokenFromHeader();
if (empty($token)) {
return json(['code' => 401, 'msg' => '未提供认证令牌']);
}
// 3. 解析 Token,校验签名和有效期
try {
$decoded = Jwt::decode($token);
} catch (\RuntimeException $e) {
return json(['code' => 401, 'msg' => $e->getMessage()]);
}
// 4. 校验 token 类型必须为 access
if (($decoded->type ?? '') !== 'access') {
return json(['code' => 401, 'msg' => '无效的认证令牌']);
}
// 5. 注入用户信息到 Request
$request->userInfo = $decoded;
// 6. 读取 #[Permission] 注解,校验权限
$permission = AttributeService::getPermission($controllerClass, $action);
if ($permission !== null) {
$hasPermission = $this->checkPermission($decoded->uid, $permission->code);
if (!$hasPermission) {
return json(['code' => 403, 'msg' => '无访问权限']);
}
}
return $next($request);
}
```
处理流程:
```text
请求进入
│
├─ 路径在排除列表? → 直接放行
│
├─ 提取 Authorization: Bearer <token>
│ └─ 为空 → 返回 401
│
├─ Jwt::decode($token) 解析验证
│ └─ 失败(过期/签名错误)→ 返回 401
│
├─ 校验 type === 'access'
│ └─ 不是 → 返回 401
│
├─ 注入 $request->userInfo = {uid, username, type, exp}
│
├─ 读取 #[Permission] 注解
│ ├─ 无注解 → 跳过权限校验
│ └─ 有注解 → checkPermission()
│ ├─ uid === 1 → 直接放行(超级管理员)
│ └─ 查询 UserRole → RoleMenu → Menu.permission
│ ├─ 包含 → 放行
│ └─ 不包含 → 返回 403
│
└─ 放行 → 进入下一个中间件
```
### 7. TenantMiddleware(租户上下文)
```php
// app/middleware/TenantMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
$userInfo = $request->userInfo ?? null;
$userId = $userInfo->uid ?? 0;
$user = User::find($userId);
$tenantId = $user->tenant_id ?? 0;
// 注入租户上下文
$request->tenantId = $tenantId;
$request->isSuperAdmin = ($tenantId == 0);
// 非超级管理员校验租户状态
if ($tenantId > 0) {
$tenant = Tenant::find($tenantId);
if (!$tenant || $tenant->status != 1) {
return json(['code' => 403, 'msg' => '租户已被禁用']);
}
if ($tenant->expire_time && strtotime($tenant->expire_time) < time()) {
return json(['code' => 403, 'msg' => '租户已过期']);
}
$request->tenantInfo = $tenant;
}
return $next($request);
}
```
注入内容:
- `$request->tenantId` — 租户 ID(0=超级管理员)
- `$request->isSuperAdmin` — 是否超级管理员
- `$request->tenantInfo` — 租户信息对象
### 8. DemoMiddleware(演示环境拦截)
```php
// app/middleware/DemoMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
// 非演示环境直接放行
if (!env('app_demo', false)) {
return $next($request);
}
// GET 请求放行(读操作)
if (strtoupper($request->method()) === 'GET') {
return $next($request);
}
// 检查 #[DemoAllow] 注解
if (AttributeService::hasDemoAllow($controllerClass, $action)) {
return $next($request);
}
// 拦截写操作
return json(['code' => 403, 'msg' => '演示环境,禁止操作']);
}
```
### 9. LogMiddleware(操作日志)
```php
// app/middleware/LogMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
$startTime = microtime(true);
// 执行后续中间件和控制器
$response = $next($request);
// 写操作始终记录
if (in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH'])) {
// 读取 #[Log] 注解
$logAttr = AttributeService::getLog($controllerClass, $action);
// 组装日志数据
$logData = [
'title' => $logAttr?->title ?: $path,
'type' => $logAttr?->type ?: $this->guessType($method),
'method' => $method,
'url' => $request->url(),
'param' => json_encode($request->param()),
'ip' => $requestInfo->getIp(),
'location' => $requestInfo->getIpLocation(),
'os' => $requestInfo->getOs(),
'browser' => $requestInfo->getBrowser(),
'consume_time'=> round((microtime(true) - $startTime) * 1000),
'status' => $this->detectStatus($response->getContent()),
];
// 写入数据库(失败不影响主业务)
OperationLog::create($logData);
}
return $response;
}
```
## 第四阶段:控制器层
### 10. ExampleController::add()
```php
// app/controller/ExampleController.php
#[Log('案例演示-新增记录', Log::TYPE_ADD, '新增案例演示:{name}')]
#[Permission('sys:example:add', '添加案例演示')]
public function add(): Json
{
// getJsonBody() 获取前端 POST 提交的 JSON 数据
// _add() 内部调用 $this->logic->add($data)
return parent::_add($this->logic, $this->getJsonBody());
}
```
控制器层职责:
- 通过 `#[Permission]` 声明所需权限
- 通过 `#[Log]` 声明日志信息
- 获取参数,调用 Logic 层,返回响应
## 第五阶段:逻辑层
### 11. ExampleLogic::add()
```php
// app/logic/ExampleLogic.php → 继承 BaseLogic
// BaseLogic::add() 的完整流程:
public function add(array $data): int
{
// 1. 钩子:新增前(可修改数据,抛异常可拦截)
$data = $this->beforeAdd($data);
// 2. 保留原始数据供 afterAdd 使用
$originalData = $data;
// 3. 入库前转换:camelCase → snake_case
$data = array_camel_to_snake($data);
// 4. 租户ID自动填充
$data = $this->processTenantId($data);
// 5. 文件字段:迁移临时文件,去掉域名
$data = $this->processFileFieldsOnSave($data);
// 6. 富文本字段:迁移临时文件
$data = $this->processContentFieldsOnSave($data);
// 7. 过滤非数据库字段
$saveData = $this->filterTableFields($data);
// 8. 唯一性校验
$this->checkUnique($saveData);
// 9. 写入数据库
$model = $this->getModel()->create($saveData);
$id = $model->id;
// 10. 钩子:新增后
$this->afterAdd($id, $originalData);
return $id;
}
```
## 第六阶段:模型层
### 12. BaseModel 自动处理
```php
// app/model/Example.php
class Example extends BaseModel
{
protected $name = 'example';
}
// BaseModel 提供的能力:
// 1. 全局查询范围 soft_delete → WHERE is_delete = 0
// 2. onBeforeInsert → 自动填充 create_user, create_time
// 3. onBeforeUpdate → 自动填充 update_user, update_time
```
### 13. 数据库执行
ThinkPHP ORM 将操作转换为 SQL:
```sql
INSERT INTO think_example (name, status, sort, avatar, create_user, create_time)
VALUES ('测试案例', 1, 0, '', 'admin', '2026-09-20 12:00:00');
```
## 第七阶段:响应返回
### 14. 响应序列化
```php
// BaseController::_add()
protected function _add(BaseLogic $logic, array $data): Json
{
$id = $logic->add($data);
return $this->success(['id' => $id], '添加成功');
}
// Result::success()
public static function success($data, string $msg = '操作成功'): Json
{
return json([
'code' => 0,
'ok' => true,
'msg' => $msg,
'data' => self::convertData($data), // snake_case → camelCase
]);
}
```
### 15. 浏览器接收响应
```json
{
"code": 0,
"ok": true,
"msg": "添加成功",
"data": {
"id": 42
}
}
```
前端 Axios 拦截器统一处理:`code === 0` 时展示成功提示,否则展示错误信息。
## 完整链路流程图
```text
[浏览器]
│ POST /api/example/add
│ Authorization: Bearer <token>
▼
[Nginx / Vite Proxy] ── 反向代理转发
│
▼
[ThinkPHP 入口] ── public/index.php
│
▼
[路由解析] ── route/app.php 匹配 api/example/add
│
▼
[CorsMiddleware] ── 跨域校验通过(全局)
│
▼
[AuthMiddleware]
├─ 提取 Token → Jwt::decode()
├─ 校验 type === 'access'
├─ 注入 $request->userInfo
├─ 读取 #[Permission] 注解
├─ checkPermission() 权限校验
│ ├─ uid=1 → 跳过(超级管理员)
│ └─ UserRole → RoleMenu → Menu.permission
└─ 放行
│
▼
[TenantMiddleware]
├─ 查询用户 tenant_id
├─ 注入 $request->tenantId
└─ 校验租户状态(禁用/过期 → 403)
│
▼
[DemoMiddleware]
├─ 非演示环境 → 放行
├─ GET 请求 → 放行
└─ 有 #[DemoAllow] → 放行
│
▼
[LogMiddleware] ── 记录开始时间
│
▼
[ExampleController::add()]
├─ #[Log] 注解元数据
├─ #[Permission] 注解元数据
└─ parent::_add($this->logic, $data)
│
▼
[ExampleLogic::add()]
├─ beforeAdd() 钩子
├─ array_camel_to_snake() 键名转换
├─ processTenantId() 租户填充
├─ processFileFieldsOnSave() 文件处理
├─ filterTableFields() 字段过滤
├─ checkUnique() 唯一性校验
├─ Model::create() 写入数据库
└─ afterAdd() 钩子
│
▼
[BaseModel]
├─ onBeforeInsert → create_user, create_time
└─ 执行 SQL INSERT
│
▼
[数据库] ── 返回自增 ID
│
▼ 逐层返回响应
[Result::success()] ── {code:0, ok:true, msg:"添加成功", data:{id:42}}
│
▼
[LogMiddleware] ── 记录操作日志(写操作)
│
▼
[浏览器] ← JSON 响应
```
## 异常处理链路
当链路中发生异常时,ExceptionHandle 统一捕获并返回标准响应:
| 异常类型 | 触发场景 | 响应 |
|---------|---------|------|
| `ValidateException` | 参数验证失败 | `{code:1, msg:"参数验证失败", data:{错误明细}}` |
| `ModelNotFoundException` | 数据不存在 | `{code:1, msg:"数据不存在"}` |
| `RuntimeException (401)` | JWT 过期/无效 | `{code:1, msg:"token已过期"}` |
| `HttpException` | HTTP 错误 | `{code:1, msg:"请求错误"}` |
| `Exception` | 未捕获异常 | `{code:1, msg:"操作失败"}`(生产环境隐藏详情) |
:::warning 异常处理原则
所有异常均返回 HTTP 200 状态码,通过响应体 `code` 字段区分业务成功(0)与失败(1/401/403/404/422)。前端统一按 `code` 判断,无需处理 HTTP 状态码差异。
:::
## 总结
一个 API 请求从浏览器到数据库经过 7 个阶段、15+ 个处理节点。中间件链负责横切关注点(认证、权限、租户、日志),控制器层负责参数获取和响应返回,逻辑层负责业务处理和数据加工,模型层负责数据库操作。这种分层设计使得每一层职责单一、可独立测试。
@@ -0,0 +1,472 @@
# 4.10 安全架构设计
:::tip 概述
本章详细阐述系统的安全架构设计,围绕认证、授权、防护、审计四大安全领域展开,旨在帮助开发者全面理解系统的安全保障体系。
:::
**认证(Authentication)**:系统采用基于 JWT 的无状态认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 `Authorization: Bearer <token>` 头传递,由 `AuthMiddleware` 统一拦截并验证 Token 的有效性(签名校验、有效期检查)。
**授权(Authorization)**:基于 RBAC 模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 `sys:user:add`),由 `#[Permission]` 注解在控制器层声明,`AuthMiddleware` 统一校验。管理员(`uid=1`)自动放行。
**防护(Protection)**:系统从多维度构建安全防护体系。参数校验通过 Validate 自动拦截非法输入;敏感密码采用 bcrypt 双重加盐哈希存储;演示模式下 `DemoMiddleware` 统一拦截写操作;跨域配置精确控制可信域名。
**审计(Audit)**:系统提供完备的操作审计能力。`LogMiddleware` 自动记录所有写操作的请求路径、参数、操作人、IP 地址及执行结果;登录日志独立记录每次登录尝试,便于安全事件追溯。
:::tip 安全设计原则
- **纵深防御**:认证、授权、参数校验多层防护,单一防线失效时仍有其他机制兜底
- **最小权限**:用户仅能访问已授权接口,角色权限按需分配
- **数据不落地**:敏感信息(Token、密码)不在日志中明文记录,密码哈希不可逆。**注意**:当前 LogMiddleware 会记录完整请求参数,敏感字段(如密码)建议在业务层脱敏后再传入
- **安全默认**:框架默认配置为安全模式(DEBUG 关闭、错误信息隐藏)
:::
## 安全架构总览
```text
┌─────────────────────────────────────────────────────────────────┐
│ 安全防御层次 │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 网络层防护 │ │
│ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ CORS │ │ 上传体积限制 │ │ │
│ │ │ 跨域控制 │ │ DoS 防御 │ │ │
│ │ └──────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 认证层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ JWT 认证 │ │ 验证码 │ │ 双令牌 │ │ │
│ │ │ Bearer │ │ 图形验证 │ │ access+ │ │ │
│ │ │ │ │ │ │ refresh │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 授权层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ RBAC 权限 │ │ 演示模式保护 │ │ 超级管理员跳过 │ │ │
│ │ │ #[Permission]│ │ DemoMiddleware│ │ uid=1 bypass │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 数据层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ 密码加密 │ │ 参数校验 │ │ SQL 注入 │ │ 操作日志 │ │ │
│ │ │ bcrypt │ │ Validate │ │ ORM 防护 │ │ 审计追踪 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## 一、认证机制
### 登录流程
```text
1. 前端提交 { username, password, code, key }
│
▼
2. CaptchaService::check(code, key) ← 验证码校验
│ 失败 → 返回"验证码错误"
▼
3. User::where('username', $username) ← 查询用户
│ 不存在 → 统一提示"用户名或密码错误"
▼
4. password_verify($password . $salt, $hashed) ← bcrypt 校验
│ 不匹配 → 统一提示"用户名或密码错误"
▼
5. 检查 user.status == 1 ← 账号状态
│ 非启用 → 返回"账号已被禁用"
▼
6. JwtService::createTokenPair($uid) ← 签发双令牌
│
▼
7. 返回 { access_token, refresh_token, expires_in }
```
### JWT 双令牌认证
```html
[前端] [后端]
│ │
│── POST /api/login ────→│
│ {username, password} │
│ │── 验证码校验
│ │── 密码校验(bcrypt)
│ │── 签发 access_token + refresh_token
│←─ {access_token, ───────│
│ refresh_token} │
│ │
│── GET /api/xxx ────────→│
│ Authorization: │
│ Bearer <access_token> │
│ │── 解析 JWT
│ │── 校验签名 + 有效期
│ │── 注入 userInfo
│←─ {data} ──────────────│
│ │
│── Token 过期 ──────────→│
│ │
│── POST /api/oauth2/ ──→│
│ token │
│ {refresh_token} │── 验证 refresh 类型
│ │── 签发新令牌对
│←─ {new_access_token, ──│
│ new_refresh_token} │
```
### Token 设计
| Token 类型 | 有效期 | 用途 | payload.type |
|-----------|--------|------|-------------|
| access_token | 2 小时 | 接口认证 | `access` |
| refresh_token | 7 天 | 刷新令牌 | `refresh` |
### 安全要点
1. **密码错误统一提示**:无论用户不存在还是密码错误,统一提示"用户名或密码错误",避免暴露账号是否存在
2. **账号状态校验**:`status != 1` 时禁止登录
3. **Token 类型校验**:access_token 和 refresh_token 通过 `type` 字段区分,不可混用
4. **密钥安全**:JWT 密钥通过 `.env` 配置,生产环境务必修改为强随机字符串
## 二、授权机制
### RBAC 权限模型
```typescript
┌─────────┐ ┌────────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐
│ User │───►│ UserRole │───►│ Role │───►│ RoleMenu │───►│ Menu │
│ │ │ │ │ │ │ │ │ │
│ id │ │ user_id │ │ id │ │ role_id │ │ id │
│ username│ │ role_id │ │ name │ │ menu_id │ │ perm │
└─────────┘ └────────────┘ │data_scope│ └────────────┘ │ type │
└─────────┘ └─────────┘
```
### 权限校验流程
```php
// AuthMiddleware::checkPermission()
protected function checkPermission(int $uid, string $permissionCode): bool
{
// 1. 超级管理员(uid=1)跳过校验
if ($uid === 1) {
return true;
}
// 2. 查询用户角色
$roleIds = UserRole::where('user_id', $uid)->column('role_id');
if (empty($roleIds)) {
return false;
}
// 3. 通过角色关联获取权限节点
$permissions = RoleMenu::alias('rm')
->join('think_menu m', 'rm.menu_id = m.id')
->whereIn('rm.role_id', $roleIds)
->where('m.is_delete', 0)
->where('m.type', 1) // type=1 为权限节点
->column('m.permission');
// 4. 判断是否包含目标权限
return in_array($permissionCode, $permissions);
}
```
### 数据权限
| data_scope | 范围 | 实现 |
|-----------|------|------|
| 1 | 全部数据 | 不过滤 |
| 2 | 本部门数据 | WHERE dept_id = 用户部门ID |
| 3 | 仅本人数据 | WHERE create_user = 用户名 |
## 三、防护机制
### 密码加密(bcrypt 双重加盐)
```text
密码存储流程:
原始密码 + 外部 salt(10位随机字符串)
│
▼ password_hash($combined, PASSWORD_BCRYPT, ['cost' => 12])
│
▼ bcrypt 内部自动生成 salt 并嵌入哈希
│
▼ 存储:password 字段(60字符)+ salt 字段(10字符)
密码校验流程:
用户输入密码 + 数据库 salt
│
▼ password_verify($input . $salt, $hashed)
│
▼ true/false
```
```php
// 加密
$password = '123456';
$salt = 'xK7pMn3qRt'; // 外部盐(10位随机)
$combined = $password . $salt; // 拼接
$hashed = password_hash($combined, PASSWORD_BCRYPT, ['cost' => 12]);
// 结果:$2y$12$...(60字符)
// 校验
$inputCombined = $userInput . $dbSalt;
$isValid = password_verify($inputCombined, $dbHashed);
```
:::tip 为什么双重加盐?
外部 salt 存储在独立字段,即使数据库泄露,攻击者也需要同时获取 salt 才能进行暴力破解。bcrypt 内部 salt 进一步增加彩虹表攻击难度。
:::
### 参数校验(自动拦截)
通过 Logic 层 `$validateClass` 配置,add/update 时自动校验参数格式:
```php
class UserValidate extends Validate
{
protected $rule = [
'username' => 'require|unique:user',
'realname' => 'require',
'mobile' => 'mobile',
'email' => 'email',
];
protected $scene = [
'add' => ['username', 'realname', 'mobile', 'email'],
'update' => ['realname', 'mobile', 'email'],
];
}
```
校验失败直接返回 422,不进入业务逻辑层。
### SQL 注入防护
ThinkPHP ORM 使用参数绑定,自动防止 SQL 注入:
```php
// ✅ ORM 查询(安全,自动参数绑定)
User::where('username', $username)->find();
// 生成:SELECT * FROM think_user WHERE username = ? [$username]
// ✅ 原生查询(手动绑定参数)
Db::query("SELECT * FROM think_user WHERE username = ?", [$username]);
// ❌ 危险!字符串拼接(禁止)
Db::query("SELECT * FROM think_user WHERE username = '$username'");
```
:::warning 安全红线
永远不要将用户输入直接拼接到 SQL 字符串中。即使使用原生 SQL,也必须使用参数绑定(`?` 占位符)。
:::
### 文件上传安全
```text
上传文件安全检查链:
│
▼ 1. 文件大小限制(config/file.php → max_size,默认 10MB)
│
▼ 2. 文件扩展名白名单(config/file.php → allow_ext)
│
▼ 3. MIME 类型检查(fileinfo 扩展)
│
▼ 4. 生成随机文件名(防止路径遍历攻击)
│
▼ 5. 存储到 Web 根目录之外(或 Nginx 禁止执行 PHP)
```
```ini
# Nginx 配置:上传目录禁止执行 PHP
location ~* /uploads/.*\.php$ {
deny all;
}
```
### XSS 防护
```text
防护措施:
1. 前端:用户输入转义显示(Vue 默认转义 {{ }} 插值)
2. 后端:富文本内容通过 save_content() 处理,过滤危险标签
3. 响应头:Content-Type: application/json(非 text/html)
```
### 演示模式保护
```php
// DemoMiddleware 拦截写操作
// .env: [APP] DEMO = true
// 受保护:POST/PUT/DELETE 请求 → 返回 403
// 放行:GET 请求 + 标注 #[DemoAllow] 的方法
```
```php
// 标注 #[DemoAllow] 的方法在演示模式下仍可执行
#[DemoAllow]
public function login(): Json { ... }
```
### CORS 跨域控制
```ini
; .env
; 生产环境:限制具体域名
[CORS]
ALLOW_ORIGIN = https://admin.example.com
; 开发环境:允许所有(仅限开发)
[CORS]
ALLOW_ORIGIN = *
```
```php
// CorsMiddleware 处理逻辑:
// 1. OPTIONS 预检请求 → 直接返回 204 + CORS 头
// 2. 普通请求 → 校验 Origin 是否在白名单
// 3. 设置 Access-Control-Allow-Origin / Allow-Methods / Allow-Headers
```
## 四、审计机制
### 操作日志(think_operation_log)
`LogMiddleware` 基于 `#[Log]` 注解自动记录所有写操作:
```php
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
public function add(): Json { ... }
```
**记录字段:**
| 字段 | 说明 | 示例 |
|------|------|------|
| `title` | 操作标题(来自注解) | 用户管理-新增记录 |
| `type` | 操作类型(常量) | 1=新增 2=修改 3=删除 4=查询 5=导入 6=导出 |
| `method` | HTTP 方法 | POST |
| `url` | 请求路径 | /api/user/add |
| `params` | 请求参数(JSON) | {"username":"admin"} |
| `ip` | 客户端 IP | 192.168.1.100 |
| `location` | IP 归属地 | 江苏省南京市 |
| `os` | 操作系统 | Windows 10 |
| `browser` | 浏览器 | Chrome 120 |
| `username` | 操作人 | admin |
| `status` | 执行状态 | 0=成功 1=失败 |
| `error_msg` | 错误信息(失败时) | 用户名已存在 |
| `duration` | 执行耗时(ms) | 125 |
| `create_time` | 操作时间 | 2026-09-22 10:30:00 |
**记录规则:**
- 写操作(POST/PUT/DELETE)始终记录
- GET 查询操作仅在 `#[Log]` 指定 `TYPE_QUERY` 时记录
- 排除列表:`config/middleware.php` → `log_except` 中的路径不记录
### 登录日志(think_login_log)
```php
// LoginLogic 中独立记录
$requestInfo = RequestInfoService::create(request());
LoginLog::create([
'username' => $username,
'type' => 1, // 1=登录 2=登出
'status' => $success ? 0 : 1, // 0=成功 1=失败
'message' => $msg,
'ip' => $requestInfo->getIp(),
'location' => $requestInfo->getIpLocation(),
'os' => $requestInfo->getOs(),
'browser' => $requestInfo->getBrowser(),
]);
```
**记录字段:**
| 字段 | 说明 |
|------|------|
| `username` | 登录用户名(无论成功失败都记录) |
| `type` | 1=登录 2=登出 |
| `status` | 0=成功 1=失败 |
| `message` | 结果消息(如"登录成功"、"密码错误") |
| `ip` | 客户端 IP |
| `location` | IP 归属地 |
| `os` | 操作系统 |
| `browser` | 浏览器 |
### 日志安全要点
| 要点 | 说明 |
|------|------|
| 密码不记录 | 登录日志不记录密码字段 |
| 参数可脱敏 | 敏感字段建议在 Controller 层处理后再传入 Logic |
| 独立存储 | 操作日志和登录日志分别存储在不同表 |
| 不可篡改 | 日志仅 INSERT,无 UPDATE/DELETE 接口 |
## 五、安全配置清单
### 环境变量(.env)
| 配置项 | 开发环境 | 生产环境 | 说明 |
|--------|---------|---------|------|
| `APP_DEBUG` | `true` | **`false`** | 关闭调试模式,隐藏详细错误 |
| `SHOW_ERROR_MSG` | `true` | **`false`** | 隐藏原始错误信息 |
| `JWT.SECRET` | 默认值 | **强随机密钥** | 32 字节以上随机字符串 |
| `CORS.ALLOW_ORIGIN` | `*` | **指定域名** | 如 `https://admin.example.com` |
| `APP.DEMO` | `false` | `false` | 演示环境按需开启 |
| `FILE.MAX_SIZE` | 10MB | 合理限制 | 防止大文件 DoS |
| `API.CAMEL_SNAKE_CONVERT` | `true` | `true` | 驼峰转换开关 |
### Nginx 配置
```nginx
# 1. 启用 HTTPS
server {
listen 443 ssl;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
}
# 2. 上传目录禁止执行 PHP
location ~* /uploads/.*\.php$ {
deny all;
}
# 3. 禁止访问隐藏文件
location ~ /\. {
deny all;
}
# 4. 安全响应头
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy strict-origin-when-cross-origin;
```
### 安全检查清单
```text
上线前逐项检查:
□ .env 中 APP_DEBUG = false
□ .env 中 SHOW_ERROR_MSG = false
□ .env 中 JWT.SECRET 已更换为强随机密钥
□ .env 中 CORS.ALLOW_ORIGIN 指定具体域名
□ Nginx 已配置 HTTPS
□ Nginx 上传目录已禁止执行 PHP
□ 数据库密码为强密码
□ Redis 已设置密码(如使用)
□ 默认管理员密码已修改
□ 演示模式已关闭(除非需要)
□ 日志中不包含密码等敏感信息
```
## 总结
系统通过认证、授权、防护、审计四层安全架构,实现了从网络层到数据层的纵深防御。JWT 双令牌认证保证无状态安全,RBAC 权限控制细化到按钮级,bcrypt 双重加盐保护密码安全,操作日志实现全链路审计。各层安全机制相互独立又协同配合,确保系统在企业级应用场景下的安全可靠性。