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

537 lines
16 KiB
Markdown
Raw Blame History

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