Skip to content

4.7 横切机制 ​

概述

横切机制是 BaseLogic/BaseModel 内置的自动处理能力,开发者通过属性配置启用,无需手动编写处理代码。项目共有 9 种横切机制,覆盖数据转换、文件处理、校验、权限、隔离等维度。

横切机制总览 ​

机制配置位置配置属性触发时机
驼峰/下划线转换config/api.phpcamel_snake_convert请求入、响应出
文件字段处理LogicfileFields + fileSaveDiradd/update 写入,detail/page 读取
多文件字段处理LogicmultiFileFields + fileSaveDiradd/update 写入,detail/page 读取
富文本占位符LogiccontentFieldsadd/update 写入,detail/page 读取
枚举显示名翻译LogicserializeMapsdetail/page 查询后
自动参数校验LogicvalidateClassadd/update 前
唯一性校验LogicuniqueFieldsadd/update 时
数据权限过滤LogicdataScopeUserField / 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   # 关闭(请求保持驼峰,响应保持下划线)

开发者无需手动处理

驼峰/下划线转换完全自动,开发者只需在前端用 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 = 当前用户名

无角色用户

无角色时默认按"仅本人"(scope=3)处理。

9. 租户隔离 ​

配置方式:继承 BaseTenantLogic(自动设置 tenantScopeField = 'tenant_id')

php
class ArticleLogic extends BaseTenantLogic  // 继承 BaseTenantLogic 即启用
{
    // tenantScopeField 已由基类设为 'tenant_id',无需再声明
}

隔离行为 ​

场景行为实现方法
查询时自动叠加 WHERE tenant_id = 当前租户IDapplyTenantScope()
新增时自动填充 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_timedate('Y-m-d H:i:s')
onBeforeUpdate(更新前)update_user$request->userInfo->username
onBeforeUpdate(更新前)update_timedate('Y-m-d H:i:s')(始终覆盖)

无用户场景

控制台任务、定时任务等无登录用户的场景下,跳过用户字段,时间字段仍写入。

数据库公共字段 ​

所有业务表必须包含以下字段:

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               ← 钩子

小蚂蚁云团队 · 提供技术支持