430 lines
16 KiB
Markdown
430 lines
16 KiB
Markdown
# 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 | ❌ 禁止,只做格式校验 |
|