签入版本
This commit is contained in:
@@ -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 双重加盐保护密码安全,操作日志实现全链路审计。各层安全机制相互独立又协同配合,确保系统在企业级应用场景下的安全可靠性。
|
||||
Reference in New Issue
Block a user