# 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 {"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 | ❌ 禁止,只做格式校验 |