签入版本
This commit is contained in:
@@ -1,46 +1,45 @@
|
||||
# 4.3 后端分层架构
|
||||
|
||||
:::tip 概述
|
||||
后端采用"三层主干 + 三层支撑"架构:Controller → Logic → Model 构成主干链路,Validate、Service、Middleware 分别提供参数校验、通用能力、请求拦截等支撑。各层职责明确,层间通过方法调用串联,不越层访问。
|
||||
后端采用"三层主干 + 三层支撑"架构:Controller → Service → Model 构成主干链路,FormRequest、横切服务、Middleware 分别提供参数校验、通用能力、请求拦截等支撑。各层职责明确,层间通过方法调用串联,不越层访问。
|
||||
:::
|
||||
|
||||
## 架构全景
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 支撑层 │
|
||||
│ 支撑层 │
|
||||
│ │
|
||||
│ Validate(参数校验) Service(通用能力) Middleware(请求拦截) │
|
||||
│ app/validate/*.php app/service/*.php app/middleware/*.php │
|
||||
│ FormRequest(参数校验) 横切 Service(通用能力) Middleware(请求拦截)│
|
||||
│ Http/Requests/*.php Services/(Jwt/Dict…) Http/Middleware/ │
|
||||
└──────────────────────────────┬──────────────────────────────────────┘
|
||||
│ 被主干层调用
|
||||
│ 被主干层调用 / 前置拦截
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 主干链路 │
|
||||
│ 主干链路 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Controller(控制器层) │ │
|
||||
│ │ 接收请求 → 获取参数 → 调用 Logic → 封装响应 │ │
|
||||
│ │ 基类:BaseController | 文件:app/controller/*.php │ │
|
||||
│ │ 注解标注 → 参数注入 → 调用 Service → 封装响应 │ │
|
||||
│ │ 基类:Controller | 文件:app/Http/Controllers/Api/*.php │ │
|
||||
│ └──────────────────────────┬──────────────────────────────────┘ │
|
||||
│ │ 调用 Logic 方法 │
|
||||
│ │ 调用 Service 方法 │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Logic(逻辑层) │ │
|
||||
│ │ 属性配置 → 参数校验 → 生命周期钩子 → 横切处理 → 调用 Model │ │
|
||||
│ │ 基类:BaseLogic / BaseTenantLogic | 文件:app/logic/*.php │ │
|
||||
│ │ Service(服务层) │ │
|
||||
│ │ 属性配置 → 唯一性校验 → 生命周期钩子 → 事务 → 调用 Model │ │
|
||||
│ │ 基类:BaseService / BaseTenantService | Services/*.php │ │
|
||||
│ └──────────────────────────┬──────────────────────────────────┘ │
|
||||
│ │ 调用 Model ORM │
|
||||
│ │ 调用 Eloquent ORM │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Model(模型层) │ │
|
||||
│ │ 表映射 → 软删除 → 审计字段 → ORM → SQL │ │
|
||||
│ │ 基类:BaseModel | 文件:app/model/*.php │ │
|
||||
│ │ 表映射 → 软删除 → 审计字段 → 关联关系 → ORM → SQL │ │
|
||||
│ │ Trait:SoftDeletesByFlag / HasAuditColumns | Models/*.php │ │
|
||||
│ └──────────────────────────┬──────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Database(数据库) │ │
|
||||
│ │ Database(五种数据库:MySQL / PG / SQL Server / Oracle / SQLite)│
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -49,117 +48,68 @@
|
||||
|
||||
### Controller 层
|
||||
|
||||
**文件**:`app/controller/{Module}Controller.php`
|
||||
**基类**:`BaseController`
|
||||
**职责**:HTTP 请求入口,只做"搬运"不做"判断"
|
||||
**文件**:`app/Http/Controllers/Api/{Module}Controller.php`
|
||||
**基类**:`App\Http\Controllers\Controller`
|
||||
**职责**:HTTP 请求入口,只做"编排"不做"判断"
|
||||
|
||||
| 职责 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 参数获取 | 从请求中提取参数 | `$this->getJsonBody()`、`$this->getParams()` |
|
||||
| 调用 Logic | 委托业务逻辑 | `$this->logic->add($data)` |
|
||||
| 响应封装 | 统一 JSON 格式返回 | `$this->success()`、`$this->fail()` |
|
||||
| 注解标注 | 声明权限和日志 | `#[Permission]`、`#[Log]` |
|
||||
| 参数注入 | 类型注入 FormRequest 完成格式校验 | `add(ExampleRequest $request)` |
|
||||
| 调用 Service | 委托业务逻辑(快捷方法一行调用) | `$this->_add($service, $data)` |
|
||||
| 响应封装 | 统一 JSON 格式返回 | `$this->success()` / `$this->fail()` |
|
||||
| 注解标注 | 声明权限和日志 | `#[Permission]`、`#[OperationLog]` |
|
||||
|
||||
**Controller 不应该做:** 业务判断、数据库操作、文件处理、参数校验(特殊场景除外)。
|
||||
**Controller 不应该做**:业务判断、数据库操作、文件处理、重复的格式校验。
|
||||
|
||||
```php
|
||||
class ArticleController extends BaseController
|
||||
class ExampleController extends Controller
|
||||
{
|
||||
protected ArticleLogic $logic;
|
||||
public function __construct(protected ExampleService $exampleService) {}
|
||||
|
||||
protected function initialize(): void
|
||||
// 标准 CRUD —— 一行调用(构造注入,无需 initialize)
|
||||
#[OperationLog('示例管理-查询分页', OperationLog::TYPE_QUERY)]
|
||||
#[Permission('sys:example:page', '示例分页')]
|
||||
public function page(): JsonResponse
|
||||
{
|
||||
$this->logic = new ArticleLogic();
|
||||
return $this->_page($this->exampleService);
|
||||
}
|
||||
|
||||
// 标准 CRUD — 一行调用
|
||||
#[Permission('sys:article:page', '文章分页')]
|
||||
public function page(): Json
|
||||
#[OperationLog('示例管理-新增', OperationLog::TYPE_ADD, '新增示例:{name}')]
|
||||
#[Permission('sys:example:add', '添加示例')]
|
||||
public function add(ExampleRequest $request): JsonResponse
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
}
|
||||
|
||||
#[Permission('sys:article:add', '添加文章')]
|
||||
public function add(): Json
|
||||
{
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
return $this->_add($this->exampleService, $request->validated());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logic 层
|
||||
### Service 层
|
||||
|
||||
**文件**:`app/logic/{Module}Logic.php`
|
||||
**基类**:`BaseLogic`(系统共享)/ `BaseTenantLogic`(租户隔离)
|
||||
**文件**:`app/Services/{Module}Service.php`
|
||||
**基类**:`BaseService`(系统共享)/ `BaseTenantService`(租户隔离)
|
||||
**职责**:业务核心,通过"属性配置 + 生命周期钩子"声明式 CRUD
|
||||
|
||||
| 职责 | 方式 | 说明 |
|
||||
|------|------|------|
|
||||
| 行为定义 | 属性配置 | `$pageLikeFields`、`$uniqueFields`、`$validateClass` 等 |
|
||||
| 业务定制 | 生命周期钩子 | `beforeAdd`、`afterUpdate`、`beforeDelete` 等 |
|
||||
| 自动校验 | `$validateClass` | add/update 时自动调用验证器 |
|
||||
| 横切处理 | 属性驱动 | 文件字段、富文本、枚举映射、数据权限、租户隔离 |
|
||||
| 行为定义 | 属性配置 | `$pageLikeFields`、`$uniqueFields`、`$serializeMaps` 等 16 个声明式属性 |
|
||||
| 业务定制 | 生命周期钩子 | `beforeAdd`、`afterUpdate`、`beforeDelete` 等 10 个钩子 |
|
||||
| 横切处理 | Trait 组合 | 文件字段、字典翻译、数据权限、租户隔离(6 个 Concerns Trait) |
|
||||
| 数据安全 | `DB::transaction` | add/update/delete/batchDelete 主流程事务包裹 |
|
||||
|
||||
**Logic 不应该做:** 直接 HTTP 交互、直接返回 JSON 响应。
|
||||
**Service 不应该做**:直接 HTTP 交互、直接返回 JSON 响应。
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
class ExampleService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Article::class;
|
||||
use HasCrudOperations, HasQueryScopes, HasSerializeMaps, HasUniqueCheck;
|
||||
|
||||
/**
|
||||
* 参数验证器类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $validateClass = ArticleValidate::class;
|
||||
protected string $modelClass = Example::class;
|
||||
protected array $pageLikeFields = ['name'];
|
||||
protected array $pageEqFields = ['status'];
|
||||
protected array $uniqueFields = ['name'];
|
||||
protected array $serializeMaps = ['status' => 'example_status'];
|
||||
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @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;
|
||||
@@ -170,23 +120,25 @@ class ArticleLogic extends BaseTenantLogic
|
||||
|
||||
### Model 层
|
||||
|
||||
**文件**:`app/model/{TableName}.php`
|
||||
**基类**:`BaseModel`
|
||||
**文件**:`app/Models/{TableName}.php`
|
||||
**基类**:Eloquent Model + Concerns Trait
|
||||
**职责**:数据持久化,只做表映射和自动维护
|
||||
|
||||
| 职责 | 说明 |
|
||||
|------|------|
|
||||
| 表映射 | `protected $name = 'article'` |
|
||||
| 软删除 | 全局查询范围自动过滤 `is_delete=1` |
|
||||
| 审计字段 | `onBeforeInsert`/`onBeforeUpdate` 自动填充 |
|
||||
| 关联关系 | `hasMany`、`belongsTo` 等 |
|
||||
| 表映射 | `protected $table = 'example'`(不含前缀,`DB_PREFIX` 自动拼接) |
|
||||
| 软删除 | `SoftDeletesByFlag` 注册全局范围自动过滤 `is_delete=1` |
|
||||
| 审计字段 | `HasAuditColumns` 的 `creating/updating` 事件自动填充 |
|
||||
| 关联关系 | `hasMany`、`belongsTo`、`belongsToMany` 等 |
|
||||
|
||||
**Model 不应该做:** 业务判断、复杂查询逻辑(交给 Logic)。
|
||||
**Model 不应该做**:业务判断、复杂查询编排(交给 Service)。
|
||||
|
||||
```php
|
||||
class Article extends BaseModel
|
||||
class Example extends Model
|
||||
{
|
||||
protected $name = 'article';
|
||||
use HasAuditColumns, SoftDeletesByFlag;
|
||||
|
||||
protected $table = 'example';
|
||||
|
||||
// 关联关系(按需定义)
|
||||
public function category()
|
||||
@@ -196,60 +148,59 @@ class Article extends BaseModel
|
||||
}
|
||||
```
|
||||
|
||||
### Validate 层
|
||||
### FormRequest 层(参数校验支撑)
|
||||
|
||||
**文件**:`app/validate/{Module}Validate.php`
|
||||
**基类**:`think\Validate`
|
||||
**职责**:参数格式校验,定义规则 + 场景
|
||||
**文件**:`app/Http/Requests/{Module}Request.php`
|
||||
**基类**:`BaseRequest`
|
||||
**职责**:参数**格式**校验(必填、类型、长度、枚举),不含业务规则
|
||||
|
||||
```php
|
||||
class ArticleValidate extends Validate
|
||||
class ExampleRequest extends BaseRequest
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:200',
|
||||
];
|
||||
protected $message = [
|
||||
'title.require' => '文章标题不能为空',
|
||||
];
|
||||
protected $scene = [
|
||||
'add' => ['title'],
|
||||
'update' => ['title'],
|
||||
];
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'nullable|integer|in:1,2,3,4',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**触发方式:** Logic 配置 `$validateClass` 后,add/update 自动调用。
|
||||
**触发方式**:控制器方法类型注入,方法执行前自动校验;失败由 `failedValidation()` 返回统一 JSON(`code=1` + 字段明细)。
|
||||
|
||||
### Service 层
|
||||
### 横切 Service(通用能力支撑)
|
||||
|
||||
**文件**:`app/service/{Name}Service.php`
|
||||
**文件**:`app/Services/{Name}Service.php`(横切类)
|
||||
**职责**:跨模块通用能力,无状态、无副作用
|
||||
|
||||
| 服务 | 能力 |
|
||||
|------|------|
|
||||
| `DictService` | 字典查询(两级缓存) |
|
||||
| `PasswordService` | 密码加密(bcrypt 双重加盐) |
|
||||
| `JwtService` | Token 生成/刷新/解析 |
|
||||
| `ExcelService` | Excel 导入导出 |
|
||||
| `CaptchaService` | 图片验证码 |
|
||||
| `JwtService` | 令牌签发/刷新/解析(Redis 有状态) |
|
||||
| `DictCacheService` | 字典查询(两级缓存) |
|
||||
| `PasswordService` | 密码加密(bcrypt,成本因子 12) |
|
||||
| `ExcelService` | Excel 导入导出(表头映射、分批) |
|
||||
| `CaptchaService` | 图片验证码(IP 限流) |
|
||||
| `RequestInfoService` | IP 归属地 / OS / 浏览器解析 |
|
||||
|
||||
**Service 不应该做:** 持有状态、直接操作数据库(通过 Model 静态方法例外)。
|
||||
**横切 Service 不应该做**:持有请求状态、直接返回响应。
|
||||
|
||||
### Middleware 层
|
||||
### Middleware 层(请求拦截支撑)
|
||||
|
||||
**文件**:`app/middleware/{Name}Middleware.php`
|
||||
**文件**:`app/Http/Middleware/*.php`
|
||||
**职责**:请求拦截,在 Controller 前后执行
|
||||
|
||||
```text
|
||||
CorsMiddleware → AuthMiddleware → TenantMiddleware → DemoMiddleware → LogMiddleware → Controller
|
||||
ProxyMiddleware → Cors → ForceJsonResponse → Authenticate → ResolveTenant → LogOperation → Controller
|
||||
```
|
||||
|
||||
| 中间件 | 作用 | 注入数据 |
|
||||
|--------|------|----------|
|
||||
| `AuthMiddleware` | JWT 认证 + 权限校验 | `$request->userInfo` |
|
||||
| `TenantMiddleware` | 租户上下文 | `$request->tenantId` |
|
||||
| `DemoMiddleware` | 演示环境拦截 | — |
|
||||
| `LogMiddleware` | 操作日志记录 | — |
|
||||
| `Authenticate` | JWT 认证 + 权限校验 | `attributes['userInfo']` |
|
||||
| `ResolveTenant` | 租户上下文 | `attributes['tenantId']` / `isSuperAdmin` / `tenantInfo` |
|
||||
| `LogOperation` | 操作日志(handle 采集,terminate 落库) | — |
|
||||
|
||||
详见 [4.2 请求处理链路](/zh/architecture/request-flow.html)。
|
||||
|
||||
## 各层方法对照表
|
||||
|
||||
@@ -257,36 +208,37 @@ CorsMiddleware → AuthMiddleware → TenantMiddleware → DemoMiddleware → Lo
|
||||
|
||||
| 层 | 方法 | 输入 | 输出 |
|
||||
|----|------|------|------|
|
||||
| Controller | `_index($logic)` | HTTP 请求参数 | Json 响应 |
|
||||
| Logic | `pageList($params, $where, $with)` | 参数数组 | `['records'=>[], 'total'=>int, ...]` |
|
||||
| Model | `Model::page($current, $size)->select()` | 分页条件 | Collection |
|
||||
| Controller | `_page($service)` | HTTP 请求参数 | JsonResponse |
|
||||
| Service | `pageList($params, $where, $with)` | 参数数组 | `['records'=>[], 'total'=>int, 'current'=>int, 'size'=>int]` |
|
||||
| Model | `Model::query()->paginate(...)` | 分页条件 | LengthAwarePaginator |
|
||||
|
||||
### 新增链路
|
||||
|
||||
| 层 | 方法 | 输入 | 输出 |
|
||||
|----|------|------|------|
|
||||
| Controller | `_add($logic, $data)` 或 `$this->logic->add($data)` | Json 请求体 | Json 响应 |
|
||||
| Logic | `add($data)` | 关联数组 | `int`(新记录 ID) |
|
||||
| Controller | `_add($service, $data)` | FormRequest 校验后的数组 | JsonResponse |
|
||||
| Service | `add($data)` | snake_case 关联数组 | `int`(新记录 ID) |
|
||||
| Model | `Model::create($data)` | 关联数组 | Model 实例 |
|
||||
|
||||
**Logic::add 内部流程:**
|
||||
`beforeAdd` → `validateData` → `camel_to_snake` → `processTenantId` → `processFile` → `processContent` → `filterTableFields` → `checkUnique` → `Model::create` → `afterAdd`
|
||||
**Service::add 内部流程**(HasCrudOperations):
|
||||
|
||||
`beforeAdd` → `checkUnique` → `DB::transaction { Model::create(HasAuditColumns 审计填充)}` → `afterAdd`
|
||||
|
||||
### 修改链路
|
||||
|
||||
| 层 | 方法 | 输入 | 输出 |
|
||||
|----|------|------|------|
|
||||
| Controller | `_edit($logic, $id, $data)` | id + Json 请求体 | Json 响应 |
|
||||
| Logic | `update($id, $data)` | id + 关联数组 | `bool` |
|
||||
| Model | `Model::find($id)->save($data)` | 模型实例 + 关联数组 | `bool` |
|
||||
| Controller | `_update($service, $id, $data)` | id + 请求数据 | JsonResponse |
|
||||
| Service | `update($id, $data)` | id + 关联数组 | `bool` |
|
||||
| Model | `Model::find($id)->update($data)` | 模型 + 关联数组 | `bool` |
|
||||
|
||||
### 删除链路
|
||||
|
||||
| 层 | 方法 | 输入 | 输出 |
|
||||
|----|------|------|------|
|
||||
| Controller | `_remove($logic, $id)` | id | Json 响应 |
|
||||
| Logic | `delete($id)` | id | `bool` |
|
||||
| Model | `$model->save(['is_delete' => 1])` | 软删除标记 | `bool` |
|
||||
| Controller | `_delete($service, $id)` | id | JsonResponse |
|
||||
| Service | `delete($id)` | id | `bool` |
|
||||
| Model | `$model->softDelete()` | 软删除标记 | `bool`(`UPDATE SET is_delete=1`) |
|
||||
|
||||
## 数据流向
|
||||
|
||||
@@ -296,28 +248,26 @@ CorsMiddleware → AuthMiddleware → TenantMiddleware → DemoMiddleware → Lo
|
||||
前端请求(camelCase JSON)
|
||||
│
|
||||
▼
|
||||
BaseController::getJsonBody() ← 获取原始参数
|
||||
FormRequest::validated() ← 格式校验(camelCase 规则),原样传递
|
||||
│
|
||||
▼
|
||||
BaseLogic::beforeAdd($data) ← 钩子:预处理
|
||||
Controller::_add($service, $data) ← 编排 + 注解元数据
|
||||
│
|
||||
▼
|
||||
BaseLogic::validateData($data) ← 自动参数校验
|
||||
Service::doAdd 入口 ← 键名统一转为 snake_case
|
||||
│
|
||||
▼
|
||||
BaseLogic::convertQueryParams() ← camelCase → snake_case
|
||||
Service::beforeAdd($data) ← 钩子:预处理
|
||||
│
|
||||
▼
|
||||
BaseLogic::processFileFieldsOnSave ← 文件字段处理
|
||||
Service::checkUnique($data) ← 唯一性校验(BizException 拦截)
|
||||
│
|
||||
▼
|
||||
BaseLogic::filterTableFields() ← 过滤非数据库字段
|
||||
│
|
||||
▼
|
||||
BaseLogic::checkUnique() ← 唯一性校验
|
||||
│
|
||||
▼
|
||||
BaseModel::create() ← 审计字段自动填充
|
||||
DB::transaction {
|
||||
Service::processTenantId($data) ← 租户填充(HasDataScope 启用时)
|
||||
Service::processFileFieldsOnSave ← 文件字段处理(HasFileProcessing 启用时)
|
||||
Model::create($data) ← 审计字段自动填充(creating 事件)
|
||||
}
|
||||
│
|
||||
▼
|
||||
Database(snake_case)
|
||||
@@ -329,16 +279,15 @@ Database(snake_case)
|
||||
Database(snake_case 数据)
|
||||
│
|
||||
▼
|
||||
BaseModel ← ORM 查询
|
||||
Model ← ORM 查询(软删除全局范围生效)
|
||||
│
|
||||
▼
|
||||
BaseLogic 处理
|
||||
│ processFileFieldsOnRead() ← 文件字段补全域名
|
||||
│ processContentFieldsOnRead() ← 富文本占位符替换
|
||||
│ processSerializeMaps() ← 枚举显示名补全
|
||||
│ afterDetail() ← 钩子:补充关联数据
|
||||
Service 处理(processAfterRead 处理链)
|
||||
│ processFileFieldsOnRead() ← 文件字段补全域名(启用时)
|
||||
│ processSerializeMapsList() ← 字典显示名补全(status → statusText)
|
||||
│ afterDetail()/afterPageList() ← 钩子:补充关联数据
|
||||
▼
|
||||
Result::convertData() ← snake_case → camelCase
|
||||
Result::success() / page() ← snake_case → camelCase 转换
|
||||
│
|
||||
▼
|
||||
JSON 响应 → 前端
|
||||
@@ -350,80 +299,87 @@ JSON 响应 → 前端
|
||||
Model 层异常(如数据库连接失败)
|
||||
│
|
||||
▼ 向上抛出
|
||||
Logic 层异常(如 beforeDelete 抛出业务异常、validateData 抛出 ValidateException)
|
||||
│
|
||||
Service 层异常
|
||||
│ beforeAdd/beforeDelete 抛出 BizException(业务规则)
|
||||
│ 唯一性校验抛出 BizException
|
||||
│ DB::transaction 自动回滚
|
||||
▼ 向上抛出
|
||||
Controller 层
|
||||
│ 方式一:try-catch 捕获,返回 fail()
|
||||
│ 方式二:不捕获,由 ExceptionHandle 统一处理
|
||||
│ 方式一:_add/_update/_delete 等快捷方法捕获 BizException → fail(消息, code)
|
||||
│ 方式二:不捕获,由全局 Handler 统一处理
|
||||
▼
|
||||
ExceptionHandle::render()
|
||||
│ ValidateException → {"code":1, "msg":"参数验证失败", "data":"..."}
|
||||
│ ModelNotFoundException → {"code":1, "msg":"数据不存在"}
|
||||
│ RuntimeException(401) → {"code":1, "msg":"未授权"}
|
||||
│ 其他异常 → {"code":1, "msg":"操作失败"}
|
||||
Exceptions\Handler::render()
|
||||
│ BizException → {code:异常code(默认1), msg:"业务提示"}
|
||||
│ ValidationException → {code:422, msg:"参数验证失败", data:...}
|
||||
│ AuthenticationException → {code:401}
|
||||
│ NotFoundHttpException → {code:404}
|
||||
│ 其它异常 → {code:500, msg:调试原文/生产"操作失败"}
|
||||
▼
|
||||
JSON 响应
|
||||
```
|
||||
|
||||
**推荐做法:**
|
||||
- Controller 通过 `_add`/`_edit`/`_remove` 调用时,不捕获异常,交给 ExceptionHandle
|
||||
- Controller 直接调用 `$this->logic->add()` 时,try-catch 捕获并返回 `$this->fail()`
|
||||
- Logic 层的业务异常(如 beforeDelete 校验)直接 `throw new \Exception`
|
||||
**推荐做法**:
|
||||
|
||||
- Controller 通过 `_add` / `_update` / `_delete` 调用时,**不捕获异常**,交给快捷方法与 Handler
|
||||
- Controller 手写复杂流程时,可 try-catch 捕获 `BizException` 后返回 `$this->fail()`
|
||||
- Service 层的业务校验直接 `throw new BizException('提示')`,**不要**返回错误码让上层判断
|
||||
|
||||
## 完整请求示例
|
||||
|
||||
以"新增文章"为例,展示一个请求从进入到返回的完整链路:
|
||||
以"新增案例"为例,展示一个请求从进入到返回的完整链路:
|
||||
|
||||
```text
|
||||
POST /api/article/add
|
||||
POST /api/example/add
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <token>
|
||||
|
||||
{"title": "新文章", "categoryId": 1, "status": 0}
|
||||
{"name": "新案例", "type": 1, "status": 1}
|
||||
```
|
||||
|
||||
```text
|
||||
1. CorsMiddleware → 放行(设置跨域头)
|
||||
2. AuthMiddleware → 校验 Token → 注入 $request->userInfo
|
||||
3. TenantMiddleware → 解析租户 → 注入 $request->tenantId
|
||||
4. LogMiddleware → 记录日志(#[Log] 注解)
|
||||
1. ProxyMiddleware → 真实 IP 识别(受信代理)
|
||||
2. Cors → 非 OPTIONS,放行(响应时追加 CORS 头)
|
||||
3. ForceJsonResponse → Accept 强制 JSON
|
||||
4. Authenticate → 校验 Token → 注入 attributes['userInfo']
|
||||
→ 反射 #[Permission] → checkPermission('sys:example:add') ✓
|
||||
5. ResolveTenant → 注入 tenantId / isSuperAdmin
|
||||
6. LogOperation(handle) → 收集上下文,读 #[OperationLog]
|
||||
|
||||
5. ArticleController::add()
|
||||
│ getJsonBody() → ["title"=>"新文章", "categoryId"=>1, "status"=>0]
|
||||
│
|
||||
7. ExampleRequest
|
||||
│ rules() 校验 name/type/status ✓
|
||||
│ validated() → ["name"=>"新案例", "type"=>1, "status"=>1]
|
||||
▼
|
||||
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 唯一 ✓
|
||||
│
|
||||
8. ExampleController::add()
|
||||
│ 注解元数据已由 4/6 读取
|
||||
│ $this->_add($service, $data)
|
||||
▼
|
||||
7. Article::create($data)
|
||||
│ onBeforeInsert → 自动填充 create_user, create_time
|
||||
│
|
||||
9. ExampleService::add($data)
|
||||
│ 入口键名转换 → array_camel_to_snake()
|
||||
│ beforeAdd($data) → 默认 sort(钩子)
|
||||
│ checkUnique($data) → name 唯一 ✓
|
||||
│ DB::transaction {
|
||||
│ Example::create($data) → creating:create_user/create_time 自动填充
|
||||
│ }
|
||||
│ afterAdd($id, $data) → 无额外操作
|
||||
▼
|
||||
8. Database INSERT
|
||||
10. Database INSERT → laravel_example → 自增 ID 42
|
||||
|
||||
9. ArticleLogic::afterAdd($id, $data) → 无额外操作
|
||||
11. 返回 {"code":0, "ok":true, "msg":"添加成功", "data":{"id":42}}
|
||||
|
||||
10. 返回 {"code":0, "ok":true, "msg":"添加成功", "data":{"id":42}}
|
||||
12. LogOperation(terminate) → 写入 laravel_operation_log(响应后,不影响业务)
|
||||
```
|
||||
|
||||
## 层间通信规范
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| Controller → Logic | ✅ 允许,标准调用 |
|
||||
| Controller → Service | ✅ 允许,标准调用 |
|
||||
| Controller → Model | ❌ 禁止,越层 |
|
||||
| Logic → Model | ✅ 允许,标准调用 |
|
||||
| Logic → Service | ✅ 允许,调用通用能力 |
|
||||
| Logic → Controller | ❌ 禁止,反向依赖 |
|
||||
| Model → Logic | ❌ 禁止,反向依赖 |
|
||||
| Model → Service | ⚠️ 慎用,仅静态工具方法 |
|
||||
| Middleware → Controller | ✅ 通过 `$request` 注入数据 |
|
||||
| Validate → Logic | ❌ 禁止,只做格式校验 |
|
||||
| Controller → FormRequest | ✅ 允许,类型注入 |
|
||||
| Service → Model | ✅ 允许,标准调用 |
|
||||
| Service → 横切 Service | ✅ 允许,调用通用能力(静态/注入均可) |
|
||||
| Service → Controller | ❌ 禁止,反向依赖 |
|
||||
| Service → FormRequest | ❌ 禁止,格式校验在 HTTP 入口完成 |
|
||||
| Model → Service / Controller | ❌ 禁止,反向依赖 |
|
||||
| Middleware → 后续链路 | ✅ 通过 `request->attributes` 注入数据 |
|
||||
| FormRequest → 业务规则 | ❌ 禁止,只做格式校验(业务规则在 Service) |
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
# 4.6 核心基类体系
|
||||
|
||||
:::tip 概述
|
||||
项目有四个核心基类,构成"控制器 → 逻辑层 → 模型"三层架构的骨架。所有业务模块都必须继承对应的基类,通过属性配置 + 生命周期钩子实现声明式 CRUD,子类只需声明"做什么"而不用写"怎么做"。
|
||||
项目有四个核心基类/骨架——`Controller`、`BaseService`、`BaseTenantService`、模型 Concerns Trait——构成"控制器 → 服务 → 模型"三层架构的骨架。所有业务模块都必须继承对应的基类,通过属性配置 + 生命周期钩子实现声明式 CRUD,子类只需声明"做什么"而不用写"怎么做"。
|
||||
:::
|
||||
|
||||
## 基类继承关系
|
||||
|
||||
:::warning 继承规则
|
||||
- 系统共享模块(Menu、Dict、Config 等)继承 `BaseLogic`
|
||||
- 租户隔离模块(User、Role、Article、Dept、Level 等)继承 `BaseTenantLogic`
|
||||
- 所有模型继承 `BaseModel`,所有控制器继承 `BaseController`
|
||||
- 系统共享模块(菜单、字典、配置等)继承 `BaseService`
|
||||
- 租户隔离模块(用户、角色、文章、部门、职级等)继承 `BaseTenantService`
|
||||
- 所有模型继承 Eloquent `Model` 并组合 `SoftDeletesByFlag` + `HasAuditColumns` Trait
|
||||
- 所有控制器继承 `App\Http\Controllers\Controller`
|
||||
- 所有请求类继承 `BaseRequest`
|
||||
:::
|
||||
|
||||
```text
|
||||
BaseController(控制器基类)
|
||||
└── 所有 Controller
|
||||
Controller(控制器基类)
|
||||
└── 所有 Api/Controller
|
||||
|
||||
BaseLogic(逻辑基类 - 系统共享模块)
|
||||
├── MenuLogic
|
||||
├── DictLogic
|
||||
├── ConfigLogic
|
||||
└── ...
|
||||
BaseService(服务基类,组合 6 个 Concerns Trait 的占位)
|
||||
├── ExampleService(系统共享/示例)
|
||||
├── MenuService、DictCacheService、ConfigService ...
|
||||
│
|
||||
└── BaseTenantLogic(租户逻辑基类,继承 BaseLogic)
|
||||
├── UserLogic
|
||||
├── RoleLogic
|
||||
├── ArticleLogic
|
||||
├── DeptLogic
|
||||
├── LevelLogic
|
||||
└── BaseTenantService(租户服务基类,use HasDataScope + tenantScopeField='tenant_id')
|
||||
├── UserService
|
||||
├── RoleService
|
||||
├── ArticleService
|
||||
└── ...
|
||||
|
||||
BaseModel(模型基类,继承 ThinkPHP Model)
|
||||
└── 所有 Model
|
||||
Illuminate\Database\Eloquent\Model(Eloquent 模型)
|
||||
└── 所有模型(use SoftDeletesByFlag, HasAuditColumns)
|
||||
|
||||
FormRequest(Laravel 表单请求)
|
||||
└── BaseRequest(统一失败响应)
|
||||
└── 所有 Request
|
||||
```
|
||||
|
||||
## 请求处理全链路
|
||||
@@ -42,21 +44,22 @@ BaseModel(模型基类,继承 ThinkPHP Model)
|
||||
HTTP 请求
|
||||
│
|
||||
▼
|
||||
BaseController(参数获取、响应封装)
|
||||
│ getJsonBody() / getParams()
|
||||
│ success() / fail() / _page()
|
||||
Controller(编排、响应封装)
|
||||
│ FormRequest 类型注入 → 格式校验(validated())
|
||||
│ success() / fail() / _page() / _add()
|
||||
│
|
||||
▼
|
||||
BaseLogic(业务编排、属性驱动)
|
||||
│ validateData() → 自动参数校验
|
||||
│ beforeAdd() → 生命周期钩子
|
||||
│ add() → 文件处理、唯一性校验、写入
|
||||
│ afterAdd() → 关联表保存
|
||||
BaseService(业务编排、属性驱动)
|
||||
│ 入口键名转换 → array_camel_to_snake()
|
||||
│ beforeAdd() → 生命周期钩子
|
||||
│ checkUnique() → 唯一性校验(BizException)
|
||||
│ DB::transaction → 事务包裹
|
||||
│ processAfterRead → 读取后处理链(文件/字典翻译)
|
||||
│
|
||||
▼
|
||||
BaseModel(数据持久化)
|
||||
│ onBeforeInsert → 审计字段自动填充
|
||||
│ soft_delete → 全局过滤已删除数据
|
||||
Model(数据持久化,Concerns Trait)
|
||||
│ HasAuditColumns → 审计字段自动填充(creating/updating 事件)
|
||||
│ SoftDeleteFlagScope → 全局过滤 is_delete=1
|
||||
│
|
||||
▼
|
||||
数据库
|
||||
@@ -64,286 +67,233 @@ BaseModel(数据持久化)
|
||||
|
||||
---
|
||||
|
||||
## BaseController
|
||||
## Controller
|
||||
|
||||
**文件**:`app/BaseController.php`
|
||||
**文件**:`app/Http/Controllers/Controller.php`
|
||||
|
||||
### 设计要点
|
||||
|
||||
- 通用 CRUD 方法以 `_` 前缀命名,避免与子类路由方法冲突
|
||||
- 子类只需实现 `initialize()` 并实例化自己的 Logic 层
|
||||
- 登录用户信息来自 `AuthMiddleware` 注入的 `$request->userInfo`
|
||||
- 子类通过**构造函数注入** Service(Laravel 依赖注入),无需 `initialize()`
|
||||
- 当前登录用户通过 `Support\Auth` 工具类获取(`Auth::userId()` 等)
|
||||
- `BizException` 由 `_add/_update/_delete` 等方法捕获转为失败响应
|
||||
|
||||
### 方法分类
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ BaseController │
|
||||
│ Controller 基类 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 统一响应 │
|
||||
│ success($data, $msg, $raw) 成功响应 │
|
||||
│ _page($result) 分页响应 │
|
||||
│ fail($msg, $code, $data) 失败响应 │
|
||||
│ 统一响应(委托 Support\Result) │
|
||||
│ success($data, $msg, $raw) 成功响应 │
|
||||
│ paginate($result) 分页响应 │
|
||||
│ fail($msg, $code, $data) 失败响应(code 默认 1) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 参数获取 │
|
||||
│ getParam($name, $default) 获取单个参数 │
|
||||
│ getParams() 获取所有参数 │
|
||||
│ getPageParams() 获取分页参数(含边界限制) │
|
||||
│ getJsonBody() 获取 JSON 请求体 │
|
||||
│ 参数获取 │
|
||||
│ getPageParams() 分页参数(pageNo≥1, │
|
||||
│ pageSize 1~100,默认 20) │
|
||||
│ getJsonBody() JSON 请求体 │
|
||||
│ getParams() 全部参数(query + body 合并) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 登录用户 │
|
||||
│ 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(子类通过 $this-> 调用) │
|
||||
│ _page($service, $where, $with) 分页查询 │
|
||||
│ _list($service, $where) 全量列表 │
|
||||
│ _treeList($service, $where) 树形列表 │
|
||||
│ _detail($service, $id) 详情(空 → 404) │
|
||||
│ _add($service, $data) 新增(返回 id) │
|
||||
│ _update($service, $id, $data) 修改 │
|
||||
│ _delete($service, $id) 删除 │
|
||||
│ _batchDelete($service, $ids) 批量删除 │
|
||||
│ _export($service) 导出(返回文件地址) │
|
||||
│ _import($service) 导入(文件校验 + 结果汇总) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 通用 CRUD 方法与 Logic 层的对应关系
|
||||
### 通用 CRUD 方法与 Service 层的对应关系
|
||||
|
||||
| Controller 方法 | Logic 方法 | 说明 |
|
||||
| Controller 方法 | Service 方法 | 说明 |
|
||||
|---|---|---|
|
||||
| `_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)` | 批量删除 |
|
||||
| `_page($service)` | `pageList()` | 分页查询 |
|
||||
| `_list($service)` | `allList()` | 全量列表 |
|
||||
| `_treeList($service)` | `treeList()` | 树形列表 |
|
||||
| `_detail($service, $id)` | `detail($id)` | 详情查询(空返回 404) |
|
||||
| `_add($service, $data)` | `add($data)` | 新增(捕获 BizException) |
|
||||
| `_update($service, $id, $data)` | `update($id, $data)` | 修改(id 缺失直接失败提示) |
|
||||
| `_delete($service, $id)` | `delete($id)` | 删除(BizException→业务提示;其它异常记日志) |
|
||||
| `_batchDelete($service, $ids)` | `batchDelete($ids)` | 批量删除(ids 为空直接提示) |
|
||||
| `_export($service)` | `export()` | 导出 |
|
||||
| `_import($service)` | `import()` | 导入(扩展名/大小/MIME 三重校验) |
|
||||
|
||||
### 分页参数
|
||||
|
||||
`getPageParams()` 返回 `['pageNo' => int, 'pageSize' => int]`,带边界限制:
|
||||
|
||||
- `pageNo` 最小为 1
|
||||
- `pageSize` 限制在 1~100,默认 20
|
||||
- `pageSize` 限制在 1~100,默认 20(防止一次拉取过多数据)
|
||||
|
||||
前端传参示例:`GET /api/article/page?pageNo=1&pageSize=10&title=测试&status=1`
|
||||
前端传参示例:`GET /api/example/page?pageNo=1&pageSize=10&name=测试&status=1`
|
||||
|
||||
### 子类实现示例
|
||||
|
||||
```php
|
||||
class ArticleController extends BaseController
|
||||
class ExampleController extends Controller
|
||||
{
|
||||
protected ArticleLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new ArticleLogic();
|
||||
}
|
||||
public function __construct(protected ExampleService $exampleService) {}
|
||||
|
||||
// 分页查询 — 一行搞定
|
||||
#[Permission('sys:article:list', '文章列表')]
|
||||
public function page(): Json
|
||||
#[Permission('sys:example:page', '示例分页')]
|
||||
public function page(): JsonResponse
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
return $this->_page($this->exampleService);
|
||||
}
|
||||
|
||||
// 详情
|
||||
#[Permission('sys:article:detail', '文章详情')]
|
||||
public function detail(int $id): Json
|
||||
#[Permission('sys:example:detail', '示例详情')]
|
||||
public function detail(int $id): JsonResponse
|
||||
{
|
||||
return parent::_detail($this->logic, $id);
|
||||
return $this->_detail($this->exampleService, $id);
|
||||
}
|
||||
|
||||
// 新增(使用 _add 快捷方法)
|
||||
#[Log('文章管理-新增', Log::TYPE_ADD, '新增文章:{title}')]
|
||||
#[Permission('sys:article:add', '添加文章')]
|
||||
public function add(): Json
|
||||
// 新增(参数由 ExampleRequest 类型注入校验)
|
||||
#[OperationLog('示例管理-新增', OperationLog::TYPE_ADD, '新增示例:{name}')]
|
||||
#[Permission('sys:example:add', '添加示例')]
|
||||
public function add(ExampleRequest $request): JsonResponse
|
||||
{
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
return $this->_add($this->exampleService, $request->validated());
|
||||
}
|
||||
|
||||
// 删除 — 一行搞定
|
||||
#[Permission('sys:article:delete', '删除文章')]
|
||||
public function delete(int $id): Json
|
||||
#[Permission('sys:example:delete', '删除示例')]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
return $this->_delete($this->exampleService, $id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BaseLogic
|
||||
## BaseService
|
||||
|
||||
**文件**:`app/BaseLogic.php`
|
||||
**文件**:`app/Services/BaseService.php`
|
||||
|
||||
BaseLogic 是整个项目的核心,通过"属性配置 + 生命周期钩子"实现声明式 CRUD。子类只需配置属性和按需重写钩子,即可获得完整的增删改查能力。
|
||||
BaseService 是整个项目的核心,通过"属性配置 + 生命周期钩子"实现声明式 CRUD。子类只需配置属性和按需重写钩子,即可获得完整的增删改查能力。
|
||||
|
||||
### 属性配置清单
|
||||
### 属性配置清单(全部集中声明于 BaseService)
|
||||
|
||||
| 属性 | 类型 | 说明 | 示例 |
|
||||
|---|---|---|---|
|
||||
| `modelClass` | string | 关联模型类名(**必须**) | `Article::class` |
|
||||
| `validateClass` | string | 验证器类名(设置后 add/update 自动校验) | `ArticleValidate::class` |
|
||||
| `fileFields` | array | 单文件字段列表 | `['cover', 'avatar']` |
|
||||
| `modelClass` | string | 关联模型类名(**必须**) | `Example::class` |
|
||||
| `defaultWith` | array | 查询默认预加载关联 | `['creator']` |
|
||||
| `fileFields` | array | 单文件字段列表(需 use HasFileProcessing) | `['cover', 'avatar']` |
|
||||
| `multiFileFields` | array | 多文件字段列表(逗号分隔的多个URL) | `['images']` |
|
||||
| `contentFields` | array | 富文本字段 | `['content']` |
|
||||
| `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'` |
|
||||
| `orderFieldAllow` | array | 允许前端动态排序的字段白名单 | `['create_time']` |
|
||||
| `uniqueFields` | array | 唯一性校验规则(字符串全局 / 数组分组) | `['username', ['code','pid']]` |
|
||||
| `serializeMaps` | array | 字典显示名映射 | `['status'=>'article_status']` |
|
||||
| `dataScopeUserField` | string | 数据权限:归属字段(需 use HasDataScope) | `'create_user'` |
|
||||
| `dataScopeDeptField` | string | 数据权限:部门字段(需 use HasDataScope) | `'dept_id'` |
|
||||
| `tenantScopeField` | string | 租户隔离字段(BaseTenantService 预设) | `'tenant_id'` |
|
||||
| `treeParentField` | string | 树形结构父级字段 | `'parent_id'` |
|
||||
| `treeLikeField` | string | 树形列表模糊搜索字段 | `'name'` |
|
||||
|
||||
### 生命周期钩子
|
||||
### 生命周期钩子(HasCrudOperations 提供 10 个)
|
||||
|
||||
| 钩子 | 时机 | 用途 | 可拦截 |
|
||||
|---|---|---|---|
|
||||
| `beforeAdd($data)` | 新增前 | 修改数据、预处理,**返回处理后的 data** | ✅ 抛异常 |
|
||||
| `afterAdd($id, $data)` | 新增后 | 保存关联表、发送通知 | ❌ |
|
||||
| `afterAdd($id, $data)` | 新增后 | 保存关联表、发送通知 | ✅(事务内) |
|
||||
| `beforeUpdate($id, $data)` | 修改前 | 修改数据、预处理,**返回处理后的 data** | ✅ 抛异常 |
|
||||
| `afterUpdate($id, $data)` | 修改后 | 更新关联表 | ❌ |
|
||||
| `afterUpdate($id, $data)` | 修改后 | 更新关联表 | ✅(事务内) |
|
||||
| `beforeDelete($id)` | 删除前 | 检查子级、业务约束 | ✅ 抛异常 |
|
||||
| `afterDelete($id)` | 删除后 | 清理关联数据 | ❌ |
|
||||
| `afterDelete($id)` | 删除后 | 清理关联数据 | ✅(事务内) |
|
||||
| `beforeBatchDelete($ids)` | 批量删除前 | 整体校验 | ✅ 抛异常 |
|
||||
| `afterBatchDelete($ids)` | 批量删除后 | 批量清理 | ❌ |
|
||||
| `afterBatchDelete($ids)` | 批量删除后 | 批量清理 | ✅(事务内) |
|
||||
| `afterDetail($id, &$data)` | 详情查询后 | 补充关联数据、格式化输出(引用传递) | ❌ |
|
||||
| `afterPageList(&$records, $params)` | 列表查询后 | 批量补充关联数据,分页 pageList 与全量 allList 均触发(引用传递) | ❌ |
|
||||
|
||||
### 新增流程(add)
|
||||
|
||||
```text
|
||||
array_camel_to_snake($data) ← 入口键名统一转为 snake_case
|
||||
│
|
||||
▼
|
||||
beforeAdd($data) ← 钩子:可修改数据,抛异常可拦截
|
||||
│
|
||||
▼
|
||||
validateData($data, 'add') ← 自动参数校验(配置了 validateClass 时)
|
||||
checkUnique($data) ← 唯一性校验(uniqueFields,BizException 拦截)
|
||||
│
|
||||
▼
|
||||
array_camel_to_snake($data) ← 键名 camelCase → snake_case
|
||||
DB::transaction { ← 事务包裹(钩子抛异常自动回滚)
|
||||
│
|
||||
▼
|
||||
processTenantId($data) ← 自动填充 tenant_id
|
||||
Model::create($data) ← 写入数据库(HasAuditColumns 自动填充审计字段)
|
||||
│
|
||||
▼
|
||||
processFileFieldsOnSave ← 文件字段:临时文件 → 正式目录
|
||||
afterAdd($id, $data) ← 钩子:保存关联表等副作用
|
||||
}
|
||||
│
|
||||
▼
|
||||
processContentFieldsOnSave ← 富文本:迁移媒体文件
|
||||
│
|
||||
▼
|
||||
filterTableFields($data) ← 过滤非数据库字段
|
||||
│
|
||||
▼
|
||||
checkUnique($data) ← 唯一性校验
|
||||
│
|
||||
▼
|
||||
Model::create($data) ← 写入数据库
|
||||
│
|
||||
▼
|
||||
afterAdd($id, $data) ← 钩子:保存关联表等副作用
|
||||
return $id
|
||||
```
|
||||
|
||||
:::tip 职责划分
|
||||
参数格式校验**不在 Service 内执行**——由控制器的 FormRequest 在入口完成(见 [3.4 FormRequest 验证器](/zh/guide/validator.html));键名转换由 Service 写入入口(doAdd/doUpdate)统一完成。Service 只做业务规则(唯一性、状态依赖)。
|
||||
:::
|
||||
|
||||
### 修改流程(update)
|
||||
|
||||
```text
|
||||
beforeUpdate($id, $data) ← 钩子
|
||||
│
|
||||
▼
|
||||
validateData($data, 'update', $id) ← 自动参数校验
|
||||
checkUnique($data, $excludeId) ← 唯一性校验(排除自身)
|
||||
│
|
||||
▼
|
||||
array_camel_to_snake → 文件 → 富文本 → 过滤 → 唯一性(排除自身)
|
||||
│
|
||||
▼
|
||||
Model::find($id)->save($data)
|
||||
│
|
||||
▼
|
||||
afterUpdate($id, $data)
|
||||
DB::transaction {
|
||||
Model::find($id)->update($data) ← 更新(HasAuditColumns 强制刷新审计字段)
|
||||
afterUpdate($id, $data) ← 钩子
|
||||
}
|
||||
```
|
||||
|
||||
### 删除流程(delete)
|
||||
|
||||
```text
|
||||
beforeDelete($id) ← 钩子:抛异常可拦截
|
||||
beforeDelete($id) ← 钩子:抛异常可拦截(如存在关联数据)
|
||||
│
|
||||
▼
|
||||
is_delete = 1 ← 软删除
|
||||
│
|
||||
▼
|
||||
afterDelete($id)
|
||||
DB::transaction {
|
||||
is_delete = 1 ← 软删除(SoftDeletesByFlag)
|
||||
afterDelete($id)
|
||||
}
|
||||
```
|
||||
|
||||
### 查询流程(pageList / allList)
|
||||
|
||||
```text
|
||||
buildQuery($model) ← 构建查询条件(LIKE + 精确匹配 + 数据权限 + 租户隔离)
|
||||
buildQuery($model) ← 构建查询条件(额外条件 + LIKE + 精确匹配
|
||||
│ + 数据权限 + 租户隔离,camel→snake 转换参数)
|
||||
▼
|
||||
applyOrderBy($model) ← 排序(前端 orderField/orderType 优先,
|
||||
│ 白名单校验,否则用 pageOrderBy 配置)
|
||||
▼
|
||||
$page / select ← 分页 / 全量查询
|
||||
│
|
||||
▼
|
||||
applyOrderBy($model) ← 排序(前端传参优先,否则使用配置)
|
||||
│
|
||||
▼
|
||||
$page / $select ← 分页 / 全量查询
|
||||
│
|
||||
▼
|
||||
processFileFieldsOnReadList ← 文件字段补全域名
|
||||
│
|
||||
▼
|
||||
processSerializeMapsList ← 枚举显示名补全
|
||||
│
|
||||
▼
|
||||
filterFields ← 字段过滤
|
||||
processAfterRead 处理链
|
||||
├─ processFileFieldsOnReadList ← 文件字段补全域名(HasFileProcessing 启用时)
|
||||
├─ processSerializeMapsList ← 字典显示名补全(status → statusText)
|
||||
└─ filterFields ← 字段过滤(fields 参数指定返回字段)
|
||||
│
|
||||
▼
|
||||
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)
|
||||
### 唯一性校验(uniqueFields,HasUniqueCheck)
|
||||
|
||||
```php
|
||||
// 全局唯一
|
||||
@@ -351,211 +301,117 @@ protected array $uniqueFields = ['username', 'mobile'];
|
||||
|
||||
// 分组内唯一:code 在同一 pid 下唯一
|
||||
protected array $uniqueFields = [
|
||||
'username', // 全局唯一
|
||||
['code', 'pid'], // code 在同一 pid 下唯一
|
||||
'username', // 字符串 = 全局唯一
|
||||
['code', 'pid'], // 数组 = [唯一字段, 分组字段],分组内唯一
|
||||
];
|
||||
```
|
||||
|
||||
校验时机:add/update 时自动校验(在 `checkUnique()` 中执行)。错误提示格式:"字段中文名 + 已存在",可通过重写 `getFieldLabel()` 自定义。
|
||||
校验规则(`checkUnique`):
|
||||
|
||||
### 查询条件构建
|
||||
- add/update 时自动执行;值为空时跳过(FormRequest 的 `required` 兜底)
|
||||
- 更新时自动排除自身(`excludeId`)
|
||||
- 重复时抛出 `BizException("{字段中文名}已存在")`
|
||||
- 字段中文名查找顺序:`$fieldLabels` 属性 → Trait 内置映射 → 字段名本身
|
||||
(子类声明 `protected array $fieldLabels = ['code' => '角色编码'];` 可自定义提示)
|
||||
|
||||
`buildQuery()` 按以下顺序叠加条件:
|
||||
### Trait 占位覆盖机制
|
||||
|
||||
1. 额外固定条件(代码层面通过 `$where` 传入)
|
||||
2. LIKE 模糊匹配(`pageLikeFields` 中配置的字段)
|
||||
3. 精确匹配(`pageEqFields` 中配置的字段)
|
||||
4. 数据权限过滤(`applyDataScope`)
|
||||
5. 租户隔离过滤(`applyTenantScope`)
|
||||
`BaseService` 底部为 6 个 Trait 的所有方法提供**占位实现**(如 `processFileFieldsOnSave()` 直接 `return $data`)。子类 `use` 对应 Trait 后,PHP Trait 机制自动以真实实现覆盖占位——未启用的 Trait 方法保持空操作,避免调用未定义方法。
|
||||
|
||||
### 子类实现示例
|
||||
| Trait | 提供的方法族 |
|
||||
|------|-------------|
|
||||
| `HasCrudOperations` | add / update / delete / batchDelete(事务 + 钩子) |
|
||||
| `HasQueryScopes` | buildQuery / applyOrderBy / 排序白名单 |
|
||||
| `HasFileProcessing` | 文件/多文件/富文本字段的保存迁移与读取补全 |
|
||||
| `HasDataScope` | applyDataScope / applyTenantScope / processTenantId |
|
||||
| `HasSerializeMaps` | processSerializeMaps(字典翻译) |
|
||||
| `HasUniqueCheck` | checkUnique / getFieldLabel |
|
||||
| `HasConfigValueProcessing` | processConfigValueOnSave / buildConfigValueList / formatConfigValueOnRead |
|
||||
|
||||
```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;
|
||||
}
|
||||
}
|
||||
```
|
||||
`HasConfigValueProcessing` 不依赖 `BaseService` 的占位实现,由 `ConfigService` 与 `ConfigItemService` 直接 `use`。
|
||||
|
||||
---
|
||||
|
||||
## BaseTenantLogic
|
||||
## BaseTenantService
|
||||
|
||||
**文件**:`app/BaseTenantLogic.php`
|
||||
**文件**:`app/Services/BaseTenantService.php`
|
||||
|
||||
### 设计说明
|
||||
|
||||
BaseTenantLogic 继承 BaseLogic,**仅做一件事**:将 `tenantScopeField` 设为 `'tenant_id'`,从而让所有子类自动获得租户隔离能力。
|
||||
BaseTenantService 继承 BaseService,**仅做两件事**:`use HasDataScope` 引入数据权限 + 租户隔离能力,并将 `tenantScopeField` 预设为 `'tenant_id'`,子类自动获得租户隔离:
|
||||
|
||||
```php
|
||||
abstract class BaseTenantLogic extends BaseLogic
|
||||
abstract class BaseTenantService extends BaseService
|
||||
{
|
||||
use Concerns\HasDataScope;
|
||||
|
||||
/**
|
||||
* 租户隔离字段名
|
||||
* 租户隔离字段
|
||||
*
|
||||
* @var string
|
||||
* 预设为 tenant_id,子类如需使用其它字段名可覆盖。
|
||||
*/
|
||||
protected string $tenantScopeField = 'tenant_id';
|
||||
}
|
||||
```
|
||||
|
||||
### 隔离机制(由 BaseLogic 实现)
|
||||
### 隔离机制(由 HasDataScope 实现)
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| **查询时** | `applyTenantScope` 自动叠加 `WHERE tenant_id = ?` 条件 |
|
||||
| **新增时** | `processTenantId` 自动填充 `tenant_id` 字段 |
|
||||
| **超级管理员** | `tenantId = 0` 时不过滤,可跨租户查看数据 |
|
||||
| **查询时** | `applyTenantScope` 自动叠加 `WHERE tenant_id = ?` |
|
||||
| **新增时** | `processTenantId` 自动填充租户字段:平台用户代入 `config('tenant.default_tenant_id')`,租户用户代入当前 `tenantId` |
|
||||
| **超级管理员** | `tenantId = 0`(平台用户)不过滤,可跨租户查看数据 |
|
||||
|
||||
### 租户上下文来源
|
||||
|
||||
`TenantMiddleware` 解析并注入 `$request->tenantId`。
|
||||
`ResolveTenant` 中间件解析并注入 `request->attributes['tenantId']`(详见 [4.2 请求处理链路](/zh/architecture/request-flow.html))。
|
||||
|
||||
### 使用约定
|
||||
### 数据权限
|
||||
|
||||
- 需要租户隔离的模块继承此基类(如 User、Article、Dept、Level 等)
|
||||
- 系统共享表不继承此基类,直接继承 BaseLogic(如 Menu、Role、Dict、Config 等)
|
||||
|
||||
### 子类示例
|
||||
`BaseTenantService` **默认未启用数据权限**,子类声明字段后 `HasDataScope` 才会叠加过滤(摘自 `app/Services/UserService.php` 实际代码):
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic // 注意:继承 BaseTenantLogic
|
||||
class UserService extends BaseTenantService
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Article::class;
|
||||
|
||||
/**
|
||||
* 参数验证器类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $validateClass = \app\validate\ArticleValidate::class;
|
||||
// tenantScopeField 已由基类设为 'tenant_id',无需再声明
|
||||
// ...
|
||||
protected string $dataScopeDeptField = 'dept_id'; // 启用按部门过滤
|
||||
}
|
||||
```
|
||||
|
||||
### 使用约定
|
||||
|
||||
- 需要租户隔离的模块继承此类(如 User、Article、Dept、Level 等)
|
||||
- 系统共享表不继承此类,直接继承 BaseService(如 Menu、Dict、Config 等)
|
||||
|
||||
---
|
||||
|
||||
## BaseModel
|
||||
## 模型层(Eloquent Model + Concerns Trait)
|
||||
|
||||
**文件**:`app/BaseModel.php`
|
||||
**文件**:`app/Models/*.php` + `app/Models/Concerns/*.php`
|
||||
|
||||
### 设计说明
|
||||
|
||||
BaseModel 继承 ThinkPHP 的 Model,提供两项横切能力:
|
||||
本项目**没有单一模型基类**,模型直接继承 Eloquent `Model`,通过组合两个 Trait 获得横切能力:
|
||||
|
||||
1. **软删除**:基于 `is_delete` 字段,不使用 ThinkPHP 内置的 `deleteTime` 机制
|
||||
2. **审计字段自动写入**:`create_user` / `create_time` / `update_user` / `update_time`
|
||||
1. **软删除**:`SoftDeletesByFlag`(基于 `is_delete` 字段,不使用 Laravel 内置 `SoftDeletes` 的 `deleted_at` 机制)
|
||||
2. **审计字段自动写入**:`HasAuditColumns`(`create_user` / `create_time` / `update_user` / `update_time`)
|
||||
|
||||
### 软删除机制
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 软删除机制 │
|
||||
│ 软删除机制 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 全局查询范围 soft_delete: │
|
||||
│ 所有查询自动叠加 WHERE is_delete = 0 │
|
||||
│ → 已删除数据默认不可见 │
|
||||
│ 全局查询范围 SoftDeleteFlagScope: │
|
||||
│ 所有查询自动叠加 WHERE is_delete = 0 │
|
||||
│ → 已删除数据默认不可见 │
|
||||
│ │
|
||||
│ 软删除操作: │
|
||||
│ $model->softDelete() → UPDATE SET is_delete = 1 │
|
||||
│ BaseModel::batchSoftDelete($ids) → 批量软删除 │
|
||||
│ 软删除操作: │
|
||||
│ $model->softDelete() → UPDATE SET is_delete = 1 │
|
||||
│ Model::batchSoftDelete($ids) → 批量软删除 │
|
||||
│ │
|
||||
│ 查询已删除数据: │
|
||||
│ Model::withoutGlobalScope(['soft_delete'])->... │
|
||||
│ 查询已删除数据: │
|
||||
│ Model::withoutGlobalScope(SoftDeleteFlagScope::class) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -565,8 +421,8 @@ BaseModel 继承 ThinkPHP 的 Model,提供两项横切能力:
|
||||
| 方法 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `softDelete()` | 实例方法 | 设置 `is_delete = 1` |
|
||||
| `batchSoftDelete($ids)` | 静态方法 | 批量软删除,先关闭全局范围再 whereIn 更新 |
|
||||
| `scopeSoftDelete($query)` | 全局范围 | 自动过滤 `is_delete = 1` 的数据 |
|
||||
| `batchSoftDelete($ids)` | 静态方法 | 批量软删除(先关闭全局范围再 whereIn 更新) |
|
||||
| `SoftDeleteFlagScope` | 全局范围 | 查询自动追加 `is_delete = 0` |
|
||||
|
||||
### 审计字段自动写入
|
||||
|
||||
@@ -574,10 +430,10 @@ BaseModel 继承 ThinkPHP 的 Model,提供两项横切能力:
|
||||
|
||||
| 事件 | 自动填充 | 说明 |
|
||||
|---|---|---|
|
||||
| `onBeforeInsert`(创建前) | `create_user`、`create_time` | 若调用方已显式传入,则不覆盖 |
|
||||
| `onBeforeUpdate`(更新前) | `update_user`、`update_time` | `update_time` 始终覆盖 |
|
||||
| `creating`(创建前) | `create_user`、`create_time` | 调用方已显式传入则不覆盖(`??` 逻辑) |
|
||||
| `updating`(更新前) | `update_user`、`update_time` | 每次强制覆盖 |
|
||||
|
||||
用户来源:`AuthMiddleware` 注入的 `$request->userInfo->username`。若无登录用户(如控制台任务),则跳过用户字段,时间字段仍写入。
|
||||
用户来源:`Auth::username()`(当前登录用户,来自 `auth.jwt` 中间件注入的 `userInfo`)。无登录用户(如队列/命令行)时用户字段保留空值,时间字段仍写入。
|
||||
|
||||
### 数据库字段规范
|
||||
|
||||
@@ -596,44 +452,65 @@ BaseModel 继承 ThinkPHP 的 Model,提供两项横切能力:
|
||||
|
||||
```php
|
||||
// 模型定义 — 最简形式
|
||||
class Article extends BaseModel
|
||||
class Example extends Model
|
||||
{
|
||||
use HasAuditColumns, SoftDeletesByFlag;
|
||||
|
||||
/**
|
||||
* 对应数据库表名(不含前缀)
|
||||
* 数据表名(不含前缀)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'article';
|
||||
protected $table = 'example';
|
||||
|
||||
protected $fillable = ['name', 'avatar', 'type', 'status', 'sort'];
|
||||
}
|
||||
|
||||
// 软删除
|
||||
$article = Article::find(1);
|
||||
$article->softDelete(); // UPDATE article SET is_delete = 1 WHERE id = 1
|
||||
$example = Example::find(1);
|
||||
$example->softDelete(); // UPDATE laravel_example SET is_delete = 1 WHERE id = 1
|
||||
|
||||
// 批量软删除
|
||||
Article::batchSoftDelete([1, 2, 3]);
|
||||
Example::batchSoftDelete([1, 2, 3]);
|
||||
|
||||
// 查询已删除数据(临时关闭全局范围)
|
||||
$all = Article::withoutGlobalScope(['soft_delete'])->select();
|
||||
$all = Example::withoutGlobalScope(SoftDeleteFlagScope::class)->get();
|
||||
|
||||
// 审计字段自动写入(无需手动处理)
|
||||
Article::create(['title' => '测试']);
|
||||
// 自动填充: create_user='admin', create_time='2026-09-22 12:00:00'
|
||||
Example::create(['name' => '测试']);
|
||||
// 自动填充: create_user='admin', create_time='2026-10-02 12:00:00'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BaseRequest(表单请求基类)
|
||||
|
||||
**文件**:`app/Http/Requests/BaseRequest.php`
|
||||
|
||||
| 能力 | 说明 |
|
||||
|---|---|
|
||||
| `authorize()` | 统一返回 true(鉴权由中间件与 `#[Permission]` 负责) |
|
||||
| `failedValidation()` | 统一返回 `code=1` + 字段错误明细的 JSON |
|
||||
|
||||
详见 [3.4 FormRequest 验证器](/zh/guide/validator.html)。
|
||||
|
||||
---
|
||||
|
||||
## 基类选择指南
|
||||
|
||||
| 场景 | 继承 | 原因 |
|
||||
|---|---|---|
|
||||
| 系统级共享数据(菜单、字典、配置) | `BaseLogic` | 不需要租户隔离 |
|
||||
| 租户业务数据(用户、角色、文章、部门、职级) | `BaseTenantLogic` | 需要按租户隔离数据 |
|
||||
| 所有数据模型 | `BaseModel` | 获得软删除 + 审计字段 |
|
||||
| 所有控制器 | `BaseController` | 获得统一响应 + 通用 CRUD |
|
||||
| 系统级共享数据(菜单、字典、配置) | `BaseService` | 不需要租户隔离 |
|
||||
| 租户业务数据(用户、角色、文章、部门、职级) | `BaseTenantService` | 自动 `tenant_id` 隔离 |
|
||||
| 所有数据模型 | `Model` + 双 Trait | 软删除 + 审计字段 |
|
||||
| 所有控制器 | `Controller` | 统一响应 + 通用 CRUD |
|
||||
| 所有表单请求 | `BaseRequest` | 统一失败响应 + 键名转换 |
|
||||
|
||||
:::tip 快速判断
|
||||
问自己:这个模块的数据是否需要按租户隔离?
|
||||
- **是** → 继承 `BaseTenantLogic`
|
||||
- **否** → 继承 `BaseLogic`
|
||||
|
||||
- **是** → 继承 `BaseTenantService`
|
||||
- **否** → 继承 `BaseService`
|
||||
|
||||
文件/富文本/数据权限等能力按需 `use` 对应 Trait,未启用时占位方法自动空操作。
|
||||
:::
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 4.8 缓存架构设计
|
||||
|
||||
:::tip 概述
|
||||
项目采用两级缓存架构:请求级缓存(内存)+ 持久缓存(file/Redis),大幅减少数据库查询。
|
||||
项目采用两级缓存架构:请求级缓存(内存)+ 持久缓存(Cache 门面,默认 Redis),大幅减少数据库查询;数据变更时由 Observer 自动失效对应缓存,保证一致性。
|
||||
:::
|
||||
|
||||
## 两级缓存架构
|
||||
@@ -19,10 +19,10 @@
|
||||
│ 未命中
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 第二级:持久缓存(cache() 门面) │
|
||||
│ 第二级:持久缓存(cache() 门面 / Redis) │
|
||||
│ │
|
||||
│ 生命周期:跨请求复用 │
|
||||
│ 存储位置:file 或 Redis │
|
||||
│ 存储位置:Redis(CACHE_STORE=redis,必需组件) │
|
||||
│ 速度:快(毫秒级) │
|
||||
│ 有效期:1 小时(3600 秒) │
|
||||
│ │
|
||||
@@ -41,50 +41,79 @@
|
||||
|
||||
| 服务 | 缓存键格式 | 说明 |
|
||||
|------|-----------|------|
|
||||
| `DictService` | `dict_{code}` | 字典项映射 |
|
||||
| `DictService` | `dict_full_{code}` | 字典项完整数据 |
|
||||
| `ParamService` | `param_{code}` | 系统参数值 |
|
||||
| `DictCacheService` | `dict_{code}`(前缀 `dict_`) | 字典项映射(`CACHE_PREFIX = 'dict_'`,TTL 见 `CACHE_TTL`) |
|
||||
| `MenuService` 等 | 权限/菜单缓存 | 角色权限列表(配合 Observer 失效) |
|
||||
|
||||
## 缓存一致性:Observer 自动失效
|
||||
|
||||
数据变更时由 Eloquent Observer 精确失效相关缓存(注册于 `AppServiceProvider`):
|
||||
|
||||
| Observer | 监听模型 | 触发动作 |
|
||||
|----------|----------|----------|
|
||||
| `DictItemObserver` | 字典项 | 清除所属字典缓存(`dict_{code}`) |
|
||||
| `MenuObserver` | 菜单 | 清除菜单/权限相关缓存 |
|
||||
| `RoleMenuObserver` | 角色-菜单关系 | 清除角色权限缓存 |
|
||||
|
||||
```text
|
||||
字典项变更 → DictItemObserver → DictCacheService::clearCache(code) → 下次读取回源重建
|
||||
菜单变更 → MenuObserver → 权限缓存失效 → 前端刷新菜单后生效
|
||||
```
|
||||
|
||||
:::tip 设计要点
|
||||
缓存写入发生在"读取回源"时,失效只做 `forget` 不做主动重建——**删除缓存比更新缓存更安全**,避免并发下旧数据回写。
|
||||
:::
|
||||
|
||||
## 缓存驱动配置
|
||||
|
||||
`.env` 配置:
|
||||
|
||||
```ini
|
||||
; 文件缓存(默认)
|
||||
[CACHE]
|
||||
DRIVER = file
|
||||
|
||||
; Redis 缓存(推荐生产环境)
|
||||
[CACHE]
|
||||
DRIVER = redis
|
||||
REDIS_HOST = 127.0.0.1
|
||||
REDIS_PORT = 6379
|
||||
REDIS_PASSWORD =
|
||||
REDIS_DB = 0
|
||||
# Redis 缓存(项目默认,必需)
|
||||
CACHE_STORE=redis
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=null # 设置了 requirepass 时必须同步
|
||||
```
|
||||
|
||||
:::warning 生产环境建议使用 Redis
|
||||
文件缓存在多进程/多服务器环境下无法共享,生产环境建议配置 Redis 缓存驱动。使用 Redis 前需安装 `php redis` 扩展。
|
||||
:::warning 不要改回 database/file 驱动
|
||||
- `CACHE_STORE=database`:数据库脚本**不含** Laravel `cache` 表,会直接报表不存在;
|
||||
- `file` 驱动:多进程/多服务器环境下无法共享,且本项目 JWT 会话、权限缓存均依赖 Redis;
|
||||
- 会话 / 缓存 / 队列三者统一使用 Redis(`SESSION_DRIVER` / `CACHE_STORE` / `QUEUE_CONNECTION`)。
|
||||
:::
|
||||
|
||||
## 缓存清除
|
||||
|
||||
```php
|
||||
// 清除指定字典缓存
|
||||
DictService::clearCache('gender');
|
||||
// 清除指定字典缓存(Observer 已自动调用,此处为手动兜底)
|
||||
DictCacheService::clearCache('gender');
|
||||
|
||||
// 清除所有字典缓存
|
||||
DictService::clearAllCache();
|
||||
|
||||
// 清除指定参数缓存
|
||||
ParamService::clearCache('DEFAULT_PASSWORD');
|
||||
|
||||
// 清除所有参数缓存
|
||||
ParamService::clearAllCache();
|
||||
DictCacheService::clearAllCache();
|
||||
```
|
||||
|
||||
对应接口:
|
||||
对应接口与命令:
|
||||
|
||||
```text
|
||||
GET /api/dict/refreshCache 刷新字典缓存
|
||||
```
|
||||
GET /api/dict/refreshCache 刷新字典缓存(需认证)
|
||||
php artisan dict:clear-cache 命令行清除字典缓存
|
||||
php artisan config:clear 清除配置缓存(修改 .env 后)
|
||||
php artisan cache:clear 清除全部应用缓存
|
||||
```
|
||||
|
||||
## Redis 使用全景
|
||||
|
||||
| 用途 | 使用方 | 说明 |
|
||||
|------|--------|------|
|
||||
| JWT 双令牌会话 | `JwtService` | refresh_token 存 Redis(支持踢下线、改密全端失效、一次性消费) |
|
||||
| 字典缓存 | `DictCacheService` | 两级缓存第二级 |
|
||||
| 权限/菜单缓存 | `Authenticate` 中间件 + Observer | 角色权限列表 |
|
||||
| 验证码 | `CaptchaService` | 图形验证码存取 + IP 限流 |
|
||||
| 会话/队列 | Laravel 驱动 | `SESSION_DRIVER` / `QUEUE_CONNECTION` |
|
||||
|
||||
连接验证:
|
||||
|
||||
```bash
|
||||
php artisan tinker --execute="try { \Illuminate\Support\Facades\Redis::setex('test:key', 10, 'ok'); echo 'OK: '.\Illuminate\Support\Facades\Redis::get('test:key'); } catch (\Throwable \$e) { echo \$e->getMessage(); }"
|
||||
# 输出 OK: ok 即连接正常
|
||||
```
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
# 4.7 横切机制
|
||||
|
||||
:::tip 概述
|
||||
横切机制是 BaseLogic/BaseModel 内置的自动处理能力,开发者通过属性配置启用,无需手动编写处理代码。项目共有 9 种横切机制,覆盖数据转换、文件处理、校验、权限、隔离等维度。
|
||||
横切机制是 `BaseService` / 模型 Trait 内置的自动处理能力,开发者通过属性配置启用,无需手动编写处理代码。项目共有 9 种横切机制,覆盖数据转换、文件处理、校验、权限、隔离等维度;参数格式校验则由 HTTP 入口的 FormRequest 承担。
|
||||
:::
|
||||
|
||||
## 横切机制总览
|
||||
|
||||
| 机制 | 配置位置 | 配置属性 | 触发时机 |
|
||||
|------|---------|---------|---------|
|
||||
| 驼峰/下划线转换 | `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 |
|
||||
| 驼峰/下划线转换 | `config/api.php` | `camel_snake_convert` | 查询参数入、响应出 |
|
||||
| 文件字段处理 | Service(需 `use HasFileProcessing`) | `fileFields` + `fileSaveDir` | add/update 写入,detail/page 读取 |
|
||||
| 多文件字段处理 | Service(需 `use HasFileProcessing`) | `multiFileFields` + `fileSaveDir` | add/update 写入,detail/page 读取 |
|
||||
| 富文本占位符 | Service(需 `use HasFileProcessing`) | `contentFields` | add/update 写入,detail/page 读取 |
|
||||
| 配置值类型化处理 | Service(需 `use HasConfigValueProcessing`) | 记录的 `type` + `fileSaveDir` | add/update 写入,detail/webIndex 读取 |
|
||||
| 字典显示名翻译 | Service(需 `use HasSerializeMaps`) | `serializeMaps` | detail/page 查询后 |
|
||||
| 唯一性校验 | Service(需 `use HasUniqueCheck`) | `uniqueFields` | add/update 时 |
|
||||
| 数据权限过滤 | Service(需 `use HasDataScope`) | `dataScopeUserField` / `dataScopeDeptField` | 查询时 |
|
||||
| 租户隔离 | `BaseTenantService`(自动 use HasDataScope) | `tenantScopeField` | 查询时 + 新增时 |
|
||||
| 审计字段 + 软删除 | 模型 Trait | 自动生效 | 所有 CRUD |
|
||||
|
||||
:::tip 参数格式校验在哪里?
|
||||
不在上表——Laravel 将其前置到 HTTP 入口:控制器方法**类型注入 FormRequest** 即自动校验(见 [3.4 FormRequest 验证器](/zh/guide/validator.html))。Service 只做业务规则(唯一性、状态依赖)。
|
||||
:::
|
||||
|
||||
## 1. 驼峰/下划线自动转换
|
||||
|
||||
@@ -29,52 +33,61 @@
|
||||
请求方向(前端 → 数据库):
|
||||
前端传参:{ userName: "admin", createTime: "2026-01-01" }
|
||||
│
|
||||
▼ BaseLogic::convertQueryParams()
|
||||
│ array_camel_to_snake($params, $except)
|
||||
▼ HasQueryScopes::convertQueryParams()
|
||||
│ camel_to_snake() 逐键转换(排除分页/排序参数)
|
||||
▼
|
||||
数据库操作:WHERE user_name = 'admin' AND create_time = '2026-01-01'
|
||||
|
||||
表单入参(add/update 写入):
|
||||
$request->validated() ← 格式校验,原样传递(camelCase 键名)
|
||||
│
|
||||
▼ HasCrudOperations::doAdd / doUpdate 入口
|
||||
│ array_camel_to_snake() 统一转换
|
||||
▼
|
||||
入库数据:snake_case 键名(非表字段由 filterTableFields 过滤)
|
||||
|
||||
响应方向(数据库 → 前端):
|
||||
数据库数据:{ user_name: "admin", create_time: "2026-01-01" }
|
||||
│
|
||||
▼ Result::convertData()
|
||||
│ array_snake_to_camel($data)
|
||||
▼ Result::success() / page()
|
||||
│ convert_to_camel() / convert_list_to_camel()
|
||||
▼
|
||||
前端接收:{ userName: "admin", createTime: "2026-01-01" }
|
||||
```
|
||||
|
||||
### 排除项(不参与转换)
|
||||
### 排除项(不参与请求方向转换)
|
||||
|
||||
以下参数保持 camelCase,不转为 snake_case:
|
||||
以下参数保持 camelCase,不转为 snake_case(`config('api.convert_except_params')`):
|
||||
|
||||
- `pageNo`、`pageSize` — 分页参数
|
||||
- `orderField`、`orderType` — 排序参数
|
||||
- `fields` — 字段过滤参数
|
||||
|
||||
### 配置开关
|
||||
|
||||
```php
|
||||
// config/api.php
|
||||
'camel_snake_convert' => env('api.camel_snake_convert', true),
|
||||
'camel_snake_convert' => env('API_CAMEL_SNAKE_CONVERT', true),
|
||||
```
|
||||
|
||||
```ini
|
||||
# .env
|
||||
API.CAMEL_SNAKE_CONVERT = true # 开启(默认)
|
||||
API.CAMEL_SNAKE_CONVERT = false # 关闭(请求保持驼峰,响应保持下划线)
|
||||
API_CAMEL_SNAKE_CONVERT=true # 开启(默认)
|
||||
API_CAMEL_SNAKE_CONVERT=false # 关闭(请求保持驼峰,响应保持下划线)
|
||||
```
|
||||
|
||||
:::tip 开发者无需手动处理
|
||||
驼峰/下划线转换完全自动,开发者只需在前端用 camelCase,在数据库用 snake_case,框架自动处理双向转换。
|
||||
驼峰/下划线转换完全自动,开发者只需在前端用 camelCase,在数据库用 snake_case,框架自动处理双向转换。需要原样返回时用 `Result::success($data, $msg, true)`(`$raw=true`)。
|
||||
:::
|
||||
|
||||
## 2. 文件字段自动处理
|
||||
|
||||
**配置方式**:Logic 中设置 `fileFields` 和 `fileSaveDir`
|
||||
**配置方式**:Service 中 `use HasFileProcessing` 并设置 `fileFields` 和 `fileSaveDir`
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
class ArticleService extends BaseTenantService
|
||||
{
|
||||
use HasFileProcessing, /* 其它 Trait */;
|
||||
|
||||
protected array $fileFields = ['cover', 'attachment'];
|
||||
protected string $fileSaveDir = 'article';
|
||||
}
|
||||
@@ -87,21 +100,21 @@ class ArticleLogic extends BaseTenantLogic
|
||||
│
|
||||
▼ processFileFieldsOnSave()
|
||||
│ 1. 检测字段值是否为临时文件 URL
|
||||
│ 2. 调用 save_file() 将临时文件迁移到正式目录
|
||||
│ 2. 将临时文件迁移到正式目录(fileSaveDir)
|
||||
│ 3. 去掉域名,只存储相对路径
|
||||
▼
|
||||
入库存储:cover = "/uploads/article/20260922/abc123.jpg"
|
||||
入库存储:cover = "article/20261002/abc123.jpg"
|
||||
```
|
||||
|
||||
### 读取时(detail/pageList/allList)
|
||||
|
||||
```text
|
||||
数据库读取:cover = "/uploads/article/20260922/abc123.jpg"
|
||||
数据库读取:cover = "article/20261002/abc123.jpg"
|
||||
│
|
||||
▼ processFileFieldsOnRead()
|
||||
│ 调用 get_file_url() 补全域名
|
||||
│ 补全完整访问 URL(config/file.php 的 domain_url)
|
||||
▼
|
||||
前端接收:cover = "https://cdn.example.com/uploads/article/20260922/abc123.jpg"
|
||||
前端接收:cover = "https://cdn.example.com/uploads/article/20261002/abc123.jpg"
|
||||
```
|
||||
|
||||
### 多个单文件字段
|
||||
@@ -111,18 +124,19 @@ protected array $fileFields = ['cover', 'avatar', 'attachment'];
|
||||
// 三个字段都会自动处理
|
||||
```
|
||||
|
||||
:::warning 未 use Trait 时为空操作
|
||||
`BaseService` 中对应方法是**占位实现**(直接 `return $data`)。未 `use HasFileProcessing` 时文件字段不会被处理——这是刻意设计(见 [4.6 核心基类体系](/zh/architecture/base-classes.html)「占位覆盖机制」)。
|
||||
:::
|
||||
|
||||
## 3. 多文件字段自动处理
|
||||
|
||||
当一个字段存储多个文件URL(逗号分隔)时,使用 `multiFileFields`:
|
||||
当一个字段存储多个文件 URL(逗号分隔)时,使用 `multiFileFields`:
|
||||
|
||||
**配置方式**:Logic 中设置 `multiFileFields`(复用 `fileSaveDir`)
|
||||
**配置方式**:Service 中设置 `multiFileFields`(复用 `fileSaveDir`)
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
{
|
||||
protected array $multiFileFields = ['images', 'attachments'];
|
||||
protected string $fileSaveDir = 'article';
|
||||
}
|
||||
protected array $multiFileFields = ['images', 'attachments'];
|
||||
protected string $fileSaveDir = 'article';
|
||||
```
|
||||
|
||||
**写入时(add/update)**
|
||||
@@ -132,34 +146,29 @@ class ArticleLogic extends BaseTenantLogic
|
||||
│
|
||||
▼ processMultiFileFieldsOnSave()
|
||||
│ 1. 按逗号拆分为数组
|
||||
│ 2. 逐个调用 save_file() 迁移临时文件并去掉域名
|
||||
│ 2. 逐个迁移临时文件并去掉域名
|
||||
│ 3. 重新用逗号拼接
|
||||
▼
|
||||
入库存储:images = "article/20260922/a.png,article/20260922/b.png"
|
||||
入库存储:images = "article/20261002/a.png,article/20261002/b.png"
|
||||
```
|
||||
|
||||
**读取时(detail/pageList/allList)**
|
||||
|
||||
```text
|
||||
数据库读取:images = "article/20260922/a.png,article/20260922/b.png"
|
||||
数据库读取:images = "article/20261002/a.png,article/20261002/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"
|
||||
前端接收:images = "https://cdn.example.com/.../a.png,https://cdn.example.com/.../b.png"
|
||||
```
|
||||
|
||||
## 4. 富文本占位符机制
|
||||
|
||||
**配置方式**:Logic 中设置 `contentFields`
|
||||
**配置方式**:Service 中设置 `contentFields`
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
{
|
||||
protected array $contentFields = ['content'];
|
||||
}
|
||||
protected array $contentFields = ['content'];
|
||||
```
|
||||
|
||||
### 写入时
|
||||
@@ -178,35 +187,32 @@ class ArticleLogic extends BaseTenantLogic
|
||||
### 读取时
|
||||
|
||||
```text
|
||||
数据库读取:content = '<p>文章内容</p><img src="[IMG_URL]/uploads/article/img001.jpg">'
|
||||
数据库读取:content 中含 [IMG_URL] 占位符
|
||||
│
|
||||
▼ processContentFieldsOnRead()
|
||||
│ 将 [IMG_URL] 替换为配置的实际域名
|
||||
▼
|
||||
前端接收:content = '<p>文章内容</p><img src="https://cdn.example.com/uploads/article/img001.jpg">'
|
||||
前端接收:content 中 img src 为完整域名 URL
|
||||
```
|
||||
|
||||
### 好处
|
||||
|
||||
域名变更时只需修改 `config/file.php` 中的 `domain_url`,已存储的内容无需逐条更新。
|
||||
域名变更时只需修改 `.env` 中的 `FILE_DOMAIN_URL`,已存储的内容无需逐条更新。
|
||||
|
||||
## 5. 枚举显示名翻译
|
||||
## 5. 字典显示名翻译
|
||||
|
||||
**配置方式**:Logic 中设置 `serializeMaps`
|
||||
**配置方式**:Service 中 `use HasSerializeMaps` 并设置 `serializeMaps`
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'status' => 'article_status',
|
||||
'type' => 'article_type',
|
||||
];
|
||||
}
|
||||
/**
|
||||
* 字典翻译配置:[字段 => 字典编码]
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'status' => 'article_status',
|
||||
'type' => 'article_type',
|
||||
];
|
||||
```
|
||||
|
||||
### 工作原理
|
||||
@@ -214,84 +220,34 @@ class ArticleLogic extends BaseTenantLogic
|
||||
```text
|
||||
数据库查询:{ status: 1, type: 0 }
|
||||
│
|
||||
▼ processSerializeMaps()
|
||||
│ DictService::getText('article_status', 1) → '已发布'
|
||||
│ DictService::getText('article_type', 0) → '原创'
|
||||
▼ processSerializeMapsList()
|
||||
│ DictCacheService 批量取词(两级缓存,同一请求只查一次)
|
||||
│ getText('article_status', 1) → '已发布'
|
||||
│ getText('article_type', 0) → '原创'
|
||||
▼
|
||||
前端接收:{ status: 1, statusText: '已发布', type: 0, typeText: '原创' }
|
||||
```
|
||||
|
||||
### 字段名映射
|
||||
|
||||
当字典编码与字段名不同时,可指定映射:
|
||||
当字典编码与字段名不同时,直接在映射中指定:
|
||||
|
||||
```php
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'gender' => 'gender',
|
||||
'status' => 'user_status',
|
||||
];
|
||||
```
|
||||
|
||||
## 6. 自动参数校验
|
||||
字典数据变更时 `DictItemObserver` 自动清除对应缓存,保证翻译不过期。
|
||||
|
||||
**配置方式**:Logic 中设置 `validateClass`
|
||||
## 6. 唯一性校验
|
||||
|
||||
**配置方式**:Service 中 `use HasUniqueCheck` 并设置 `uniqueFields`
|
||||
|
||||
```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'];
|
||||
}
|
||||
// 全局唯一
|
||||
protected array $uniqueFields = ['username', 'mobile'];
|
||||
```
|
||||
|
||||
### 两种规则
|
||||
@@ -300,7 +256,7 @@ class UserLogic extends BaseTenantLogic
|
||||
// 字符串 — 全局唯一
|
||||
protected array $uniqueFields = ['username', 'mobile'];
|
||||
|
||||
// 数组 — 分组内唯一
|
||||
// 数组 — 分组内唯一:[唯一字段, 分组字段]
|
||||
protected array $uniqueFields = [
|
||||
'username', // username 全局唯一
|
||||
['code', 'pid'], // code 在同一 pid 下唯一
|
||||
@@ -312,57 +268,45 @@ protected array $uniqueFields = [
|
||||
```text
|
||||
add() 时:
|
||||
checkUnique($data)
|
||||
→ SELECT * FROM think_user WHERE username = 'admin'
|
||||
→ 存在 → throw new \Exception('用户名已存在')
|
||||
→ SELECT * FROM laravel_user WHERE username = 'admin'
|
||||
→ 存在 → throw new BizException('用户名已存在')
|
||||
|
||||
update() 时:
|
||||
checkUnique($data, $excludeId)
|
||||
→ SELECT * FROM think_user WHERE username = 'admin' AND id <> 42
|
||||
→ 存在 → throw new \Exception('用户名已存在')
|
||||
→ SELECT * FROM laravel_user WHERE username = 'admin' AND id <> 42
|
||||
→ 存在 → throw new BizException('用户名已存在')
|
||||
```
|
||||
|
||||
- 值为空时跳过(FormRequest 的 `required` 规则兜底)
|
||||
- `BizException` 中断 `DB::transaction` 事务并回滚,由 Handler 转为 `code=1` 响应
|
||||
|
||||
### 错误提示自定义
|
||||
|
||||
默认提示格式:"字段中文名 + 已存在"。可通过重写 `getFieldLabel()` 自定义:
|
||||
默认提示格式:"字段中文名 + 已存在"。中文名查找顺序:`$fieldLabels` 属性 → Trait 内置映射 → 字段名本身:
|
||||
|
||||
```php
|
||||
protected function getFieldLabel(string $field): string
|
||||
class RoleService extends BaseTenantService
|
||||
{
|
||||
$map = [
|
||||
'username' => '用户名',
|
||||
'mobile' => '手机号',
|
||||
'code' => '编码',
|
||||
// 自定义字段中文名映射
|
||||
protected array $fieldLabels = [
|
||||
'code' => '角色编码',
|
||||
'name' => '角色名称',
|
||||
];
|
||||
return $map[$field] ?? $field;
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 数据权限过滤
|
||||
## 7. 数据权限过滤
|
||||
|
||||
**配置方式**:Logic 中设置 `dataScopeUserField` 和/或 `dataScopeDeptField`
|
||||
**配置方式**:Service 中设置 `dataScopeUserField` 和/或 `dataScopeDeptField`(需 `use HasDataScope`)
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 数据权限:归属字段名(用于按创建人过滤)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $dataScopeUserField = 'create_user';
|
||||
|
||||
/**
|
||||
* 数据权限:部门关联字段名(用于按部门过滤)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $dataScopeDeptField = 'dept_id';
|
||||
}
|
||||
protected string $dataScopeUserField = 'create_user'; // 按创建人过滤
|
||||
protected string $dataScopeDeptField = 'dept_id'; // 按部门过滤
|
||||
```
|
||||
|
||||
### 三种权限范围
|
||||
|
||||
由角色表 `think_role` 的 `data_scope` 字段决定:
|
||||
数据范围由 `HasDataScope::getRoleDataScope()` 按角色解析。当前 `laravel_role` 表不含 `data_scope` 字段,读取缺省为 1——所有角色按全部数据处理:
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
@@ -376,7 +320,7 @@ class ArticleLogic extends BaseTenantLogic
|
||||
│ │ A │ │ B │ │███│ │ D │ │ E │ ← 只看到本部门数据 │
|
||||
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ data_scope = 3 → 仅本人数据 │
|
||||
│ data_scope = 3 → 仅本人数据(无角色用户默认) │
|
||||
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
|
||||
│ │ A │ │ B │ │███│ │ D │ │ E │ ← 只看到自己创建的数据 │
|
||||
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
|
||||
@@ -387,25 +331,25 @@ class ArticleLogic extends BaseTenantLogic
|
||||
|
||||
```text
|
||||
查询时 applyDataScope():
|
||||
1. 获取当前用户角色 ID
|
||||
2. 查询角色的 data_scope
|
||||
1. 获取当前用户角色 ID(无角色 → 按 3 仅本人)
|
||||
2. 读取角色数据范围(当前无该字段 → 按 1 全部)
|
||||
3. scope=1 → 不过滤
|
||||
4. scope=2 → WHERE dept_id = 当前用户部门ID
|
||||
5. scope=3 → WHERE create_user = 当前用户名
|
||||
4. scope=2 → WHERE dept_id = 当前用户部门ID(需声明 dataScopeDeptField)
|
||||
5. scope=3 → WHERE create_user = 当前用户名(需声明 dataScopeUserField)
|
||||
```
|
||||
|
||||
:::warning 无角色用户
|
||||
无角色时默认按"仅本人"(scope=3)处理。
|
||||
:::
|
||||
|
||||
## 9. 租户隔离
|
||||
## 8. 租户隔离
|
||||
|
||||
**配置方式**:继承 `BaseTenantLogic`(自动设置 `tenantScopeField = 'tenant_id'`)
|
||||
**配置方式**:继承 `BaseTenantService`(自动 `use HasDataScope` 并设置 `tenantScopeField = 'tenant_id'`)
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic // 继承 BaseTenantLogic 即启用
|
||||
class ArticleService extends BaseTenantService
|
||||
{
|
||||
// tenantScopeField 已由基类设为 'tenant_id',无需再声明
|
||||
// tenantScopeField 基类默认即为 'tenant_id',无需再声明
|
||||
}
|
||||
```
|
||||
|
||||
@@ -415,48 +359,48 @@ class ArticleLogic extends BaseTenantLogic // 继承 BaseTenantLogic 即启用
|
||||
|------|------|---------|
|
||||
| **查询时** | 自动叠加 `WHERE tenant_id = 当前租户ID` | `applyTenantScope()` |
|
||||
| **新增时** | 自动填充 `tenant_id` 字段 | `processTenantId()` |
|
||||
| **超级管理员**(tenantId=0) | 查询不过滤,可跨租户查看 | 条件判断跳过 |
|
||||
| **超级管理员新增** | 自动代入默认租户(tenant_id=1) | 读取配置 `tenant.default_tenant_id` |
|
||||
| **平台用户**(tenantId=0) | 查询不过滤,可跨租户查看 | 条件判断跳过 |
|
||||
| **平台用户新增** | 自动代入默认租户(DEFAULT_TENANT_ID) | 读取 `config/tenant.php` |
|
||||
|
||||
### 租户上下文来源
|
||||
|
||||
`TenantMiddleware` 解析 JWT 中的租户信息,注入 `$request->tenantId`。
|
||||
`ResolveTenant` 中间件解析并注入 `request->attributes['tenantId']`,并校验租户状态与有效期(详见 [4.2 请求处理链路](/zh/architecture/request-flow.html))。
|
||||
|
||||
### 适用范围
|
||||
|
||||
- 需要租户隔离的模块:继承 `BaseTenantLogic`(User、Article、Dept、Level 等)
|
||||
- 系统共享数据:继承 `BaseLogic`(Menu、Role、Dict、Config 等)
|
||||
- 需要租户隔离的模块:继承 `BaseTenantService`(User、Article、Dept、Level 等)
|
||||
- 系统共享数据:继承 `BaseService`(Menu、Dict、Config 等)
|
||||
|
||||
## 10. 审计字段 + 软删除(BaseModel)
|
||||
## 9. 审计字段 + 软删除(模型 Trait)
|
||||
|
||||
**配置方式**:继承 `BaseModel` 即自动生效,无需额外配置。
|
||||
**配置方式**:模型 `use HasAuditColumns, SoftDeletesByFlag` 即自动生效,无需额外配置。
|
||||
|
||||
### 软删除
|
||||
|
||||
```text
|
||||
全局查询范围 soft_delete:
|
||||
全局查询范围 SoftDeleteFlagScope:
|
||||
所有查询自动叠加 WHERE is_delete = 0
|
||||
→ 已删除数据默认不可见
|
||||
|
||||
软删除操作:
|
||||
$model->softDelete() → UPDATE SET is_delete = 1
|
||||
BaseModel::batchSoftDelete($ids) → 批量软删除
|
||||
Model::batchSoftDelete($ids) → 批量软删除
|
||||
|
||||
查询已删除数据:
|
||||
Model::withoutGlobalScope(['soft_delete'])->...
|
||||
Model::withoutGlobalScope(SoftDeleteFlagScope::class)->...
|
||||
```
|
||||
|
||||
### 审计字段自动写入
|
||||
|
||||
| 事件 | 自动填充字段 | 数据来源 |
|
||||
|------|------------|---------|
|
||||
| `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')`(始终覆盖) |
|
||||
| `creating`(创建前) | `create_user` | `Auth::username()`(当前登录用户) |
|
||||
| `creating`(创建前) | `create_time` | `now()`(已赋值则保留) |
|
||||
| `updating`(更新前) | `update_user` | `Auth::username()` |
|
||||
| `updating`(更新前) | `update_time` | `now()`(始终覆盖) |
|
||||
|
||||
:::tip 无用户场景
|
||||
控制台任务、定时任务等无登录用户的场景下,跳过用户字段,时间字段仍写入。
|
||||
控制台任务、定时任务等无登录用户的场景下,用户字段保留空值(可显式赋值跳过),时间字段仍写入。
|
||||
:::
|
||||
|
||||
### 数据库公共字段
|
||||
@@ -477,14 +421,14 @@ update_time DATETIME COMMENT '更新时间'
|
||||
一个模块可以同时启用多种横切机制,它们互不干扰:
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
class ArticleService extends BaseTenantService
|
||||
{
|
||||
// 租户隔离(继承自动启用:use HasDataScope + tenantScopeField)
|
||||
use HasCrudOperations, HasQueryScopes, HasFileProcessing, HasSerializeMaps, HasUniqueCheck;
|
||||
|
||||
protected string $modelClass = Article::class;
|
||||
|
||||
// 自动参数校验
|
||||
protected string $validateClass = ArticleValidate::class;
|
||||
|
||||
// 文件字段处理
|
||||
// 文件字段处理(需 HasFileProcessing)
|
||||
protected array $fileFields = ['cover'];
|
||||
protected string $fileSaveDir = 'article';
|
||||
|
||||
@@ -494,18 +438,16 @@ class ArticleLogic extends BaseTenantLogic
|
||||
// 富文本占位符
|
||||
protected array $contentFields = ['content'];
|
||||
|
||||
// 枚举显示名翻译
|
||||
// 字典显示名翻译(需 HasSerializeMaps)
|
||||
protected array $serializeMaps = ['status' => 'article_status'];
|
||||
|
||||
// 唯一性校验
|
||||
// 唯一性校验(需 HasUniqueCheck)
|
||||
protected array $uniqueFields = ['title'];
|
||||
|
||||
// 数据权限
|
||||
// 数据权限(需 HasDataScope,租户版基类已 use)
|
||||
protected string $dataScopeUserField = 'create_user';
|
||||
|
||||
// 租户隔离(继承 BaseTenantLogic 自动启用)
|
||||
|
||||
// 审计字段 + 软删除(继承 BaseModel 自动生效)
|
||||
// 审计字段 + 软删除(模型 Trait 自动生效)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -514,23 +456,20 @@ class ArticleLogic extends BaseTenantLogic
|
||||
```text
|
||||
beforeAdd ← 钩子
|
||||
▼
|
||||
validateData ← 自动参数校验
|
||||
(入口) FormRequest ← 参数格式校验(rules)
|
||||
▼
|
||||
camel_to_snake ← 驼峰转下划线
|
||||
checkUnique ← 唯一性校验(BizException 拦截)
|
||||
▼
|
||||
processTenantId ← 租户ID填充
|
||||
▼
|
||||
processFileFields ← 文件字段处理(单值)
|
||||
▼
|
||||
processMultiFileFields ← 多文件字段处理(逗号分隔多值)
|
||||
▼
|
||||
processContentFields ← 富文本处理
|
||||
▼
|
||||
filterTableFields ← 过滤非数据库字段
|
||||
▼
|
||||
checkUnique ← 唯一性校验
|
||||
▼
|
||||
Model::create ← 审计字段自动填充
|
||||
▼
|
||||
afterAdd ← 钩子
|
||||
DB::transaction {
|
||||
processTenantId ← 租户ID填充(HasDataScope 启用时)
|
||||
processFileFields ← 文件字段处理(单值)
|
||||
processMultiFileFields ← 多文件字段处理(逗号分隔多值)
|
||||
processContentFields ← 富文本处理
|
||||
Model::create ← 审计字段自动填充(creating 事件)
|
||||
afterAdd ← 钩子
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 设计价值
|
||||
开发者只需声明"这个字段是文件"(`fileFields`)、"这个字段要唯一"(`uniqueFields`),全部处理逻辑由基类与 Trait 统一完成——改动一处,全模块受益。
|
||||
:::
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
# 4.9 数据库设计
|
||||
|
||||
:::tip 概述
|
||||
项目支持 MySQL、PostgreSQL、SQL Server、Oracle、SQLite 五种数据库,通过 `.env` 的 `DATABASE.TYPE` 一键切换。所有数据库共用同一套代码,无需修改业务逻辑。本章涵盖命名规范、表结构设计、字段说明、索引策略和多数据库适配。
|
||||
项目支持 MySQL、PostgreSQL、SQL Server、Oracle、SQLite 五种数据库,通过 `.env` 的 `DB_CONNECTION` 一键切换,`document/` 下提供五套初始化脚本。所有数据库共用同一套代码,无需修改业务逻辑。本章涵盖命名规范、表结构设计、字段说明、索引策略和多数据库适配。
|
||||
:::
|
||||
|
||||
## 命名规范
|
||||
|
||||
| 对象 | 规范 | 示例 |
|
||||
|------|------|------|
|
||||
| 表名 | `think_` 前缀 + 小写下划线 | `think_user`、`think_article` |
|
||||
| 表名 | 不含前缀的业务名 + `DB_PREFIX`(默认 `laravel_`) | `laravel_user`、`laravel_article` |
|
||||
| 模型 `$table` | **不含前缀** | `protected $table = 'user'` |
|
||||
| 字段名 | 小写下划线 | `create_time`、`user_name` |
|
||||
| 主键 | 统一 `id`,INT 自增 | `id INT PRIMARY KEY` |
|
||||
| 外键 | `表名_id` | `user_id`、`role_id` |
|
||||
| 索引 | `idx_字段名` | `idx_status`、`idx_tenant_id` |
|
||||
| 唯一索引 | `uk_字段名` | `uk_username` |
|
||||
| 唯一索引 | `uk_含义`(常含 `is_delete` 协同) | `uk_tenant_username` |
|
||||
|
||||
## 公共字段
|
||||
|
||||
所有业务表**必须**包含以下公共字段,由 `BaseModel` 自动维护:
|
||||
所有业务表**必须**包含以下公共字段,由模型 Trait 自动维护:
|
||||
|
||||
```sql
|
||||
-- MySQL 格式
|
||||
@@ -31,183 +32,183 @@ 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` | 始终覆盖为当前时间 |
|
||||
| `is_delete` | `SoftDeletesByFlag` | `softDelete()` 设置为 1,`SoftDeleteFlagScope` 全局过滤 |
|
||||
| `create_user` | `HasAuditColumns` creating 事件 | 取 `Auth::username()`,已传入则不覆盖 |
|
||||
| `create_time` | `HasAuditColumns` creating 事件 | 取 `now()`,已传入则不覆盖 |
|
||||
| `update_user` | `HasAuditColumns` updating 事件 | 取 `Auth::username()` |
|
||||
| `update_time` | `HasAuditColumns` updating 事件 | 始终覆盖为当前时间 |
|
||||
|
||||
## 核心表清单
|
||||
## 核心表清单(25 张)
|
||||
|
||||
### 用户与权限(6 张表)
|
||||
### 用户与权限(5 张)
|
||||
|
||||
| 表名 | 说明 | Logic 基类 |
|
||||
| 表名 | 说明 | Service 基类 |
|
||||
|------|------|-----------|
|
||||
| `think_user` | 用户表 | BaseTenantLogic |
|
||||
| `think_user_role` | 用户角色关联表(N:N 中间表) | — |
|
||||
| `think_role` | 角色表 | BaseLogic |
|
||||
| `think_role_menu` | 角色菜单关联表(N:N 中间表) | — |
|
||||
| `think_menu` | 菜单/权限表 | BaseLogic |
|
||||
| `think_dept` | 部门表(树形) | BaseTenantLogic |
|
||||
| `laravel_user` | 用户表 | `BaseTenantService` |
|
||||
| `laravel_user_role` | 用户角色关联表(N:N 中间表) | — |
|
||||
| `laravel_role` | 角色表 | `BaseTenantService` |
|
||||
| `laravel_role_menu` | 角色菜单关联表(N:N 中间表) | — |
|
||||
| `laravel_menu` | 菜单/权限表(菜单即权限) | `BaseService` |
|
||||
|
||||
**关联关系:**
|
||||
|
||||
```text
|
||||
think_user ──┐
|
||||
│ N:N (think_user_role)
|
||||
think_role ──┤
|
||||
│ N:N (think_role_menu)
|
||||
think_menu ──┘
|
||||
laravel_user ──┐
|
||||
│ N:N (laravel_user_role)
|
||||
laravel_role ──┤
|
||||
│ N:N (laravel_role_menu)
|
||||
laravel_menu ──┘
|
||||
|
||||
think_dept ←── think_user.dept_id
|
||||
think_tenant ←── think_user.tenant_id
|
||||
laravel_dept ←── laravel_user.dept_id
|
||||
laravel_tenant ←── laravel_user.tenant_id
|
||||
```
|
||||
|
||||
### 组织架构(3 张表)
|
||||
### 组织架构(3 张)
|
||||
|
||||
| 表名 | 说明 | Logic 基类 |
|
||||
| 表名 | 说明 | Service 基类 |
|
||||
|------|------|-----------|
|
||||
| `think_dept` | 部门表(树形,parent_id) | BaseTenantLogic |
|
||||
| `think_position` | 岗位表 | BaseTenantLogic |
|
||||
| `think_level` | 职级表 | BaseTenantLogic |
|
||||
| `laravel_dept` | 部门表(树形,parent_id) | `BaseTenantService` |
|
||||
| `laravel_position` | 岗位表 | `BaseTenantService` |
|
||||
| `laravel_level` | 职级表 | `BaseTenantService` |
|
||||
|
||||
### 租户(1 张表)
|
||||
### 租户(1 张)
|
||||
|
||||
| 表名 | 说明 | Logic 基类 |
|
||||
| 表名 | 说明 | Service 基类 |
|
||||
|------|------|-----------|
|
||||
| `think_tenant` | 租户表 | BaseLogic |
|
||||
| `laravel_tenant` | 租户表(状态、过期时间、用户数限制) | `BaseService` |
|
||||
|
||||
### 系统配置(5 张表)
|
||||
### 系统配置(5 张)
|
||||
|
||||
| 表名 | 说明 | Logic 基类 |
|
||||
| 表名 | 说明 | Service 基类 |
|
||||
|------|------|-----------|
|
||||
| `think_dict` | 字典表 | BaseLogic |
|
||||
| `think_dict_item` | 字典项表 | BaseLogic |
|
||||
| `think_config` | 配置组表 | BaseLogic |
|
||||
| `think_config_item` | 配置项表 | BaseLogic |
|
||||
| `think_param` | 系统参数表 | BaseLogic |
|
||||
| `laravel_dict` | 字典表 | `BaseService` |
|
||||
| `laravel_dict_item` | 字典项表 | `BaseService` |
|
||||
| `laravel_config` | 配置组表 | `BaseService` |
|
||||
| `laravel_config_item` | 配置项表 | `BaseService` |
|
||||
| `laravel_param` | 系统参数表 | `BaseService` |
|
||||
|
||||
**关联关系:**
|
||||
|
||||
```text
|
||||
think_dict ←── think_dict_item.dict_id
|
||||
think_config ←── think_config_item.config_id
|
||||
laravel_dict ←── laravel_dict_item.dict_id
|
||||
laravel_config ←── laravel_config_item.config_id
|
||||
```
|
||||
|
||||
### 内容管理(4 张表)
|
||||
### 内容管理(4 张)
|
||||
|
||||
| 表名 | 说明 | Logic 基类 |
|
||||
| 表名 | 说明 | Service 基类 |
|
||||
|------|------|-----------|
|
||||
| `think_article` | 文章表 | BaseTenantLogic |
|
||||
| `think_category` | 文章分类表(树形) | BaseTenantLogic |
|
||||
| `think_link` | 友情链接表 | BaseTenantLogic |
|
||||
| `think_notice` | 通知公告表 | BaseTenantLogic |
|
||||
| `laravel_article` | 文章表 | `BaseTenantService` |
|
||||
| `laravel_category` | 文章分类表(树形) | `BaseTenantService` |
|
||||
| `laravel_link` | 友情链接表 | `BaseTenantService` |
|
||||
| `laravel_notice` | 通知公告表 | `BaseTenantService` |
|
||||
|
||||
### 日志审计(2 张表)
|
||||
### 日志审计(2 张)
|
||||
|
||||
| 表名 | 说明 | Logic 基类 |
|
||||
| 表名 | 说明 | Service 基类 |
|
||||
|------|------|-----------|
|
||||
| `think_login_log` | 登录日志表 | BaseLogic |
|
||||
| `think_operation_log` | 操作日志表 | BaseLogic |
|
||||
| `laravel_login_log` | 登录日志表 | `BaseService` |
|
||||
| `laravel_operation_log` | 操作日志表 | `BaseService` |
|
||||
|
||||
### 定时任务(2 张表)
|
||||
### 定时任务(2 张)
|
||||
|
||||
| 表名 | 说明 | Logic 基类 |
|
||||
| 表名 | 说明 | Service 基类 |
|
||||
|------|------|-----------|
|
||||
| `think_job` | 定时任务表 | BaseLogic |
|
||||
| `think_job_log` | 定时任务执行日志表 | BaseLogic |
|
||||
| `laravel_job` | 定时任务表 | `BaseService` |
|
||||
| `laravel_job_log` | 定时任务执行日志表 | `BaseService` |
|
||||
|
||||
**关联关系:**
|
||||
|
||||
```text
|
||||
think_job ←── think_job_log.job_id
|
||||
laravel_job ←── laravel_job_log.job_id
|
||||
```
|
||||
|
||||
### 基础数据(2 张表)
|
||||
### 基础数据(3 张)
|
||||
|
||||
| 表名 | 说明 | Logic 基类 |
|
||||
| 表名 | 说明 | Service 基类 |
|
||||
|------|------|-----------|
|
||||
| `think_city` | 城市行政区划表(树形,pid) | BaseLogic |
|
||||
| `think_file_template` | 文件模板表 | BaseLogic |
|
||||
| `laravel_city` | 城市行政区划表(树形) | `BaseService` |
|
||||
| `laravel_file_template` | 文件模板表 | `BaseService` |
|
||||
| `laravel_example` | 案例演示表(生成器参考模板) | `BaseService` |
|
||||
|
||||
**共计 25 张业务表**(不含中间表)。
|
||||
:::tip 租户隔离范围
|
||||
继承 `BaseTenantService` 的 9 个服务:User / Role / Dept / Position / Level / Article / Category / Notice / Link——业务数据按租户隔离;系统共享数据(字典、配置、菜单、日志等)继承 `BaseService` 不隔离。
|
||||
:::
|
||||
|
||||
## 关键表字段说明
|
||||
|
||||
### think_user(用户表)
|
||||
### laravel_user(用户表)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | INT | 主键 |
|
||||
| `username` | VARCHAR(50) | 用户名(登录账号) |
|
||||
| `tenant_id` | INT | 租户 ID(0=平台用户) |
|
||||
| `code` | VARCHAR(100) | 用户编码 |
|
||||
| `username` | VARCHAR(30) | 用户名(登录账号) |
|
||||
| `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 | 更新时间 |
|
||||
| `salt` | VARCHAR(30) | 加密盐(外部盐,与 password 分离存储) |
|
||||
| `realname` | VARCHAR(150) | 用户姓名 |
|
||||
| `gender` | INT | 性别:1-男 2-女 3-保密 |
|
||||
| `mobile` / `email` | VARCHAR(30) | 手机号 / 邮箱 |
|
||||
| `avatar` | VARCHAR(255) | 用户头像 |
|
||||
| `status` | INT | 状态:1-正常 2-禁用 |
|
||||
| `dept_id` / `level_id` / `position_id` | INT | 部门 / 职级 / 岗位 ID |
|
||||
| `province_code` / `city_code` / `district_code` / `street_code` | VARCHAR(30) | 省市县街道编码 |
|
||||
| `city_info` | VARCHAR(255) | 省市区信息拼接 |
|
||||
| `address` | VARCHAR(255) | 详细地址 |
|
||||
| `intro` / `note` | VARCHAR(255) | 个人简介 / 备注 |
|
||||
| `sort` | INT | 排序 |
|
||||
| `is_delete` + 四个审计字段 | | 公共字段 |
|
||||
|
||||
### think_role(角色表)
|
||||
**唯一索引(与软删除协同):**
|
||||
|
||||
```sql
|
||||
UNIQUE INDEX uk_tenant_username (tenant_id, username, is_delete)
|
||||
UNIQUE INDEX uk_tenant_mobile (tenant_id, mobile, is_delete)
|
||||
UNIQUE INDEX uk_tenant_code (tenant_id, code, is_delete)
|
||||
```
|
||||
|
||||
:::tip 为什么唯一索引含 is_delete?
|
||||
软删除的用户 `is_delete=1` 仍占唯一索引——把 `is_delete` 纳入联合唯一后,同名账号删除后可重新注册(新记录 `is_delete=0` 不与已删记录冲突),同时保证**同租户内未删数据**用户名唯一。业务层的 `uniqueFields` 校验与数据库索引双保险。
|
||||
:::
|
||||
|
||||
### laravel_role(角色表)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | INT | 主键 |
|
||||
| `name` | VARCHAR(50) | 角色名称 |
|
||||
| `code` | VARCHAR(50) | 角色编码 |
|
||||
| `data_scope` | TINYINT | 数据权限范围:1=全部 2=本部门 3=仅本人 |
|
||||
| `tenant_id` | INT | 租户 ID(角色按租户隔离) |
|
||||
| `status` | TINYINT | 状态 |
|
||||
| `remark` | VARCHAR(500) | 备注 |
|
||||
| `sort` | INT | 排序 |
|
||||
| `...` | | 公共字段 |
|
||||
| `remark` / `sort` | | 备注 / 排序 |
|
||||
|
||||
### think_menu(菜单/权限表)
|
||||
### laravel_menu(菜单/权限表)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | INT | 主键 |
|
||||
| `parent_id` | INT | 父级 ID(0=顶级) |
|
||||
| `name` | VARCHAR(50) | 菜单名称 |
|
||||
| `permission` | VARCHAR(100) | 权限标识(如 `sys:user:add`) |
|
||||
| `type` | TINYINT | 类型:0=目录 1=菜单 2=按钮 |
|
||||
| `permission` | VARCHAR(100) | 权限标识(如 `sys:user:add`,**菜单即权限**) |
|
||||
| `type` | TINYINT | 类型:0=菜单/目录(可导航) 1=权限节点 |
|
||||
| `path` | VARCHAR(200) | 路由路径 |
|
||||
| `icon` | VARCHAR(100) | 图标 |
|
||||
| `sort` | INT | 排序 |
|
||||
| `status` | TINYINT | 状态 |
|
||||
| `remark` | VARCHAR(500) | 备注 |
|
||||
| `...` | | 公共字段 |
|
||||
| `sort` / `status` / `remark` | | 排序 / 状态 / 备注 |
|
||||
|
||||
### think_tenant(租户表)
|
||||
### laravel_tenant(租户表)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | INT | 主键 |
|
||||
| `name` | VARCHAR(100) | 租户名称 |
|
||||
| `code` | VARCHAR(50) | 租户编码(系统自动生成:T+4位ID) |
|
||||
| `code` | VARCHAR(50) | 租户编码(系统自动生成) |
|
||||
| `status` | TINYINT | 状态(字典 tenant_status):0=禁用 1=启用 |
|
||||
| `expire_time` | DATETIME | 过期时间 |
|
||||
| `expire_time` | DATETIME | 过期时间(ResolveTenant 中间件校验) |
|
||||
| `max_user_num` | INT | 最大用户数限制 |
|
||||
| `contact_name` | VARCHAR(50) | 联系人 |
|
||||
| `contact_phone` | VARCHAR(20) | 联系电话 |
|
||||
| `remark` | VARCHAR(500) | 备注 |
|
||||
| `...` | | 公共字段 |
|
||||
| `contact_name` / `contact_phone` | | 联系人 / 联系电话 |
|
||||
|
||||
### think_article(文章表)
|
||||
### laravel_article(文章表)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
@@ -215,51 +216,48 @@ think_job ←── think_job_log.job_id
|
||||
| `title` | VARCHAR(200) | 标题 |
|
||||
| `category_id` | INT | 分类 ID |
|
||||
| `cover` | VARCHAR(500) | 封面图路径 |
|
||||
| `content` | TEXT | 内容(富文本,含 [IMG_URL] 占位符) |
|
||||
| `content` | TEXT | 内容(富文本,含 `[IMG_URL]` 占位符) |
|
||||
| `author` | VARCHAR(50) | 作者 |
|
||||
| `status` | TINYINT | 状态(字典 article_status):0=正常 1=下架 |
|
||||
| `status` | TINYINT | 状态(字典 article_status) |
|
||||
| `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 laravel_user ADD INDEX idx_tenant_id (tenant_id);
|
||||
ALTER TABLE laravel_user ADD INDEX idx_dept_id (dept_id);
|
||||
ALTER TABLE laravel_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 laravel_user_role ADD INDEX idx_user_id (user_id);
|
||||
ALTER TABLE laravel_user_role ADD INDEX idx_role_id (role_id);
|
||||
ALTER TABLE laravel_role_menu ADD INDEX idx_role_id (role_id);
|
||||
ALTER TABLE laravel_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 laravel_article ADD INDEX idx_category_id (category_id);
|
||||
ALTER TABLE laravel_article ADD INDEX idx_status (status);
|
||||
ALTER TABLE laravel_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 laravel_login_log ADD INDEX idx_create_time (create_time);
|
||||
ALTER TABLE laravel_operation_log ADD INDEX idx_create_time (create_time);
|
||||
ALTER TABLE laravel_job_log ADD INDEX idx_job_id (job_id);
|
||||
|
||||
-- 菜单表:树形查询
|
||||
ALTER TABLE think_menu ADD INDEX idx_parent_id (parent_id);
|
||||
ALTER TABLE laravel_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 laravel_dict_item ADD INDEX idx_dict_id (dict_id);
|
||||
ALTER TABLE laravel_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);
|
||||
ALTER TABLE laravel_city ADD INDEX idx_area_code (area_code);
|
||||
ALTER TABLE laravel_city ADD INDEX idx_parent_code (parent_code);
|
||||
```
|
||||
|
||||
### 索引设计原则
|
||||
@@ -268,21 +266,22 @@ ALTER TABLE think_city ADD INDEX idx_pid (pid);
|
||||
|------|------|
|
||||
| 外键必加 | 所有 `_id` 外键字段都应加索引 |
|
||||
| 过滤字段 | `pageEqFields` 中配置的精确匹配字段应加索引 |
|
||||
| 模糊搜索 | `pageLikeFields` 中的字段可考虑全文索引,但 LIKE '%xx%' 无法用普通索引 |
|
||||
| 模糊搜索 | `pageLikeFields` 中的字段可考虑全文索引,但 `LIKE '%xx%'` 无法用普通索引 |
|
||||
| 排序字段 | `pageOrderBy` 中的默认排序字段可加索引 |
|
||||
| 时间字段 | 日志表的 `create_time` 必加索引(按时间范围查询) |
|
||||
| 联合唯一 | 软删除表的唯一约束必须含 `is_delete`(见上文) |
|
||||
|
||||
## 多数据库支持
|
||||
|
||||
### 支持的数据库
|
||||
|
||||
| 数据库 | 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` |
|
||||
| 数据库 | `DB_CONNECTION` 值 | 默认端口 | PHP 扩展 |
|
||||
|--------|---------|----------|----------|
|
||||
| MySQL | `mysql` | 3306 | `pdo_mysql` |
|
||||
| PostgreSQL | `pgsql` | 5432 | `pdo_pgsql` |
|
||||
| SQL Server | `sqlsrv` | 1433 | `pdo_sqlsrv` |
|
||||
| Oracle | `oracle` | 1521 | `pdo_oci`(自定义 oci 驱动接入) |
|
||||
| SQLite | `sqlite` | - | `pdo_sqlite` |
|
||||
|
||||
### 切换数据库
|
||||
|
||||
@@ -291,49 +290,49 @@ ALTER TABLE think_city ADD INDEX idx_pid (pid);
|
||||
::: code-group
|
||||
|
||||
```ini [MySQL(默认)]
|
||||
[DATABASE]
|
||||
TYPE = mysql
|
||||
HOSTNAME = 127.0.0.1
|
||||
DATABASE = rxthinkcmf.thinkphp6.elevue
|
||||
USERNAME = root
|
||||
PASSWORD = root
|
||||
HOSTPORT = 3306
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=rxthinkcmf.laravel13.elevue
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
DB_PREFIX=laravel_
|
||||
```
|
||||
|
||||
```ini [PostgreSQL]
|
||||
[DATABASE]
|
||||
TYPE = pgsql
|
||||
HOSTNAME = 127.0.0.1
|
||||
DATABASE = rxthinkcmf.thinkphp6.elevue
|
||||
USERNAME = postgres
|
||||
PASSWORD = root
|
||||
HOSTPORT = 5432
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=rxthinkcmf.laravel13.elevue
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=root
|
||||
DB_PREFIX=laravel_
|
||||
```
|
||||
|
||||
```ini [SQL Server]
|
||||
[DATABASE]
|
||||
TYPE = sqlsrv
|
||||
HOSTNAME = 127.0.0.1
|
||||
DATABASE = rxthinkcmf.thinkphp6.elevue
|
||||
USERNAME = sa
|
||||
PASSWORD = root
|
||||
HOSTPORT = 1433
|
||||
DB_CONNECTION=sqlsrv
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=1433
|
||||
DB_DATABASE=rxthinkcmf.laravel13.elevue
|
||||
DB_USERNAME=sa
|
||||
DB_PASSWORD=root
|
||||
DB_PREFIX=laravel_
|
||||
```
|
||||
|
||||
```ini [Oracle]
|
||||
[DATABASE]
|
||||
TYPE = oracle
|
||||
HOSTNAME = 127.0.0.1
|
||||
DATABASE = rxthinkcmf.thinkphp6.elevue
|
||||
USERNAME = root
|
||||
PASSWORD = root
|
||||
HOSTPORT = 1521
|
||||
DB_CONNECTION=oracle
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=1521
|
||||
DB_DATABASE=rxthinkcmf.laravel13.elevue
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
DB_PREFIX=laravel_
|
||||
```
|
||||
|
||||
```ini [SQLite]
|
||||
[DATABASE]
|
||||
TYPE = sqlite
|
||||
DATABASE = /www/api/runtime/database.sqlite
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_DATABASE 指向 database/database.sqlite(默认值)
|
||||
DB_PREFIX=laravel_
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -343,22 +342,26 @@ DATABASE = /www/api/runtime/database.sqlite
|
||||
```php
|
||||
// config/database.php
|
||||
return [
|
||||
'default' => env('database.type', 'mysql'),
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
'connections' => [
|
||||
'mysql' => [...],
|
||||
'pgsql' => [...],
|
||||
'sqlsrv' => [...],
|
||||
'oracle' => [...],
|
||||
'sqlite' => [...],
|
||||
'sqlite' => [...],
|
||||
'mysql' => [...],
|
||||
'pgsql' => [...],
|
||||
'sqlsrv' => [...],
|
||||
'oracle' => [...], // 自定义 oci 驱动(AppServiceProvider 注册 DB::extend)
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
:::tip Oracle 接入说明
|
||||
Laravel 核心不内置 Oracle 连接器,项目通过 `AppServiceProvider` 中 `DB::extend('oci', ...)` 注册自定义驱动,配套 `App\Support\Database\{OracleConnection, OracleGrammar, OracleProcessor}` 完成分页语法与自增主键回填适配。切换前需安装 `pdo_oci` 扩展(见 [2.1 开发环境准备](/zh/quickstart/environment.html))。
|
||||
:::
|
||||
|
||||
### 字段类型映射
|
||||
|
||||
| 通用类型 | MySQL | PostgreSQL | SQL Server | Oracle | SQLite |
|
||||
|---------|-------|------------|------------|--------|--------|
|
||||
| 自增主键 | `INT AUTO_INCREMENT` | `SERIAL` | `INT IDENTITY` | `NUMBER` + 序列 | `INTEGER PRIMARY KEY` |
|
||||
| 自增主键 | `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` |
|
||||
@@ -366,352 +369,12 @@ return [
|
||||
| 小数 | `DECIMAL(m,n)` | `NUMERIC(m,n)` | `DECIMAL(m,n)` | `NUMBER(m,n)` | `REAL` |
|
||||
|
||||
:::tip 业务代码零修改
|
||||
ThinkPHP 的 ORM 层会自动处理大部分 SQL 差异,切换数据库只需修改 `.env` 中的 `DATABASE.TYPE`,业务代码无需任何修改。
|
||||
Eloquent 的方言层自动处理大部分 SQL 差异,切换数据库只需修改 `.env` 中的 `DB_CONNECTION`,业务代码无需任何修改。跨库 DDL 由 `DbSchemaBuilder` 按驱动生成(Oracle 自增序列与触发器自动创建)。
|
||||
:::
|
||||
|
||||
### 各数据库安装与配置
|
||||
### 各数据库安装
|
||||
|
||||
项目支持五种数据库,按需选择一种即可。
|
||||
|
||||
| 数据库 | 推荐场景 | 难度 | 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
|
||||
```
|
||||
|
||||
:::
|
||||
数据库的安装与初始化步骤已在 [2.1 开发环境准备](/zh/quickstart/environment.html)(含四平台安装命令、扩展启用、Oracle Instant Client)与 [2.5 数据库初始化](/zh/quickstart/database-init.html)(建库、脚本导入、`.env` 配置)中完整说明,此处不再重复。
|
||||
|
||||
## SQL 脚本
|
||||
|
||||
@@ -720,30 +383,32 @@ EOF
|
||||
```text
|
||||
document/
|
||||
├── mysql/
|
||||
│ └── rxthinkcmf.thinkphp6.elevue.sql # MySQL 版本
|
||||
│ └── rxthinkcmf.laravel13.elevue.sql # MySQL 版本
|
||||
├── postgresql/
|
||||
│ └── rxthinkcmf.thinkphp6.elevue.sql # PostgreSQL 版本
|
||||
│ └── rxthinkcmf.laravel13.elevue.sql # PostgreSQL 版本
|
||||
├── sqlserver/
|
||||
│ └── rxthinkcmf.thinkphp6.elevue.sql # SQL Server 版本
|
||||
│ └── rxthinkcmf.laravel13.elevue.sql # SQL Server 版本
|
||||
├── oracle/
|
||||
│ └── rxthinkcmf.thinkphp6.elevue.sql # Oracle 版本
|
||||
│ └── rxthinkcmf.laravel13.elevue.sql # Oracle 版本
|
||||
└── sqlite/
|
||||
└── rxthinkcmf.thinkphp6.elevue.sql # SQLite 版本
|
||||
└── rxthinkcmf.laravel13.elevue.sql # SQLite 版本
|
||||
```
|
||||
|
||||
各数据库脚本包含完整的表结构定义和初始数据(用户、角色、菜单、字典、城市行政区划等),已针对各数据库的 SQL 语法做了适配,可直接导入使用。
|
||||
|
||||
导入方法详见 [2.4 数据库初始化](/zh/quickstart/database-init.html)。
|
||||
:::warning
|
||||
本项目 `database/migrations/` 为空,**建表以 SQL 脚本为准**,`php artisan migrate` 不会创建业务表。导入方法详见 [2.5 数据库初始化](/zh/quickstart/database-init.html)。
|
||||
:::
|
||||
|
||||
## 慢查询排查
|
||||
|
||||
```ini
|
||||
# MySQL my.cnf
|
||||
# MySQL my.cnf / my.ini
|
||||
slow_query_log = 1
|
||||
slow_query_log_file = /var/log/mysql/slow.log
|
||||
long_query_time = 2
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
项目所有查询都通过 ThinkPHP ORM 构建,不会出现原生 SQL 注入。如需排查慢查询,建议开启数据库慢查询日志,而非在代码层面埋点。
|
||||
项目所有查询都通过 Eloquent 查询构建器构建(参数绑定),不会出现原生 SQL 注入。如需排查慢查询,建议开启数据库慢查询日志,而非在代码层面埋点;同时核对 [索引策略](#索引策略) 中的推荐索引是否缺失。
|
||||
:::
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 4.5 目录结构设计
|
||||
|
||||
:::tip 概述
|
||||
目录结构遵循"按职责分层、按模块分组"的原则。后端以 `app/` 为核心,按 Controller → Logic → Model → Validate 四层组织;前端以 `src/` 为核心,按 API → Views → Store 分层。两侧通过路由和 API 接口对接。
|
||||
目录结构遵循"按职责分层、按模块分组"的原则。后端以 `app/` 为核心,按 Controller → Request → Service → Model 四层组织;前端以 `src/` 为核心,按 API → Views → Store 分层。两侧通过路由和 API 接口对接。
|
||||
:::
|
||||
|
||||
## 后端整体结构
|
||||
@@ -9,57 +9,65 @@
|
||||
```text
|
||||
项目根目录/
|
||||
├── app/ ← 应用代码(核心)
|
||||
├── config/ ← 配置文件
|
||||
├── extend/ ← 扩展类库(非 Composer)
|
||||
├── bootstrap/ ← 应用引导(中间件别名/分组、健康检查路由)
|
||||
├── config/ ← 配置文件(18 个,7 个独立业务配置)
|
||||
├── database/ ← 迁移(空)/ 种子 / 工厂
|
||||
├── docker/ ← Docker 部署文件
|
||||
├── public/ ← Web 入口 + 静态资源
|
||||
├── route/ ← 路由定义
|
||||
├── templates/ ← 代码生成器模板
|
||||
├── runtime/ ← 运行时缓存(自动生成)
|
||||
├── resources/ ← 生成器模板(templates/)+ Blade 视图 + Vite 资源
|
||||
├── routes/ ← 路由定义(api / web / console)
|
||||
├── storage/ ← 运行时产物(日志/缓存/会话,需可写)
|
||||
├── tests/ ← PHPUnit 测试
|
||||
├── uploads/ ← 上传文件目录
|
||||
├── vendor/ ← Composer 依赖
|
||||
├── wiki/ ← 文档站点(VitePress)
|
||||
├── document/ ← 开发参考手册(txt)
|
||||
├── .env ← 环境变量配置
|
||||
└── think ← ThinkPHP 命令行入口
|
||||
├── document/ ← 五种数据库初始化脚本
|
||||
├── artisan ← Artisan 命令行入口
|
||||
└── .env ← 环境变量配置
|
||||
```
|
||||
|
||||
## 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 个)
|
||||
├── Attributes/ 注解定义(3 个)
|
||||
│ ├── Permission.php 权限校验注解
|
||||
│ ├── OperationLog.php 操作日志注解
|
||||
│ └── DemoAllow.php 演示环境放行注解
|
||||
├── Console/Commands/ 命令行命令(5 个)
|
||||
├── Exceptions/ 异常处理
|
||||
│ ├── BizException.php 业务异常
|
||||
│ └── Handler.php 统一异常渲染(JSON)
|
||||
├── Helpers/helpers.php 全局辅助函数(21 个)
|
||||
├── Http/
|
||||
│ ├── Controllers/
|
||||
│ │ ├── Controller.php 控制器基类
|
||||
│ │ └── Api/ 控制器层(28 个)
|
||||
│ ├── Middleware/ 中间件(7 个)
|
||||
│ └── Requests/ 表单请求(24 个 + BaseRequest)
|
||||
├── Models/ 数据模型层(25 个)
|
||||
│ └── Concerns/ 模型 Trait(3 个)
|
||||
├── Observers/ 模型观察者(3 个,缓存失效)
|
||||
├── Providers/ 服务提供者(启动校验 + Observer + oci 驱动注册)
|
||||
├── Services/ 服务层(36 个)
|
||||
│ └── Concerns/ Service Trait(7 个)
|
||||
├── Support/ 基础支撑(Auth / Result)
|
||||
└── 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 | 定时任务的具体执行逻辑 |
|
||||
| `Http/Controllers/Api/` | HTTP 请求入口 | `{Module}Controller.php` | 28 | 一个模块一个文件,注解标注权限和日志 |
|
||||
| `Http/Requests/` | 参数格式校验 | `{Module}Request.php` | 24+1 | 继承 BaseRequest,控制器类型注入 |
|
||||
| `Services/` | 业务逻辑 | `{Module}Service.php` | 36 | 属性配置 + 生命周期钩子,核心业务层 |
|
||||
| `Models/` | 数据库映射 | `{TableName}.php`(大驼峰) | 25 | 模型 Trait 获软删除 + 审计字段 |
|
||||
| `Http/Middleware/` | 中间件 | 见下方清单 | 7 | 请求拦截:认证、租户、日志等 |
|
||||
| `Attributes/` | 注解类 | `{Name}.php` | 3 | Permission、OperationLog、DemoAllow |
|
||||
| `Console/Commands/` | 命令行 | `{Name}Command.php` 等 | 5 | CLI 命令:生成器、迁移、任务、字典缓存 |
|
||||
| `Task/` | 任务处理器 | `{Name}Task.php` | 3 | 定时任务的具体执行逻辑 |
|
||||
| `Observers/` | 缓存失效 | `{Name}Observer.php` | 3 | 字典/菜单/角色菜单变更 → 清缓存 |
|
||||
| `Support/` | 基础支撑 | `Auth.php` / `Result.php` | 2 | 当前登录用户 / 统一响应 |
|
||||
|
||||
### 模块文件对应关系
|
||||
|
||||
@@ -67,141 +75,110 @@ app/
|
||||
|
||||
```text
|
||||
app/
|
||||
├── controller/UserController.php ← HTTP 入口(路由指向这里)
|
||||
├── logic/UserLogic.php ← 业务逻辑(属性配置 + 钩子)
|
||||
├── model/User.php ← 数据模型(表映射)
|
||||
└── validate/UserValidate.php ← 参数校验(规则 + 场景)
|
||||
├── Http/Controllers/Api/UserController.php ← HTTP 入口(路由指向这里)
|
||||
├── Http/Requests/UserRequest.php ← 参数校验(规则 + 中文消息)
|
||||
├── Services/UserService.php ← 业务逻辑(属性配置 + 钩子)
|
||||
└── Models/User.php ← 数据模型(表映射 + Trait)
|
||||
```
|
||||
|
||||
请求流转:`Controller → Logic → Model → 数据库`
|
||||
请求流转:`Controller → (FormRequest 校验) → Service → 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 等 |
|
||||
| `Http/Controllers/Controller.php` | 控制器基类:`success/fail/paginate` 响应 + `_page/_add` 等通用 CRUD 快捷方法 + 参数获取 |
|
||||
| `Http/Requests/BaseRequest.php` | 请求基类:授权统一放行、失败统一 JSON |
|
||||
| `Services/BaseService.php` | 服务基类:**全部声明式属性集中声明** + Trait 占位方法 + 处理链 |
|
||||
| `Services/BaseTenantService.php` | 租户服务基类:继承 BaseService,自动 `tenant_id` 查询/写入隔离 |
|
||||
| `Exceptions/Handler.php` | 异常处理:`api/*` 异常统一渲染为 JSON 响应 |
|
||||
| `Exceptions/BizException.php` | 业务异常:Service 抛出,默认 `code=1` |
|
||||
| `Support/Result.php` | 统一响应:`success/page/fail/unauthorized/forbidden/notFound/validateError` |
|
||||
| `Support/Auth.php` | 当前登录用户:`Auth::userId()` / `Auth::username()` |
|
||||
| `Helpers/helpers.php` | 公共函数:驼峰转换、文件处理、字符串等 21 个(composer autoload.files) |
|
||||
|
||||
### service/ 服务层清单
|
||||
### Services 横切服务清单
|
||||
|
||||
服务层提供跨模块复用的无状态能力,按单一职责拆分:
|
||||
跨模块复用的无状态能力(业务 Service 之外):
|
||||
|
||||
| 服务 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| JWT 认证 | `JwtService.php` | 登录、刷新、解析 Token |
|
||||
| 密码加密 | `PasswordService.php` | bcrypt 双重加盐 |
|
||||
| 字典服务 | `DictService.php` | 字典缓存与查询(两级缓存) |
|
||||
| 系统参数 | `ParamService.php` | 系统参数服务(两级缓存) |
|
||||
| 验证码 | `CaptchaService.php` | 图片验证码生成与校验 |
|
||||
| 请求信息 | `RequestInfoService.php` | 客户端 IP、UA、归属地解析 |
|
||||
| 注解读取 | `AttributeService.php` | PHP 8 注解反射读取 + 缓存 |
|
||||
| JWT 认证 | `JwtService.php` | 登录、刷新、解析、踢下线(Redis 有状态) |
|
||||
| 密码加密 | `PasswordService.php` | bcrypt(成本因子 12) |
|
||||
| 字典服务 | `DictCacheService.php` | 字典缓存与查询(两级缓存) |
|
||||
| 验证码 | `CaptchaService.php` | 图片验证码生成与校验(IP 限流) |
|
||||
| 请求信息 | `RequestInfoService.php` | 客户端 IP、UA、归属地解析(ip2region) |
|
||||
| 代码生成 | `GeneratorService.php` | 根据数据库表生成 CRUD 代码 |
|
||||
| 数据库迁移 | `DbMigrateService.php` | 跨数据库迁移引擎 |
|
||||
| DDL 生成 | `DbSchemaBuilder.php` | 跨库类型映射的 DDL 生成器 |
|
||||
| Excel | `ExcelService.php` | 导入导出(支持大数据量分批) |
|
||||
| 数据库迁移 | `DbMigrateService.php` | 跨数据库迁移引擎(五驱动) |
|
||||
| DDL 生成 | `DbSchemaBuilder.php` | 跨库类型映射的 DDL 生成器(Oracle 序列/触发器) |
|
||||
| Excel | `ExcelService.php` | 导入导出(表头映射、分批) |
|
||||
| 配置管理 | `ConfigService.php` | 系统配置读写与缓存 |
|
||||
| 字典管理 | `DictService.php` | 字典/字典项管理 |
|
||||
|
||||
### middleware/ 中间件清单
|
||||
### Middleware 中间件清单
|
||||
|
||||
| 中间件 | 文件 | 作用域 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| 跨域 | `CorsMiddleware.php` | 全局 | CORS 头设置 |
|
||||
| 认证权限 | `AuthMiddleware.php` | 路由级 | JWT 校验 + 权限校验 |
|
||||
| 租户上下文 | `TenantMiddleware.php` | 路由级 | 解析注入 tenantId |
|
||||
| 演示环境 | `DemoMiddleware.php` | 路由级 | 演示环境写操作拦截 |
|
||||
| 操作日志 | `LogMiddleware.php` | 路由级 | 基于注解记录操作日志 |
|
||||
| 中间件 | 文件 | 别名 | 作用域 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| 真实 IP | `ProxyMiddleware.php` | proxy | 全局(api prepend) | 受信代理 `X-Forwarded-For` 解析 |
|
||||
| 跨域 | `Cors.php` | cors | 全局 | CORS 头设置(`config/cors.php`) |
|
||||
| 强制 JSON | `ForceJsonResponse.php` | force.json | 全局 | 规范化 Accept 头 |
|
||||
| 认证权限 | `Authenticate.php` | auth.jwt | 路由组 | JWT 校验 + `#[Permission]` 鉴权 |
|
||||
| 租户上下文 | `ResolveTenant.php` | tenant | 路由组 | 解析注入 tenantId |
|
||||
| 操作日志 | `LogOperation.php` | log.operation | 路由组 | 基于 `#[OperationLog]` 采集,terminable 落库 |
|
||||
| 演示环境 | `DemoMiddleware.php` | demo | 路由别名 | 写操作拦截,`#[DemoAllow]` 放行 |
|
||||
|
||||
### command/ 命令行清单
|
||||
### Console 命令清单
|
||||
|
||||
| 命令 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| 定时任务执行 | `JobRunCommand.php` | 执行一次定时任务 |
|
||||
| 定时任务守护 | `JobDaemonCommand.php` | 常驻进程循环扫描执行 |
|
||||
| 代码生成 | `GeneratorCommand.php` | CLI 方式生成 CRUD 代码 |
|
||||
| 数据库迁移 | `DbMigrateCommand.php` | 跨数据库迁移 CLI 工具 |
|
||||
| 命令 | 文件 | 签名 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 代码生成 | `GeneratorCommand.php` | `generate:code` | CLI 方式生成 CRUD 代码 |
|
||||
| 数据库迁移 | `DbMigrateCommand.php` | `db:migrate` | 跨数据库迁移 CLI 工具 |
|
||||
| 定时任务执行 | `JobRunCommand.php` | `job:run` | 执行一次到期任务(配合系统 cron) |
|
||||
| 定时任务守护 | `JobDaemonCommand.php` | `job:daemon` | 常驻进程循环扫描执行 |
|
||||
| 字典缓存清理 | `ClearDictCache.php` | `dict:clear-cache` | 清除字典缓存 |
|
||||
|
||||
## 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 API 配置(驼峰转换开关)★
|
||||
├── captcha.php 验证码配置 ★
|
||||
├── cors.php 跨域配置 ★
|
||||
├── file.php 文件上传配置(域名、大小限制)★
|
||||
├── jwt.php JWT 配置(密钥、过期时间)★
|
||||
├── middleware.php 中间件配置(受信代理)★
|
||||
├── tenant.php 多租户配置 ★
|
||||
├── generator.php 代码生成器配置
|
||||
├── app.php / auth.php / cache.php / database.php / filesystems.php
|
||||
├── logging.php / mail.php / queue.php / services.php / session.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) |
|
||||
标 ★ 为本项目独立维护的配置文件(不与 Laravel 默认配置合并)。
|
||||
|
||||
## extend/ 扩展类库
|
||||
|
||||
存放不通过 Composer 管理的自定义类库,按 PSR-4 自动加载:
|
||||
## routes/ 路由定义
|
||||
|
||||
```text
|
||||
extend/
|
||||
├── jwt/
|
||||
│ └── Jwt.php JWT 工具类(封装 firebase/php-jwt)
|
||||
├── response/
|
||||
│ └── Result.php 统一响应类(success/fail/page 等)
|
||||
└── (无自定义扩展类)
|
||||
routes/
|
||||
├── api.php 全部 API 路由(公开接口在中间件组外)
|
||||
├── web.php 欢迎页
|
||||
└── console.php 调度注册
|
||||
```
|
||||
|
||||
使用方式:直接通过命名空间引用,如 `\response\Result::success()`、`\jwt\Jwt::getTokenFromHeader()`。
|
||||
路由组织约定见 [3.7 路由注册](/zh/guide/route.html)。
|
||||
|
||||
## route/ 路由定义
|
||||
## resources/templates/ 代码生成模板
|
||||
|
||||
```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/
|
||||
resources/templates/
|
||||
├── controller.php.tpl 控制器模板
|
||||
├── logic.php.tpl 业务逻辑模板
|
||||
├── service.php.tpl 服务层模板
|
||||
├── model.php.tpl 模型模板
|
||||
├── validate.php.tpl 验证器模板
|
||||
├── ui/ 普通列表前端模板
|
||||
├── request.php.tpl FormRequest 模板
|
||||
├── ui/ 普通列表前端模板(6 个)
|
||||
└── ui2/ 树形结构前端模板
|
||||
```
|
||||
|
||||
由 `GeneratorService` 渲染,通过代码生成器 CLI 或 API 调用。
|
||||
由 `GeneratorService` 渲染,通过 CLI(`generate:code`)或 Web 界面调用。
|
||||
|
||||
## 前端目录设计
|
||||
|
||||
@@ -211,28 +188,32 @@ ui/src/
|
||||
├── views/ ← 页面视图(按业务域分组)
|
||||
├── components/ ← 公共组件(跨页面复用)
|
||||
├── store/ ← 状态管理(Pinia)
|
||||
├── router/ ← 路由配置
|
||||
├── router/ ← 路由配置(动态路由由后端菜单驱动)
|
||||
├── hooks/ ← 组合式函数
|
||||
├── directives/ ← 自定义指令
|
||||
├── directives/ ← 自定义指令(v-perm 权限)
|
||||
├── enums/ ← 枚举常量
|
||||
├── styles/ ← 全局样式
|
||||
├── utils/ ← 工具函数
|
||||
├── utils/ ← 工具函数(http 封装)
|
||||
├── layout/ ← 布局组件
|
||||
├── plugins/ ← 插件
|
||||
├── settings/ ← 项目配置
|
||||
└── assets/ ← 静态资源
|
||||
```
|
||||
|
||||
:::tip
|
||||
前端工程位于本仓库 `ui/` 目录,代码生成器输出至 `ui/src/` 对应位置。详细结构见 [9.2 目录结构详解](/zh/frontend/directory.html)。
|
||||
:::
|
||||
|
||||
## 前后端目录对应关系
|
||||
|
||||
| 后端 | 前端 | 说明 |
|
||||
|------|------|------|
|
||||
| `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/` | 字典 ↔ 枚举常量 |
|
||||
| `Http/Controllers/Api/UserController.php` | `ui/src/api/system/user.ts` | API 接口函数 |
|
||||
| `Services/UserService.php` | `src/views/system/user/` | 业务页面 |
|
||||
| `Models/User.php` | — | 前端不直接操作模型 |
|
||||
| `Http/Requests/UserRequest.php` | 表单组件校验 | 前端校验为体验层,后端 FormRequest 为最终防线 |
|
||||
| `routes/api.php` | `ui/src/api/**/*.ts` | 路由 ↔ API 函数映射 |
|
||||
| 字典数据(`dict` 表) | `src/enums/` | 字典 ↔ 枚举常量 |
|
||||
|
||||
## 设计原则
|
||||
|
||||
@@ -242,29 +223,29 @@ ui/src/
|
||||
|
||||
| 层 | 职责 | 不应该做 |
|
||||
|----|------|----------|
|
||||
| Controller | 参数获取、响应封装、注解标注 | 业务逻辑、数据库操作 |
|
||||
| Logic | 业务编排、钩子处理、校验 | 直接 HTTP 交互 |
|
||||
| Model | 表映射、关联关系、软删除 | 复杂业务判断 |
|
||||
| Validate | 参数格式校验 | 业务规则校验 |
|
||||
| Controller | 参数注入、响应封装、注解标注 | 业务逻辑、数据库操作 |
|
||||
| Request | 参数**格式**校验 | 业务规则校验 |
|
||||
| Service | 业务编排、钩子处理、业务规则校验 | 直接 HTTP 交互 |
|
||||
| Model | 表映射、关联关系、软删除、审计 | 复杂业务判断 |
|
||||
|
||||
### 按模块分组
|
||||
|
||||
同一业务模块的 4 个文件(Controller / Logic / Model / Validate)分散在各自目录中,通过命名前缀关联。这种"水平分层 + 垂直命名"的方式:
|
||||
同一业务模块的 4 个文件分散在各自目录中,通过命名前缀关联。这种"水平分层 + 垂直命名"的方式:
|
||||
|
||||
- 便于按职责查找(所有控制器在一起)
|
||||
- 便于按职责查找(所有服务在一起)
|
||||
- 便于代码生成器按模板批量生成
|
||||
- 便于基类统一处理(如 BaseLogic 的属性驱动)
|
||||
- 便于基类统一处理(BaseService 属性驱动 + Trait 占位覆盖)
|
||||
|
||||
### 文件命名规范
|
||||
|
||||
| 类型 | 规范 | 示例 |
|
||||
|------|------|------|
|
||||
| 控制器 | 大驼峰 + Controller | `ArticleController.php` |
|
||||
| 逻辑层 | 大驼峰 + Logic | `ArticleLogic.php` |
|
||||
| 请求 | 大驼峰 + Request | `ArticleRequest.php` |
|
||||
| 服务 | 大驼峰 + Service | `ArticleService.php` |
|
||||
| 模型 | 大驼峰(表名) | `Article.php` |
|
||||
| 验证器 | 大驼峰 + Validate | `ArticleValidate.php` |
|
||||
| 服务 | 大驼峰 + Service | `DictService.php` |
|
||||
| 中间件 | 大驼峰 + Middleware | `AuthMiddleware.php` |
|
||||
| 注解 | 大驼峰 | `Log.php`、`Permission.php` |
|
||||
| 命令 | 大驼峰 + Command | `GeneratorCommand.php` |
|
||||
| 中间件 | 大驼峰(动词+名词) | `Authenticate.php` / `ResolveTenant.php` |
|
||||
| 注解 | 大驼峰 | `Permission.php`、`OperationLog.php` |
|
||||
| 命令 | 大驼峰 + Command(或动词开头) | `GeneratorCommand.php`、`ClearDictCache.php` |
|
||||
| 任务 | 大驼峰 + Task | `SendSmsTask.php` |
|
||||
| Trait | 动词开头 + Has/By | `HasCrudOperations.php`、`SoftDeletesByFlag.php` |
|
||||
|
||||
@@ -1,311 +1,189 @@
|
||||
# 4.11 扩展性设计
|
||||
|
||||
:::tip 概述
|
||||
系统通过六种机制保证良好的扩展性:属性配置、生命周期钩子、自动参数校验、中间件链、注解系统、服务层复用。新增模块无需修改框架代码,只需继承基类 + 配置属性 + 按需重写钩子。
|
||||
系统通过六种机制保证良好的扩展性:属性配置、生命周期钩子、FormRequest 参数校验、中间件链、注解系统、服务层复用。新增模块无需修改框架代码,只需继承基类 + 配置属性 + 按需重写钩子。
|
||||
:::
|
||||
|
||||
## 扩展机制总览
|
||||
|
||||
| 机制 | 位置 | 作用 | 扩展方式 |
|
||||
|------|------|------|----------|
|
||||
| 属性配置 | Logic 子类 | 声明式行为定义 | 设置属性值 |
|
||||
| 生命周期钩子 | Logic 子类 | 注入定制逻辑 | 重写钩子方法 |
|
||||
| 自动参数校验 | Validate + Logic | 参数格式校验 | 创建 Validate + 配置 `$validateClass` |
|
||||
| 中间件链 | middleware/ | 请求拦截处理 | 创建中间件 + 注册 |
|
||||
| 注解系统 | attribute/ | 声明式元数据 | 创建注解 + 中间件读取 |
|
||||
| 服务层复用 | service/ | 跨模块通用能力 | 创建 Service 类 |
|
||||
| 属性配置 | Service 子类 | 声明式行为定义 | 设置属性值 |
|
||||
| 生命周期钩子 | Service 子类 | 注入定制逻辑 | 重写钩子方法 |
|
||||
| 参数格式校验 | FormRequest | HTTP 入口拦截非法输入 | 创建 Request + 类型注入 |
|
||||
| 中间件链 | `Http/Middleware/` | 请求拦截处理 | 创建中间件 + 注册别名 |
|
||||
| 注解系统 | `Attributes/` | 声明式元数据 | 创建注解 + 中间件反射读取 |
|
||||
| 服务层复用 | `Services/` | 跨模块通用能力 | 创建 Service 类 |
|
||||
|
||||
## 1. 属性配置模式
|
||||
|
||||
Logic 层通过属性配置声明行为,新增模块只需设置属性,无需编写 CRUD 代码。
|
||||
Service 层通过属性配置声明行为,新增模块只需设置属性,无需编写 CRUD 代码。
|
||||
|
||||
### 完整属性清单
|
||||
### 完整属性清单(摘自 `app/Services/ArticleService.php` 实际代码)
|
||||
|
||||
```php
|
||||
class NewModuleLogic extends BaseLogic
|
||||
class ArticleService extends BaseTenantService
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* 所有增删改查操作都会基于此模型执行,子类必须指定
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = NewModule::class;
|
||||
use HasCrudOperations, HasFileProcessing, HasQueryScopes, HasSerializeMaps;
|
||||
// 数据权限能力按需 use HasDataScope
|
||||
|
||||
/**
|
||||
* 参数验证器类
|
||||
*
|
||||
* 配置后,新增 / 修改等操作会自动调用该验证器进行参数校验,
|
||||
* 校验不通过会直接抛出异常,无需在业务代码中手动验证
|
||||
*
|
||||
* @var string
|
||||
* 绑定的模型类
|
||||
*/
|
||||
protected string $validateClass = NewModuleValidate::class;
|
||||
protected string $modelClass = Article::class;
|
||||
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
* 单文件字段:保存时迁移临时文件,读取时补全域名
|
||||
*
|
||||
* 提交数据中若包含这些字段,框架会自动完成文件上传、存储与路径回填
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $fileFields = ['cover', 'avatar'];
|
||||
protected array $fileFields = ['cover'];
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
* 多文件字段:保存时逐个迁移,读取时逐个补全域名
|
||||
*
|
||||
* 上传的文件会保存在该子目录下(相对于文件存储根目录)
|
||||
*
|
||||
* @var string
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected string $fileSaveDir = 'new_module';
|
||||
protected array $multiFileFields = ['images'];
|
||||
|
||||
/**
|
||||
* 富文本字段
|
||||
*
|
||||
* 这些字段会做富文本特殊处理(如 XSS 过滤、图片路径修正等)
|
||||
*
|
||||
* @var array
|
||||
* 文件保存子目录
|
||||
*/
|
||||
protected array $contentFields = ['content'];
|
||||
protected string $fileSaveDir = 'article';
|
||||
|
||||
/**
|
||||
* 列表查询中需要 LIKE 模糊匹配的字段
|
||||
* 分页时模糊匹配的字段
|
||||
*
|
||||
* 请求参数中若带有这些字段,会自动以 LIKE '%value%' 的方式参与查询
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $pageLikeFields = ['name', 'title'];
|
||||
protected array $pageLikeFields = ['title', 'author'];
|
||||
|
||||
/**
|
||||
* 列表查询中需要精确匹配的字段
|
||||
* 分页时精确匹配的字段
|
||||
*
|
||||
* 请求参数中若带有这些字段,会自动以 = 的方式参与查询
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $pageEqFields = ['status', 'type'];
|
||||
protected array $pageEqFields = ['category_id', 'status'];
|
||||
|
||||
/**
|
||||
* 列表查询的默认排序规则
|
||||
* 默认排序规则(多字段)
|
||||
*
|
||||
* field:排序字段;type:排序方式(asc / desc)
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, array<string, string>>
|
||||
*/
|
||||
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'tenant_id', 'type' => 'asc'],
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 唯一性校验字段
|
||||
* 字典翻译配置:[字段 => 字典编码]
|
||||
*
|
||||
* 支持两种写法:
|
||||
* - 字符串:单字段全局唯一,如 'code'
|
||||
* - 数组:多字段组合唯一,如 ['name', 'pid'] 表示同层级下 name 唯一
|
||||
*
|
||||
* 新增 / 修改时会自动校验,冲突则抛出异常
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $uniqueFields = ['code', ['name', 'pid']];
|
||||
protected array $serializeMaps = [
|
||||
'status' => 'article_status',
|
||||
];
|
||||
|
||||
/**
|
||||
* 枚举字段与显示名的映射
|
||||
*
|
||||
* 键为数据表字段名,值为对应的枚举字典 key,
|
||||
* 返回数据时会自动附加对应的枚举显示名(如 status_text)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = ['status' => 'module_status'];
|
||||
|
||||
/**
|
||||
* 数据权限-按创建人过滤的字段
|
||||
*
|
||||
* 配置后,非管理员用户只能看到自己创建的数据
|
||||
*
|
||||
* @var string
|
||||
* 数据权限的用户字段(data_scope=3 仅本人时按此字段过滤)
|
||||
*/
|
||||
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';
|
||||
}
|
||||
```
|
||||
|
||||
其它可声明属性(`uniqueFields`、`contentFields`、`dataScopeDeptField`、`tenantScopeField`、`treeParentField` / `treeLikeField` 等)均在 `BaseService` 中声明,完整清单见 [4.6 核心基类体系](/zh/architecture/base-classes.html)。
|
||||
|
||||
### 属性驱动的自动行为
|
||||
|
||||
| 属性 | 触发的自动行为 |
|
||||
|------|---------------|
|
||||
| `validateClass` | add/update 时自动调用验证器校验参数 |
|
||||
| `fileFields` | add/update 时迁移临时文件,查询时补全域名 |
|
||||
| `multiFileFields` | 逗号分隔多文件逐个迁移/补全 |
|
||||
| `contentFields` | add/update 时处理富文本媒体文件,查询时替换占位符 |
|
||||
| `pageLikeFields` | pageList 时自动构建 LIKE 查询 |
|
||||
| `pageEqFields` | pageList 时自动构建精确查询 |
|
||||
| `pageOrderBy` | pageList/allList 时自动应用默认排序 |
|
||||
| `uniqueFields` | add/update 时自动校验唯一性 |
|
||||
| `serializeMaps` | 查询时自动补全 `{字段名}Text` 枚举显示名 |
|
||||
| `dataScopeUserField` | 查询时自动按创建人过滤 |
|
||||
| `dataScopeDeptField` | 查询时自动按部门过滤 |
|
||||
| `tenantScopeField` | 查询时自动叠加 tenant_id 条件,新增时自动填充 |
|
||||
| `pageOrderBy` / `orderFieldAllow` | 自动应用默认排序 / 动态排序白名单 |
|
||||
| `uniqueFields` | add/update 时自动校验唯一性(BizException 拦截) |
|
||||
| `serializeMaps` | 查询时自动补全 `{字段}Text` 字典显示名 |
|
||||
| `dataScopeUserField` / `dataScopeDeptField` | 查询时自动按创建人/部门过滤 |
|
||||
| `tenantScopeField` | 查询时自动叠加 `tenant_id` 条件,新增时自动填充 |
|
||||
| `treeParentField` / `treeLikeField` | 树形列表自动组装 / 子树模糊搜索 |
|
||||
|
||||
:::tip 未启用的属性是空操作
|
||||
`BaseService` 为 6 个 Trait 的方法提供占位实现,子类 `use` 对应 Trait 后真实实现才生效(占位覆盖机制)。属性声明了但未 `use` 对应 Trait 时,对应自动行为不会执行。
|
||||
:::
|
||||
|
||||
## 2. 生命周期钩子
|
||||
|
||||
通过重写钩子注入定制逻辑,无需修改基类。钩子方法在基类中定义为空实现或默认返回,子类按需覆盖。
|
||||
通过重写钩子注入定制逻辑,无需修改基类。钩子方法在 `HasCrudOperations` 中定义为空实现或默认返回,子类按需覆盖。
|
||||
|
||||
### 钩子清单
|
||||
|
||||
| 钩子 | 时机 | 参数 | 返回值 | 可拦截 |
|
||||
|------|------|------|--------|--------|
|
||||
| `beforeAdd` | 新增前 | `$data` | 处理后的 `$data` | ✅ 抛异常 |
|
||||
| `afterAdd` | 新增后 | `$id, $data` | void | ❌ |
|
||||
| `afterAdd` | 新增后 | `$id, $data` | void(事务内) | ✅ 抛异常回滚 |
|
||||
| `beforeUpdate` | 修改前 | `$id, $data` | 处理后的 `$data` | ✅ 抛异常 |
|
||||
| `afterUpdate` | 修改后 | `$id, $data` | void | ❌ |
|
||||
| `afterUpdate` | 修改后 | `$id, $data` | void(事务内) | ✅ 抛异常回滚 |
|
||||
| `beforeDelete` | 删除前 | `$id` | void | ✅ 抛异常 |
|
||||
| `afterDelete` | 删除后 | `$id` | void | ❌ |
|
||||
| `afterDelete` | 删除后 | `$id` | void(事务内) | ✅ 抛异常回滚 |
|
||||
| `beforeBatchDelete` | 批量删除前 | `$ids` | void | ✅ 抛异常 |
|
||||
| `afterBatchDelete` | 批量删除后 | `$ids` | void | ❌ |
|
||||
| `afterBatchDelete` | 批量删除后 | `$ids` | void(事务内) | ✅ 抛异常回滚 |
|
||||
| `afterDetail` | 详情查询后 | `$id, &$data` | void(引用传递) | ❌ |
|
||||
| `afterPageList` | 列表查询后 | `&$records, $params` | void(引用传递,分页与全量列表均触发) | ❌ |
|
||||
|
||||
### 典型用法
|
||||
### 典型用法(摘自项目实际代码)
|
||||
|
||||
```php
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
// app/Services/DeptService.php —— 删除前校验(抛 BizException 拦截删除并回滚)
|
||||
protected function beforeDelete(int $id): void
|
||||
{
|
||||
/**
|
||||
* 新增前处理:设置默认值
|
||||
*
|
||||
* @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;
|
||||
$hasChildren = Dept::where('parent_id', $id)->exists();
|
||||
if ($hasChildren) {
|
||||
throw new BizException('存在子部门,不可删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增后处理:保存关联数据
|
||||
*
|
||||
* @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']));
|
||||
}
|
||||
$hasUsers = User::where('dept_id', $id)->exists();
|
||||
if ($hasUsers) {
|
||||
throw new BizException('该部门下存在用户,不可删除');
|
||||
}
|
||||
}
|
||||
|
||||
// app/Services/ArticleService.php —— 详情后处理(引用传递,补充关联数据)
|
||||
protected function afterDetail(int $id, array &$data): void
|
||||
{
|
||||
$images = $data['images'] ?? '';
|
||||
$data['imagesList'] = ! empty($images)
|
||||
? array_map('trim', explode(',', $images))
|
||||
: [];
|
||||
}
|
||||
|
||||
// app/Services/UserService.php —— 列表后处理(批量补充,避免 N+1)
|
||||
protected function afterPageList(array &$records, array $params): void
|
||||
{
|
||||
$userIds = array_column($records, 'id');
|
||||
if (empty($userIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改前处理:过滤不可修改字段
|
||||
*
|
||||
* @param int $id 文章 ID
|
||||
* @param array $data 待更新的数据
|
||||
* @return array 过滤后的数据
|
||||
*/
|
||||
protected function beforeUpdate(int $id, array $data): array
|
||||
{
|
||||
// author(作者)字段不允许通过更新接口修改,直接剔除
|
||||
unset($data['author']);
|
||||
return $data;
|
||||
// 批量获取用户角色(仅查所需字段)
|
||||
$userRoles = [];
|
||||
$roleData = UserRole::whereIn('user_id', $userIds)->select('user_id', 'role_id')->get();
|
||||
foreach ($roleData as $rd) {
|
||||
$userRoles[$rd->user_id][] = $rd->role_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除前处理:业务约束校验
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
$roleMap = Role::getNameMap();
|
||||
$deptMap = Dept::getNameMap();
|
||||
foreach ($records as &$item) {
|
||||
// 补充角色名称,组装为 [{id, name}]
|
||||
$roleIds = $userRoles[$item['id']] ?? [];
|
||||
$item['roles'] = array_map(
|
||||
fn ($roleId) => ['id' => $roleId, 'name' => $roleMap[$roleId] ?? ''],
|
||||
$roleIds
|
||||
);
|
||||
$item['deptName'] = $deptMap[$item['dept_id'] ?? 0] ?? '';
|
||||
unset($item['password'], $item['salt']);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -313,51 +191,72 @@ class ArticleLogic extends BaseTenantLogic
|
||||
### 执行顺序
|
||||
|
||||
**add 流程:**
|
||||
`beforeAdd` → `validateData` → `camel_to_snake` → `processTenantId` → `processFile` → `processContent` → `filterTableFields` → `checkUnique` → `Model::create` → `afterAdd`
|
||||
`beforeAdd` → `checkUnique` → `DB::transaction { processTenantId → processFile → processMultiFile → processContent → Model::create(审计填充)→ afterAdd }`
|
||||
|
||||
**update 流程:**
|
||||
`beforeUpdate` → `validateData` → `camel_to_snake` → `processFile` → `processContent` → `filterTableFields` → `checkUnique` → `Model::find + save` → `afterUpdate`
|
||||
`beforeUpdate` → `checkUnique(excludeId)` → `DB::transaction { 同上处理 → update(审计刷新)→ afterUpdate }`
|
||||
|
||||
**delete 流程:**
|
||||
`beforeDelete` → `is_delete = 1` → `afterDelete`
|
||||
`beforeDelete` → `DB::transaction { is_delete = 1 → afterDelete }`
|
||||
|
||||
**pageList/allList 流程:**
|
||||
`buildQuery` → `排序` → `分页` → `processFile` → `processContent` → `processSerializeMaps` → `filterFields` → `afterPageList`
|
||||
`buildQuery` → `排序` → `分页/全量` → `processAfterRead(文件/字典翻译/字段过滤)` → `afterPageList`
|
||||
|
||||
## 3. 自动参数校验
|
||||
(参数格式校验在入口 FormRequest 完成,不在此流程内。)
|
||||
|
||||
通过在 Logic 中配置 `$validateClass`,add/update 时自动调用验证器,无需手动调用。
|
||||
## 3. 参数格式校验(FormRequest)
|
||||
|
||||
### 扩展方式
|
||||
通过控制器方法类型注入 FormRequest,进入方法前自动校验,无需手动调用。
|
||||
|
||||
### 扩展方式(摘自 `app/Http/Requests/ExampleRequest.php` 实际代码)
|
||||
|
||||
```php
|
||||
// 1. 创建验证器
|
||||
// app/validate/NewModuleValidate.php
|
||||
class NewModuleValidate extends Validate
|
||||
// 1. 创建请求类
|
||||
// app/Http/Requests/ExampleRequest.php
|
||||
class ExampleRequest extends BaseRequest
|
||||
{
|
||||
protected $rule = [
|
||||
'name' => 'require|max:100',
|
||||
'code' => 'require|unique:new_module',
|
||||
];
|
||||
protected $message = [
|
||||
'name.require' => '名称不能为空',
|
||||
'code.unique' => '编码已存在',
|
||||
];
|
||||
protected $scene = [
|
||||
'add' => ['name', 'code'],
|
||||
'update' => ['name'],
|
||||
];
|
||||
/**
|
||||
* 校验规则
|
||||
*
|
||||
* @return array<string, string> 字段规则
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255', // 案例名称:必填,最长 255
|
||||
'avatar' => 'nullable|string|max:255', // 头像/图片地址:可空,最长 255
|
||||
'type' => 'nullable|integer|in:1,2,3,4', // 类型:1=类型1,2=类型2,3=类型3,4=类型4
|
||||
'status' => 'nullable|integer|in:1,2', // 状态:1=在用,2=停用
|
||||
'sort' => 'nullable|integer', // 排序值:可空,整数
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义错误消息
|
||||
*
|
||||
* @return array<string, string> 字段规则 => 错误消息
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => '案例名称不能为空',
|
||||
'name.max' => '案例名称不能超过 255 个字符',
|
||||
'avatar.max' => '图片地址不能超过 255 个字符',
|
||||
'type.in' => '案例类型不合法,允许:1-类型1 2-类型2 3-类型3 4-类型4',
|
||||
'status.in' => '状态值不合法,允许:1-在用 2-停用',
|
||||
'sort.integer' => '排序值格式不正确',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 在 Logic 中配置
|
||||
class NewModuleLogic extends BaseLogic
|
||||
// 2. 控制器类型注入(校验通过才进入方法)
|
||||
public function add(ExampleRequest $request): JsonResponse
|
||||
{
|
||||
protected string $validateClass = NewModuleValidate::class;
|
||||
// ...
|
||||
return $this->_add($this->exampleService, $request->validated());
|
||||
}
|
||||
```
|
||||
|
||||
详见 [3.4 验证器开发](/zh/guide/validator.html)。
|
||||
详见 [3.4 FormRequest 验证器](/zh/guide/validator.html)。
|
||||
|
||||
## 4. 中间件机制
|
||||
|
||||
@@ -369,68 +268,95 @@ class NewModuleLogic extends BaseLogic
|
||||
请求进入
|
||||
│
|
||||
▼
|
||||
CorsMiddleware(全局) ← OPTIONS 预检 + 跨域响应头
|
||||
ProxyMiddleware(API 组前置) ← 受信代理真实 IP 还原
|
||||
│
|
||||
▼
|
||||
AuthMiddleware(路由级) ← JWT 认证 + 权限校验 → 401/403
|
||||
│ 注入 userInfo
|
||||
▼
|
||||
TenantMiddleware(路由级) ← 租户上下文注入 → 403
|
||||
│ 注入 tenantId
|
||||
▼
|
||||
DemoMiddleware(路由级) ← 演示环境拦截写操作 → 403
|
||||
Cors(API 组前置) ← OPTIONS 预检 + 跨域响应头
|
||||
│
|
||||
▼
|
||||
LogMiddleware(路由级) ← 操作日志记录
|
||||
ForceJsonResponse(API 组前置) ← Accept 强制 application/json
|
||||
│
|
||||
▼
|
||||
Authenticate(路由级,别名 auth.jwt) ← JWT 认证 + #[Permission] 权限校验 → 401/403
|
||||
│ 注入 attributes['userInfo']
|
||||
▼
|
||||
ResolveTenant(路由级,别名 tenant) ← 租户上下文注入 → 403
|
||||
│ 注入 attributes['tenantId']
|
||||
▼
|
||||
LogOperation(路由级,别名 log.operation) ← 操作日志采集(terminate 落库)
|
||||
│
|
||||
▼
|
||||
控制器方法
|
||||
(另有 DemoMiddleware,已注册 demo 别名,可挂载到路由组)
|
||||
```
|
||||
|
||||
### 创建自定义中间件
|
||||
|
||||
中间件结构参考(摘自 `app/Http/Middleware/DemoMiddleware.php` 实际代码):
|
||||
|
||||
```php
|
||||
// 1. 创建中间件类
|
||||
// app/middleware/RateLimitMiddleware.php
|
||||
namespace app\middleware;
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use think\facade\Cache;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RateLimitMiddleware
|
||||
class DemoMiddleware
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$ip = $request->ip();
|
||||
$key = 'rate_limit_' . $ip;
|
||||
$count = Cache::get($key, 0);
|
||||
|
||||
if ($count > 100) {
|
||||
return json(['code' => 1, 'msg' => '请求过于频繁'], 429);
|
||||
// 非演示模式直接放行
|
||||
if (! config('app.demo_mode', false)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
Cache::set($key, $count + 1, 60);
|
||||
return $next($request);
|
||||
// GET 请求放行(读操作不受限)
|
||||
if ($request->isMethod('GET')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// 检查是否有 #[DemoAllow] 注解(反射读取,存在即放行)
|
||||
$route = $request->route();
|
||||
if ($route) {
|
||||
$reflection = new \ReflectionMethod(
|
||||
$route->getControllerClass(),
|
||||
$route->getActionMethod()
|
||||
);
|
||||
if (! empty($reflection->getAttributes(DemoAllow::class))) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
// 演示模式下拦截写操作
|
||||
return response()->json([
|
||||
'code' => 403,
|
||||
'ok' => false,
|
||||
'msg' => '演示模式下禁止操作',
|
||||
'data' => null,
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
// 2. 注册中间件
|
||||
// 方式一:全局注册(app/middleware.php)
|
||||
return [
|
||||
\app\middleware\RateLimitMiddleware::class,
|
||||
];
|
||||
注册方式(`bootstrap/app.php` 中的三种挂载点):
|
||||
|
||||
// 方式二:路由级注册(route/app.php)
|
||||
Route::group('api', function () {
|
||||
// ...
|
||||
})->middleware(\app\middleware\RateLimitMiddleware::class);
|
||||
```php
|
||||
// 方式一:注册别名(现有 7 个别名即在此处)
|
||||
$middleware->alias([
|
||||
'proxy' => ProxyMiddleware::class,
|
||||
'cors' => Cors::class,
|
||||
'force.json' => ForceJsonResponse::class,
|
||||
'auth.jwt' => Authenticate::class,
|
||||
'tenant' => ResolveTenant::class,
|
||||
'demo' => DemoMiddleware::class,
|
||||
'log.operation' => LogOperation::class,
|
||||
]);
|
||||
|
||||
// 方式三:控制器级注册
|
||||
class SomeController extends BaseController
|
||||
{
|
||||
protected $middleware = [
|
||||
\app\middleware\RateLimitMiddleware::class,
|
||||
];
|
||||
}
|
||||
// 方式二:API 组前置(对全部 /api 请求生效)
|
||||
$middleware->api(prepend: [ProxyMiddleware::class, Cors::class, ForceJsonResponse::class]);
|
||||
|
||||
// 方式三:路由组注册(routes/api.php)
|
||||
Route::middleware(['auth.jwt', 'tenant', 'demo', 'log.operation'])->group(function () { /* ... */ });
|
||||
```
|
||||
|
||||
## 5. 注解系统
|
||||
@@ -441,15 +367,15 @@ class SomeController extends BaseController
|
||||
|
||||
| 注解 | 目标 | 说明 | 读取方 |
|
||||
|------|------|------|--------|
|
||||
| `#[Log]` | 方法 | 操作日志(标题、类型、描述) | LogMiddleware |
|
||||
| `#[Permission]` | 方法/类 | 权限校验(权限编码) | AuthMiddleware |
|
||||
| `#[DemoAllow]` | 方法 | 演示环境放行标记 | DemoMiddleware |
|
||||
| `#[OperationLog]` | 方法 | 操作日志(标题、类型、描述模板) | `LogOperation` 中间件(ReflectionMethod) |
|
||||
| `#[Permission]` | 方法 | 权限校验(权限编码、名称) | `Authenticate` 中间件(ReflectionMethod) |
|
||||
| `#[DemoAllow]` | 方法 | 演示环境放行标记 | `DemoMiddleware`(ReflectionMethod) |
|
||||
|
||||
### 使用示例
|
||||
|
||||
```php
|
||||
// 操作日志
|
||||
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
|
||||
// 操作日志(TYPE 常量 30 种,描述支持 {param} 占位符)
|
||||
#[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')]
|
||||
|
||||
// 权限校验
|
||||
#[Permission('sys:user:add', '添加用户')]
|
||||
@@ -458,54 +384,50 @@ class SomeController extends BaseController
|
||||
#[DemoAllow]
|
||||
|
||||
// 组合使用
|
||||
#[Log('文章管理-删除记录', Log::TYPE_DELETE, '删除文章ID:{id}')]
|
||||
#[OperationLog('文章管理-删除', OperationLog::TYPE_DELETE, '删除文章ID:{id}')]
|
||||
#[Permission('sys:article:delete', '删除文章')]
|
||||
public function delete(int $id): Json
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
return $this->_delete($this->service, $id);
|
||||
}
|
||||
```
|
||||
|
||||
### 创建自定义注解
|
||||
### 注解结构与读取方式(摘自项目实际代码)
|
||||
|
||||
```php
|
||||
// 1. 定义注解类
|
||||
// app/attribute/Cacheable.php
|
||||
namespace app\attribute;
|
||||
|
||||
use Attribute;
|
||||
|
||||
// 1. 定义注解类(app/Attributes/Permission.php 实际代码)
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class Cacheable
|
||||
class Permission
|
||||
{
|
||||
/**
|
||||
* @param string $code 权限编码(鉴权唯一依据,如 sys:user:add)
|
||||
* @param string $name 权限名称(用于展示,如"添加用户")
|
||||
* @param string $description 权限描述(可选,补充说明)
|
||||
*/
|
||||
public function __construct(
|
||||
public string $key = '',
|
||||
public int $ttl = 3600,
|
||||
public string $code,
|
||||
public string $name = '',
|
||||
public string $description = '',
|
||||
) {}
|
||||
}
|
||||
|
||||
// 2. 在控制器方法上使用
|
||||
#[Cacheable(key: 'article_list', ttl: 1800)]
|
||||
public function list(): Json { ... }
|
||||
#[Permission('sys:user:add', '添加用户')]
|
||||
public function add(UserRequest $request): JsonResponse { ... }
|
||||
|
||||
// 3. 在中间件中读取
|
||||
// 通过 AttributeService 或反射获取注解实例
|
||||
$ref = new \ReflectionMethod($controller, $method);
|
||||
$attrs = $ref->getAttributes(Cacheable::class);
|
||||
if (!empty($attrs)) {
|
||||
$cacheable = $attrs[0]->newInstance();
|
||||
// $cacheable->key, $cacheable->ttl
|
||||
// 3. 在中间件中读取(DemoMiddleware 实际代码同款反射方式)
|
||||
$reflection = new \ReflectionMethod($controllerClass, $method);
|
||||
$attrs = $reflection->getAttributes(DemoAllow::class);
|
||||
if (! empty($attrs)) {
|
||||
return $next($request); // 命中注解即放行
|
||||
}
|
||||
```
|
||||
|
||||
### 注解读取服务
|
||||
新增自定义注解时,按同样结构在 `app/Attributes/` 下定义类,再在中间件中用 `ReflectionMethod::getAttributes()` 读取即可。
|
||||
|
||||
`AttributeService` 提供统一的注解读取接口(带缓存):
|
||||
|
||||
```php
|
||||
AttributeService::getLog($class, $method); // 获取 Log 注解
|
||||
AttributeService::getPermission($class, $method); // 获取 Permission 注解
|
||||
```
|
||||
:::tip 注解读取性能
|
||||
反射读取开销为微秒级,`Authenticate` / `LogOperation` 均直接使用 `ReflectionMethod` 读取(无缓存),避免开发阶段改注解后需等缓存过期。
|
||||
:::
|
||||
|
||||
## 6. 服务层复用
|
||||
|
||||
@@ -513,100 +435,124 @@ AttributeService::getPermission($class, $method); // 获取 Permission 注解
|
||||
|
||||
### 现有服务清单
|
||||
|
||||
| 服务 | 文件 | 能力 |
|
||||
| 服务 | 文件(`app/Services/`) | 能力 |
|
||||
|------|------|------|
|
||||
| `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 生成器(跨库映射) |
|
||||
| `JwtService` | `JwtService.php` | 登录、刷新、解析 Token(Redis 有状态) |
|
||||
| `PasswordService` | `PasswordService.php` | bcrypt 双重加盐加密 |
|
||||
| `DictCacheService` | `DictCacheService.php` | 字典查询(两级缓存) |
|
||||
| `CaptchaService` | `CaptchaService.php` | 图片验证码生成与校验(IP 限流) |
|
||||
| `RequestInfoService` | `RequestInfoService.php` | 客户端 IP、UA、归属地解析 |
|
||||
| `GeneratorService` | `GeneratorService.php` | 代码生成 |
|
||||
| `ExcelService` | `ExcelService.php` | Excel 导入导出 |
|
||||
| `DbMigrateService` | `DbMigrateService.php` | 跨数据库迁移引擎 |
|
||||
| `DbSchemaBuilder` | `DbSchemaBuilder.php` | DDL 生成器(跨库映射、Oracle 序列) |
|
||||
| `ConfigService` | `ConfigService.php` | 系统配置读写与缓存 |
|
||||
| `DictService` | `DictService.php` | 字典/字典项管理 |
|
||||
|
||||
### 使用示例
|
||||
|
||||
```php
|
||||
// 字典服务
|
||||
DictService::getText('gender', 1); // → '男'
|
||||
DictService::getValue('user_status', '启用'); // → 1
|
||||
DictService::getOptions('gender'); // → [['value'=>0,'label'=>'女'], ...]
|
||||
// 字典服务(静态调用,内部两级缓存)
|
||||
DictCacheService::getText('gender', 1); // → '男'
|
||||
DictCacheService::getValue('user_status', '启用'); // → 1
|
||||
|
||||
// 密码服务
|
||||
$encrypted = PasswordService::encrypt('123456'); // → ['password'=>'...', 'salt'=>'...']
|
||||
$default = PasswordService::getDefaultPasswordPlain(); // → '123456'
|
||||
|
||||
// Excel 服务
|
||||
ExcelService::importWithMap($filePath, $headerMap);
|
||||
ExcelService::chunkExport($headers, $callback, '导出.xlsx', 500);
|
||||
ExcelService::export($list, $headers, '导出.xlsx');
|
||||
|
||||
// 请求信息
|
||||
// 请求信息(IP 经 ProxyMiddleware 还原真实值)
|
||||
$requestInfo = RequestInfoService::create(request());
|
||||
$ip = $requestInfo->getIp(); // 客户端 IP(经 ProxyMiddleware 还原真实 IP)
|
||||
$location = $requestInfo->getIpLocation(); // IP 归属地(基于 zoujingli/ip2region)
|
||||
$os = $requestInfo->getOs(); // 操作系统
|
||||
$browser = $requestInfo->getBrowser(); // 浏览器
|
||||
$location = $requestInfo->getIpLocation(); // 基于 zoujingli/ip2region
|
||||
```
|
||||
|
||||
### 创建自定义服务
|
||||
|
||||
```php
|
||||
// app/service/NotificationService.php
|
||||
namespace app\service;
|
||||
服务结构参考(摘自 `app/Services/ParamService.php` 实际代码——无状态静态方法 + 请求级缓存的典型写法):
|
||||
|
||||
class NotificationService
|
||||
```php
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Param;
|
||||
|
||||
/**
|
||||
* 系统参数服务
|
||||
*
|
||||
* 从 param 表获取系统参数配置。
|
||||
* 数据量小,不使用持久缓存,仅保留请求级缓存避免同请求内重复查库。
|
||||
*/
|
||||
class ParamService
|
||||
{
|
||||
// 发送站内通知
|
||||
public static function send(int $userId, string $title, string $content): void
|
||||
/**
|
||||
* 请求级缓存:[参数编码 => 参数值]
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected static array $cache = [];
|
||||
|
||||
/**
|
||||
* 获取参数值
|
||||
*
|
||||
* 读取顺序:请求级缓存 → 查库。
|
||||
*/
|
||||
public static function get(string $code): mixed
|
||||
{
|
||||
\app\model\Notice::create([
|
||||
'user_id' => $userId,
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'status' => 0,
|
||||
]);
|
||||
if (array_key_exists($code, static::$cache)) {
|
||||
return static::$cache[$code];
|
||||
}
|
||||
|
||||
$value = /* 查库逻辑 */ null;
|
||||
static::$cache[$code] = $value;
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
// 批量发送
|
||||
public static function batchSend(array $userIds, string $title, string $content): void
|
||||
/**
|
||||
* 清除缓存(参数变更后调用)
|
||||
*/
|
||||
public static function clearCache(string $code = ''): void
|
||||
{
|
||||
foreach ($userIds as $userId) {
|
||||
self::send($userId, $title, $content);
|
||||
if ($code !== '') {
|
||||
unset(static::$cache[$code]);
|
||||
|
||||
return;
|
||||
}
|
||||
static::$cache = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
NotificationService::send($userId, '系统通知', '您有一条新消息');
|
||||
```
|
||||
|
||||
新增服务时按此结构放在 `app/Services/` 下:无状态、单一职责、静态方法或构造注入均可;跨模块复用的能力才提升为 Service,业务专属逻辑留在业务 Service 中。
|
||||
|
||||
## 扩展性设计原则
|
||||
|
||||
### 开闭原则(OCP)
|
||||
|
||||
| 扩展场景 | 做法 | 不应该做 |
|
||||
|----------|------|----------|
|
||||
| 新增 CRUD 模块 | 创建 Controller/Logic/Model/Validate | 修改基类 |
|
||||
| 新增查询条件 | 设置 `pageLikeFields`/`pageEqFields` | 重写 `buildQuery` |
|
||||
| 新增文件处理 | 设置 `fileFields` | 手动调用 `save_file` |
|
||||
| 新增校验规则 | 创建 Validate + 配置 `validateClass` | 在 Controller 手动 validate |
|
||||
| 新增请求拦截 | 创建中间件 | 修改现有中间件 |
|
||||
| 新增操作日志 | 添加 `#[Log]` 注解 | 在方法内手动记录日志 |
|
||||
| 新增 CRUD 模块 | 创建 Request/Service/Model/Controller | 修改基类 |
|
||||
| 新增查询条件 | 设置 `pageLikeFields` / `pageEqFields` | 重写 `buildQuery` |
|
||||
| 新增文件处理 | `use HasFileProcessing` + 设置 `fileFields` | 手动搬运临时文件 |
|
||||
| 新增校验规则 | 创建 FormRequest + 类型注入 | 在 Controller 手动校验 |
|
||||
| 新增请求拦截 | 创建中间件 + 注册 | 修改现有中间件 |
|
||||
| 新增操作日志 | 添加 `#[OperationLog]` 注解 | 在方法内手动记录日志 |
|
||||
| 新增权限节点 | 添加 `#[Permission]` 注解 | 在方法内手动校验权限 |
|
||||
| 新增通用能力 | 创建 Service | 在 Logic 中写静态方法 |
|
||||
| 新增通用能力 | 创建 Service | 在业务 Service 里写静态方法 |
|
||||
|
||||
### 最小改动原则
|
||||
|
||||
新增一个完整的业务模块,只需要:
|
||||
|
||||
1. 建表(含公共字段)
|
||||
2. 创建 Model(继承 BaseModel,声明表名)
|
||||
3. 创建 Validate(定义规则 + 场景)
|
||||
4. 创建 Logic(继承 BaseLogic/BaseTenantLogic,配置属性)
|
||||
5. 创建 Controller(继承 BaseController,初始化 Logic,CRUD 一行调用)
|
||||
6. 注册路由
|
||||
2. 创建 Model(组合双 Trait,声明表名与 fillable)
|
||||
3. 创建 FormRequest(定义规则与中文消息)
|
||||
4. 创建 Service(继承 BaseService / BaseTenantService,配置属性 + 按需钩子)
|
||||
5. 创建 Controller(构造注入 Service,CRUD 一行调用 + 双注解)
|
||||
6. 注册路由(`routes/api.php` 认证组内)
|
||||
7. 配置菜单权限(可选)
|
||||
8. 前端 API + 页面(或交给代码生成器一次完成)
|
||||
|
||||
**不需要修改的文件:** 基类、中间件、配置文件、服务层。
|
||||
**不需要修改的文件:** 基类、中间件、配置文件、横切服务。
|
||||
(或直接使用 [第 10 章 代码生成器](/zh/generator/overview.html) 一键生成以上全部。)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# 4.4 前端分层架构
|
||||
|
||||
:::tip 概述
|
||||
前端采用 Vue 3 + TypeScript 组件化架构,分为 View(视图)→ API(接口)→ Store(状态)→ HTTP(请求)四层。辅以 Router(路由)、Directives(指令)、Hooks(组合式函数)、Utils(工具)等支撑模块。
|
||||
前端采用 Vue 3 + TypeScript 组件化架构,分为 View(视图)→ API(接口)→ Store(状态)→ HTTP(请求)四层。辅以 Router(路由)、Directives(指令)、Hooks(组合式函数)、Utils(工具)等支撑模块。前端为本仓库 `ui/` 目录内的 EleVue 管理后台。
|
||||
:::
|
||||
|
||||
## 架构全景
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 支撑模块 │
|
||||
│ 支撑模块 │
|
||||
│ │
|
||||
│ Router(路由) Directives(指令) Hooks(组合式函数) │
|
||||
│ src/router/ src/directives/ src/hooks/ │
|
||||
@@ -19,7 +19,7 @@
|
||||
│ 被主干层引用
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 主干链路 │
|
||||
│ 主干链路 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ View(视图层) │ │
|
||||
@@ -40,10 +40,9 @@
|
||||
│ │ Axios 封装:拦截器、Token 携带、错误处理 │ │
|
||||
│ │ 文件:src/utils/http/axios/ │ │
|
||||
│ └──────────────────────────┬──────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 后端 API(RESTful JSON) │ │
|
||||
│ │ 后端 API(RESTful JSON,{code, ok, msg, data}) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
@@ -72,7 +71,7 @@ src/views/
|
||||
├── content/ 内容管理
|
||||
│ ├── article/ 文章管理
|
||||
│ └── category/ 分类管理
|
||||
├── login.vue 登录页
|
||||
├── login/ 登录页
|
||||
├── dashboard/ 仪表盘
|
||||
└── ...
|
||||
```
|
||||
@@ -107,7 +106,7 @@ src/views/
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<!-- 分页(后端固定返回 records / total / current / size / pages) -->
|
||||
<el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize"
|
||||
:total="total" @change="fetchData" />
|
||||
</template>
|
||||
@@ -145,44 +144,48 @@ onMounted(() => fetchData());
|
||||
| 新增 | `{module}Add` | POST | `data` |
|
||||
| 修改 | `{module}Update` | PUT | `data` |
|
||||
| 删除 | `{module}Delete` | DELETE | URL 参数 |
|
||||
| 批量删除 | `{module}BatchDelete` | DELETE | `data` |
|
||||
| 批量删除 | `{module}BatchDelete` | **DELETE**(带 body) | `data`(`{ids:[]}`) |
|
||||
| 导入 | `{module}Import` | POST | FormData |
|
||||
| 导出 | `{module}Export` | GET | `params` |
|
||||
|
||||
:::warning 批量删除方法为 DELETE
|
||||
本项目 `batchDelete` 路由为 **DELETE**(请求体仍传 `ids` 数组——DELETE 方法携带 body),见 [3.7 路由注册](/zh/guide/route.html)。
|
||||
:::
|
||||
|
||||
**完整示例:**
|
||||
|
||||
```typescript
|
||||
// src/api/system/user.ts
|
||||
// src/api/tool/example.ts
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
// 分页查询
|
||||
export function getUserPage(params?) {
|
||||
return http.request({ url: '/user/page', method: 'GET', params });
|
||||
export function getExamplePage(params?) {
|
||||
return http.request({ url: '/example/page', method: 'GET', params });
|
||||
}
|
||||
|
||||
// 详情
|
||||
export function getUserDetail(id) {
|
||||
return http.request({ url: '/user/detail/' + id, method: 'GET' });
|
||||
export function getExampleDetail(id) {
|
||||
return http.request({ url: '/example/detail/' + id, method: 'GET' });
|
||||
}
|
||||
|
||||
// 新增
|
||||
export function userAdd(data: any) {
|
||||
return http.request({ url: '/user/add', method: 'POST', data });
|
||||
export function exampleAdd(data: any) {
|
||||
return http.request({ url: '/example/add', method: 'POST', data });
|
||||
}
|
||||
|
||||
// 修改
|
||||
export function userUpdate(data: any) {
|
||||
return http.request({ url: '/user/update', method: 'PUT', data });
|
||||
export function exampleUpdate(data: any) {
|
||||
return http.request({ url: '/example/update', method: 'PUT', data });
|
||||
}
|
||||
|
||||
// 删除
|
||||
export function userDelete(id) {
|
||||
return http.request({ url: '/user/delete/' + id, method: 'DELETE' });
|
||||
export function exampleDelete(id) {
|
||||
return http.request({ url: '/example/delete/' + id, method: 'DELETE' });
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
export function userBatchDelete(data: any) {
|
||||
return http.request({ url: '/user/batchDelete', method: 'DELETE', data });
|
||||
// 批量删除(DELETE + 请求体 ids 数组)
|
||||
export function exampleBatchDelete(data: any) {
|
||||
return http.request({ url: '/example/batchDelete', method: 'DELETE', data });
|
||||
}
|
||||
```
|
||||
|
||||
@@ -239,15 +242,18 @@ src/utils/http/axios/
|
||||
```text
|
||||
登录:
|
||||
userStore.login(userInfo)
|
||||
→ POST /login → 获取 access_token
|
||||
→ POST /login(含 key/code 验证码参数)
|
||||
→ 获取 access_token
|
||||
→ Storage.set(ACCESS_TOKEN, token)
|
||||
→ this.setToken(token)
|
||||
|
||||
获取用户信息:
|
||||
userStore.GetInfo()
|
||||
→ GET /index/getUserInfo
|
||||
→ this.setPermissions(res.permissions)
|
||||
→ this.setUserInfo(res)
|
||||
→ GET /index/getUser
|
||||
→ setPermissions(res.permissions)
|
||||
→ setUserInfo(res)
|
||||
|
||||
获取菜单(动态路由):
|
||||
→ GET /index/getMenus
|
||||
|
||||
登出:
|
||||
userStore.logout()
|
||||
@@ -280,7 +286,7 @@ src/utils/http/axios/
|
||||
```text
|
||||
1. 用户登录 → 获取 Token → 存入 Store
|
||||
2. 路由守卫(router.beforeEach)检测到 Token
|
||||
3. 调用 GET /index/getMenus 获取菜单树
|
||||
3. 调用 GET /index/getMenus 获取菜单树(后端按用户权限过滤)
|
||||
4. generator-routers.ts 将菜单树转换为路由表
|
||||
5. router.addRoute() 动态注入路由
|
||||
6. 渲染侧边栏菜单
|
||||
@@ -324,6 +330,10 @@ src/utils/http/axios/
|
||||
用户权限列表包含 `*:*:*` 时,表示超级管理员,拥有所有权限,`v-perm`/`v-perms` 指令始终放行。
|
||||
:::
|
||||
|
||||
:::warning 前端权限不是安全防线
|
||||
`v-perm` 只是体验层(隐藏按钮);真正的安全校验在后端 `#[Permission]` 注解 + `auth.jwt` 中间件(见 [3.8 权限节点配置](/zh/guide/permission.html))。
|
||||
:::
|
||||
|
||||
## 支撑模块
|
||||
|
||||
### Hooks(组合式函数)
|
||||
@@ -366,8 +376,8 @@ API: getExamplePage(params)
|
||||
HTTP: 请求拦截器添加 Token
|
||||
│ 发送 GET /example/page?pageNo=1&pageSize=20
|
||||
▼
|
||||
后端: Controller → Logic → Model → Database
|
||||
│
|
||||
后端: Controller → Service → Model → Database
|
||||
│(响应 {code, ok, msg, data},snake→camel 已在后端完成)
|
||||
▼
|
||||
HTTP: 响应拦截器判断 code
|
||||
│ code === 0 → 返回 data
|
||||
@@ -387,8 +397,8 @@ Vue 响应式 → 表格自动刷新
|
||||
登录成功
|
||||
│
|
||||
▼
|
||||
userStore.setToken(token) → 响应式更新
|
||||
userStore.setUserInfo(info) → 响应式更新
|
||||
userStore.setToken(token) → 响应式更新
|
||||
userStore.setUserInfo(info) → 响应式更新
|
||||
userStore.setPermissions(perm) → 响应式更新
|
||||
│
|
||||
▼
|
||||
@@ -402,14 +412,14 @@ 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` 指令 | 权限码对应 |
|
||||
| `Http/Controllers/Api/UserController.php` | `src/api/system/user.ts` | API 函数映射 |
|
||||
| `Services/UserService.php` | `src/views/system/user/` | 业务页面 |
|
||||
| `Http/Requests/UserRequest.php` 的 rules | 表单 `rules` | 前端表单校验规则(体验层) |
|
||||
| `Models/User.php` | — | 前端不直接操作模型 |
|
||||
| `routes/api.php` | `src/api/**/*.ts` | 路由 ↔ API 函数 |
|
||||
| 字典数据(`dict` 表) | `src/enums/` + 字典 Store | 枚举常量 |
|
||||
| `#[Permission]` 注解 | `v-perm` 指令 | 权限码对应 |
|
||||
| 分页 `records/total/current/size/pages` | el-pagination 绑定 | 固定分页结构 |
|
||||
|
||||
## 层间通信规范
|
||||
|
||||
|
||||
@@ -15,43 +15,50 @@
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Nginx(反向代理) │
|
||||
│ 静态资源托管 / SSL 终止 / 负载均衡 / gzip 压缩 │
|
||||
│ 静态资源托管 / SSL 终止 / gzip 压缩 / 仅放行入口PHP │
|
||||
└──────────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ThinkPHP 6.1 Application │
|
||||
│ Laravel 13 Application │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ 中间件链(按顺序执行) │ │
|
||||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────┐ ┌───────┐ │ │
|
||||
│ │ │ CORS │→│ Auth │→│ Tenant │→│ Demo │→│ Log │ │ │
|
||||
│ │ │ 跨域处理 │ │JWT+权限 │ │ 租户上下文│ │演示拦截│ │操作日志│ │ │
|
||||
│ │ └─────────┘ └─────────┘ └─────────┘ └──────┘ └───────┘ │ │
|
||||
│ │ 中间件链(按注册顺序执行) │ │
|
||||
│ │ ┌───────┐ ┌──────┐ ┌───────────┐ ┌──────────┐ │ │
|
||||
│ │ │ Proxy │→│ Cors │→│ForceJson │→│ auth.jwt │ │ │
|
||||
│ │ │真实IP │ │ 跨域 │ │ 强制JSON │ │JWT+权限 │ │ │
|
||||
│ │ └───────┘ └──────┘ └───────────┘ └──────────┘ │ │
|
||||
│ │ 全局组(api prepend) 路由组 │ │
|
||||
│ │ ┌────────┐ ┌─────────┐ │ │
|
||||
│ │ │ tenant │→│log.oper.│ │ │
|
||||
│ │ │租户上下文│ │操作日志 │ │ │
|
||||
│ │ └────────┘ └─────────┘ │ │
|
||||
│ │ 另有 DemoMiddleware(演示拦截,按路由别名启用) │ │
|
||||
│ └───────────────────────────────┬───────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ 控制器层(Controller) │ │
|
||||
│ │ BaseController → 统一响应 · 参数获取 · 通用 CRUD │ │
|
||||
│ │ #[Permission] 权限注解 · #[Log] 日志注解 │ │
|
||||
│ │ Controller 基类 → success/fail/paginate · _page/_add 等 │ │
|
||||
│ │ #[Permission] 权限注解 · #[OperationLog] 日志注解 │ │
|
||||
│ │ FormRequest 类型注入 → 参数格式校验 │ │
|
||||
│ └───────────────────────────────┬───────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ 逻辑层(Logic) │ │
|
||||
│ │ BaseLogic / BaseTenantLogic │ │
|
||||
│ │ 属性配置 · 生命周期钩子 · 文件处理 · 枚举翻译 │ │
|
||||
│ │ 唯一性校验 · 数据权限 · 租户隔离 · 事务管理 │ │
|
||||
│ │ 服务层(Service) │ │
|
||||
│ │ BaseService / BaseTenantService │ │
|
||||
│ │ 属性配置 · 生命周期钩子 · 6 个 Concerns Trait 组合 │ │
|
||||
│ │ 文件处理 · 字典翻译 · 唯一性校验 · 数据权限 · 租户隔离 │ │
|
||||
│ │ DB::transaction 事务管理 · BizException 业务异常 │ │
|
||||
│ └───────────────────────────────┬───────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ 模型层(Model) │ │
|
||||
│ │ BaseModel │ │
|
||||
│ │ 软删除(is_delete)· 审计字段自动写入 │ │
|
||||
│ │ 模型层(Eloquent Model) │ │
|
||||
│ │ SoftDeletesByFlag(is_delete 软删除)· HasAuditColumns │ │
|
||||
│ └───────────────────────────────┬───────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ MySQL │ │ Redis │ │ 文件系统 │ │
|
||||
│ │ 数据库 │ │ 缓存 │ │ uploads/ │ │
|
||||
│ │ MySQL 等 │ │ Redis │ │ 文件系统 │ │
|
||||
│ │ 五种数据库 │ │ 会话/缓存/队列│ │ public/uploads/ │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -64,7 +71,7 @@
|
||||
|
||||
### 1. 前后端分离
|
||||
|
||||
前端 Vue3 应用通过 HTTP API 调用后端 ThinkPHP 服务,两者独立开发、独立部署。前端开发阶段通过 Vite 的 `proxy` 配置将 `/api` 请求代理到后端 `http://localhost:8000`,生产环境由 Nginx 统一反向代理。
|
||||
前端 Vue3 应用通过 HTTP API 调用后端 Laravel 服务,两者独立开发、独立部署。前端开发阶段通过 Vite 的 `proxy` 配置将 `/api` 请求代理到后端 `http://localhost:8000`,生产环境由 Nginx 统一反向代理。
|
||||
|
||||
### 2. 分层架构
|
||||
|
||||
@@ -72,92 +79,92 @@
|
||||
|
||||
| 层次 | 目录 | 职责 |
|
||||
|------|------|------|
|
||||
| 控制器层 | `app/controller/` | 路由入口、权限注解、参数获取、响应返回 |
|
||||
| 逻辑层 | `app/logic/` | 业务逻辑、属性配置、生命周期钩子、横切处理 |
|
||||
| 模型层 | `app/model/` | 数据库表映射、软删除、审计字段 |
|
||||
| 控制器层 | `app/Http/Controllers/Api/` | 路由入口、权限注解、参数注入、响应返回 |
|
||||
| 服务层 | `app/Services/` | 业务逻辑、属性配置、生命周期钩子、横切处理 |
|
||||
| 模型层 | `app/Models/` | 数据库表映射、软删除、审计字段 |
|
||||
|
||||
验证职责分层:**FormRequest 只做格式验证,Service 只做业务规则**(唯一性、状态依赖)。
|
||||
|
||||
### 3. 模板方法模式(声明式 CRUD)
|
||||
|
||||
通过 `BaseLogic` 基类,将通用 CRUD 流程固化为模板方法。子类只需声明差异点(过滤字段、排序规则、唯一性校验等),即可获得完整的增删改查能力,极大减少重复代码。
|
||||
通过 `BaseService` 基类,将通用 CRUD 流程固化为模板方法。子类只需声明差异点(过滤字段、排序规则、唯一性校验等),即可获得完整的增删改查能力,极大减少重复代码。
|
||||
|
||||
```php
|
||||
// 子类只需声明差异点,即可获得完整 CRUD 能力
|
||||
class ExampleLogic extends BaseLogic
|
||||
class ExampleService extends BaseService
|
||||
{
|
||||
use HasCrudOperations, HasQueryScopes, HasSerializeMaps, HasUniqueCheck;
|
||||
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
* 绑定的模型类
|
||||
*/
|
||||
protected string $modelClass = Example::class;
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $pageLikeFields = ['name'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $pageEqFields = ['status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = ['field'=>'sort', 'type'=>'asc'];
|
||||
|
||||
/**
|
||||
* 唯一性校验字段
|
||||
*
|
||||
* @var array
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $uniqueFields = ['name'];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $serializeMaps = ['status'=>'example_status'];
|
||||
protected array $serializeMaps = ['status' => 'example_status'];
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 中间件链式处理
|
||||
|
||||
请求处理采用中间件链,按注册顺序依次执行:
|
||||
请求处理采用中间件链,在 `bootstrap/app.php` 中注册别名与分组:
|
||||
|
||||
```text
|
||||
请求进入 → CORS → Auth → Tenant → Demo → Log → 控制器
|
||||
请求进入 → Proxy → Cors → ForceJson → auth.jwt → tenant → log.operation → 控制器
|
||||
└────────── 全局 api 组 ─────────┘└──────── 路由中间件组 ────────┘
|
||||
│
|
||||
响应返回 ← CORS ← Auth ← Tenant ← Demo ← Log ←───┘
|
||||
响应返回 ←────────────(逆向穿过)──────────────────┘
|
||||
(log.operation 在响应后 terminable 阶段落库)
|
||||
```
|
||||
|
||||
| 中间件 | 职责 | 配置方式 |
|
||||
|--------|------|----------|
|
||||
| CorsMiddleware | 跨域处理 | 全局,config/cors.php |
|
||||
| AuthMiddleware | JWT 认证 + RBAC 鉴权 | 路由级,config/middleware.php |
|
||||
| TenantMiddleware | 租户上下文注入 | 路由级 |
|
||||
| DemoMiddleware | 演示环境拦截 | 路由级,.env [APP] DEMO |
|
||||
| LogMiddleware | 操作日志记录 | 路由级,config/middleware.php |
|
||||
| 中间件 | 别名 | 作用域 | 职责 |
|
||||
|--------|------|--------|------|
|
||||
| `ProxyMiddleware` | proxy | 全局(api prepend) | 受信代理真实 IP 提取 |
|
||||
| `Cors` | cors | 全局 | 跨域处理(`config/cors.php`) |
|
||||
| `ForceJsonResponse` | force.json | 全局 | 强制 JSON Accept 头 |
|
||||
| `Authenticate` | auth.jwt | 路由组 | JWT 认证 + `#[Permission]` 反射鉴权 |
|
||||
| `ResolveTenant` | tenant | 路由组 | 租户上下文解析注入 |
|
||||
| `LogOperation` | log.operation | 路由组 | 操作日志采集(terminable 落库) |
|
||||
| `DemoMiddleware` | demo | 路由别名 | 演示环境写操作拦截(`#[DemoAllow]` 放行) |
|
||||
|
||||
### 5. 全局异常处理
|
||||
|
||||
`ExceptionHandle` 统一捕获所有异常并返回标准 JSON 响应:
|
||||
`App\Exceptions\Handler` 统一捕获 `api/*` 请求的异常并返回标准 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:"没有权限"}` |
|
||||
| Handler | `BizException` | 业务规则失败(Service 抛出) | `{code:异常code(默认1), msg:"业务提示"}` |
|
||||
| Handler | `ValidationException` | 代码内手动校验失败 | `{code:422, msg:"参数验证失败", data:{错误明细}}` |
|
||||
| Handler | `AuthenticationException` | 未登录 / 令牌失效 | `{code:401, msg:"未授权,请先登录"}` |
|
||||
| Handler | `NotFoundHttpException` | 路由不存在 | `{code:404, msg:"数据不存在"}` |
|
||||
| Handler | 其它异常 | 未捕获异常 | `{code:500, msg:调试模式为原文,生产为"操作失败"}` |
|
||||
| FormRequest | `HttpResponseException` | 表单格式校验失败 | `{code:1, msg:"参数验证失败", data:{字段=>错误数组}}` |
|
||||
| auth.jwt 中间件 | — | Token 缺失/过期/无权限 | `{code:401/403, ...}`(到达控制器前拦截) |
|
||||
|
||||
### 6. 统一响应格式
|
||||
|
||||
@@ -172,7 +179,7 @@ class ExampleLogic extends BaseLogic
|
||||
}
|
||||
```
|
||||
|
||||
成功响应 `code=0`,失败响应 `code=1`(ExceptionHandle 统一处理)。`code=401`(AuthMiddleware 直接返回)和 `code=403`(PermissionMiddleware 直接返回)由中间件在请求到达控制器之前拦截,不经过 ExceptionHandle。前端无需针对不同接口做差异化解析。
|
||||
成功响应 `code=0`,失败响应 `code=1`。HTTP 状态码默认 200,仅 403 同时设置对应 HTTP 状态码(便于网关处理),`code=401` 由前端跳转登录。前端无需针对不同接口做差异化解析。见 [5.16 统一响应与异常抛出](/zh/core/exception.html)。
|
||||
|
||||
### 7. 软删除策略
|
||||
|
||||
@@ -180,17 +187,17 @@ class ExampleLogic extends BaseLogic
|
||||
所有业务数据必须使用软删除(`is_delete` 字段),禁止物理删除。物理删除仅用于日志清理等特殊场景。
|
||||
:::
|
||||
|
||||
所有业务表通过 `is_delete` 字段实现软删除(0=正常,1=已删除)。`BaseModel` 的全局查询范围自动追加 `is_delete=0` 过滤,业务代码无需关心已删除数据。
|
||||
所有业务表通过 `is_delete` 字段实现软删除(0=正常,1=已删除)。`SoftDeleteFlagScope` 全局查询范围自动追加 `is_delete=0` 过滤,业务代码无需关心已删除数据。
|
||||
|
||||
```php
|
||||
// 自动过滤已删除数据
|
||||
$users = User::select(); // WHERE is_delete = 0
|
||||
$users = User::query()->get(); // WHERE is_delete = 0
|
||||
|
||||
// 查询已删除数据
|
||||
$deleted = User::withoutGlobalScope(['soft_delete'])->select();
|
||||
$deleted = User::withoutGlobalScope(SoftDeleteFlagScope::class)->get();
|
||||
|
||||
// 软删除
|
||||
$user->softDelete(); // UPDATE SET is_delete = 1
|
||||
$user->softDelete(); // UPDATE SET is_delete = 1
|
||||
|
||||
// 批量软删除
|
||||
User::batchSoftDelete([1, 2, 3]);
|
||||
@@ -198,27 +205,25 @@ User::batchSoftDelete([1, 2, 3]);
|
||||
|
||||
### 8. 审计字段自动写入
|
||||
|
||||
通过 BaseModel 的模型事件自动填充审计字段:
|
||||
通过 `HasAuditColumns` Trait 的模型事件自动填充审计字段:
|
||||
|
||||
```php
|
||||
// 创建前自动填充
|
||||
public static function onBeforeInsert($model): void
|
||||
{
|
||||
$model->create_user = request()->userInfo->username ?? '';
|
||||
$model->create_time = date('Y-m-d H:i:s');
|
||||
}
|
||||
// 创建时自动填充(已赋值则保留原值)
|
||||
static::creating(function ($model) {
|
||||
$model->create_user = $model->create_user ?? Auth::username();
|
||||
$model->create_time = $model->create_time ?? now();
|
||||
});
|
||||
|
||||
// 更新前自动填充
|
||||
public static function onBeforeUpdate($model): void
|
||||
{
|
||||
$model->update_user = request()->userInfo->username ?? '';
|
||||
$model->update_time = date('Y-m-d H:i:s');
|
||||
}
|
||||
// 更新时自动填充(强制覆盖)
|
||||
static::updating(function ($model) {
|
||||
$model->update_user = Auth::username();
|
||||
$model->update_time = now();
|
||||
});
|
||||
```
|
||||
|
||||
### 9. 多数据库支持
|
||||
|
||||
通过 `DATABASE.TYPE` 环境变量切换数据库驱动(MySQL / PostgreSQL / SQL Server / Oracle / SQLite),ThinkPHP ORM 的方言层屏蔽了底层差异,业务代码无需修改。
|
||||
通过 `DB_CONNECTION` 环境变量切换数据库驱动(MySQL / PostgreSQL / SQL Server / Oracle / SQLite),`document/` 下提供五套初始化脚本;Oracle 通过自定义 `oci` 驱动接入(`AppServiceProvider` 注册,配套 `OracleGrammar` / `OracleProcessor`)。业务代码无需修改。
|
||||
|
||||
### 10. 两级缓存架构
|
||||
|
||||
@@ -226,13 +231,13 @@ public static function onBeforeUpdate($model): void
|
||||
请求级缓存(static 变量) ← 同一请求内不重复查库
|
||||
│ 未命中
|
||||
▼
|
||||
持久缓存(file / Redis) ← 跨请求复用,1小时过期
|
||||
持久缓存(Cache 门面/Redis) ← 跨请求复用,1小时过期
|
||||
│ 未命中
|
||||
▼
|
||||
数据库查询 ← 查询后写入两级缓存
|
||||
```
|
||||
|
||||
DictService 和 ParamService 均采用此架构,大幅减少数据库查询。
|
||||
`DictCacheService` 采用此架构,配合 Observer(`DictItemObserver` / `MenuObserver` / `RoleMenuObserver`)在数据变更时自动失效缓存。详见 [4.8 缓存架构设计](/zh/architecture/cache.html)。
|
||||
|
||||
## 技术分层图
|
||||
|
||||
@@ -243,59 +248,59 @@ DictService 和 ParamService 均采用此架构,大幅减少数据库查询。
|
||||
└──────────────────────────┬───────────────────────────────────┘
|
||||
│ HTTP / JSON
|
||||
┌──────────────────────────┴───────────────────────────────────┐
|
||||
│ ThinkPHP 6.1 Application │
|
||||
│ Laravel 13 Application │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
|
||||
│ │ 中间件 │ │ 路由 │ │ 异常处理器 │ │
|
||||
│ │ ───────── │ │ ───────── │ │ ───────────────── │ │
|
||||
│ │ CORS │ │ route/ │ │ ValidateException │ │
|
||||
│ │ Auth │ │ app.php │ │ ModelNotFound │ │
|
||||
│ │ Tenant │ │ │ │ RuntimeException │ │
|
||||
│ │ Demo │ │ │ │ HttpException │ │
|
||||
│ │ Log │ │ │ │ Exception │ │
|
||||
│ │ Proxy │ │ routes/ │ │ BizException │ │
|
||||
│ │ Cors │ │ api.php │ │ ValidationException │ │
|
||||
│ │ ForceJson │ │ web.php │ │ Authentication │ │
|
||||
│ │ auth.jwt │ │ console.php│ │ NotFoundHttp │ │
|
||||
│ │ tenant │ │ │ │ Exception(500) │ │
|
||||
│ │ log.oper. │ │ │ │ │ │
|
||||
│ │ demo │ │ │ │ │ │
|
||||
│ └─────────────┘ └─────────────┘ └──────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ 控制器层(app/controller/) │ │
|
||||
│ │ BaseController · #[Permission] · #[Log] │ │
|
||||
│ │ 控制器层(app/Http/Controllers/Api/)28 个 │ │
|
||||
│ │ Controller 基类 · #[Permission] · #[OperationLog] │ │
|
||||
│ │ FormRequest 注入(24 个) │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ 逻辑层(app/logic/) │ │
|
||||
│ │ BaseLogic / BaseTenantLogic │ │
|
||||
│ │ 属性配置 · 钩子 · 文件处理 · 枚举翻译 · 数据权限 │ │
|
||||
│ │ 服务层(app/Services/)36 个 │ │
|
||||
│ │ BaseService / BaseTenantService │ │
|
||||
│ │ 6 Trait:CRUD · 查询 · 文件 · 数据权限 · 字典 · 唯一 │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ 模型层(app/model/) │ │
|
||||
│ │ BaseModel · 软删除 · 审计字段 │ │
|
||||
│ │ 模型层(app/Models/)25 个 │ │
|
||||
│ │ SoftDeletesByFlag · HasAuditColumns │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
|
||||
│ │ 服务层 │ │ 验证器 │ │ 注解 │ │
|
||||
│ │ ────────────── │ │ ───────── │ │ ───────────────── │ │
|
||||
│ │ JwtService │ │ Validate │ │ #[Log] │ │
|
||||
│ │ PasswordService │ │ │ │ #[Permission] │ │
|
||||
│ │ DictService │ │ │ │ #[DemoAllow] │ │
|
||||
│ │ ParamService │ │ │ │ │ │
|
||||
│ │ ExcelService │ │ │ │ │ │
|
||||
│ │ CaptchaService │ │ │ │ │ │
|
||||
│ │ GeneratorService│ │ │ │ │ │
|
||||
│ │ AttributeService│ │ │ │ │ │
|
||||
│ │ DbMigrateService│ │ │ │ │ │
|
||||
│ │ DbSchemaBuilder │ │ │ │ │ │
|
||||
│ │ RequestInfoServ │ │ │ │ │ │
|
||||
│ └──────────────────┘ └─────────────┘ └──────────────────────┘ │
|
||||
│ ┌──────────────────┐ ┌─────────────┐ ┌──────────────────┐ │
|
||||
│ │ 横切服务 │ │ FormRequest │ │ 注解(Attributes)│ │
|
||||
│ │ ────────────── │ │ ────────── │ │ ────────────── │ │
|
||||
│ │ JwtService │ │ BaseRequest │ │ Permission │ │
|
||||
│ │ PasswordService │ │ 24 个 │ │ OperationLog │ │
|
||||
│ │ DictCacheService │ │ │ │ DemoAllow │ │
|
||||
│ │ CaptchaService │ │ │ │ │ │
|
||||
│ │ ExcelService │ │ │ │ │ │
|
||||
│ │ GeneratorService│ │ │ │ │ │
|
||||
│ │ DbMigrateService│ │ │ │ │ │
|
||||
│ │ RequestInfo... │ │ │ │ │ │
|
||||
│ └──────────────────┘ └─────────────┘ └──────────────────┘ │
|
||||
└──────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ MySQL │ │ Redis │
|
||||
│ PostgreSQL │ │ 缓存 │
|
||||
│ SQL Server │ └─────────────┘
|
||||
│ Oracle │
|
||||
│ SQLite │
|
||||
│ PostgreSQL │ │ 会话/缓存/队列│
|
||||
│ SQL Server │ └─────────────┘
|
||||
│ Oracle │
|
||||
│ SQLite │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
@@ -304,26 +309,27 @@ DictService 和 ParamService 均采用此架构,大幅减少数据库查询。
|
||||
每个业务模块遵循统一的文件结构:
|
||||
|
||||
```text
|
||||
app/controller/UserController.php # HTTP 入口
|
||||
app/logic/UserLogic.php # 业务逻辑
|
||||
app/model/User.php # 数据模型
|
||||
app/validate/UserValidate.php # 参数验证
|
||||
app/Http/Controllers/Api/UserController.php # HTTP 入口
|
||||
app/Http/Requests/UserRequest.php # 参数验证
|
||||
app/Services/UserService.php # 业务逻辑
|
||||
app/Models/User.php # 数据模型
|
||||
```
|
||||
|
||||
| 文件 | 职责 | 基类 |
|
||||
|------|------|------|
|
||||
| Controller | 接收请求、调用 Logic、返回响应 | BaseController |
|
||||
| Logic | 业务逻辑、属性配置、钩子处理 | BaseLogic / BaseTenantLogic |
|
||||
| Model | 数据库表映射 | BaseModel |
|
||||
| Validate | 参数校验规则 | ThinkPHP Validate |
|
||||
| Controller | 接收请求、调用 Service、返回响应 | `App\Http\Controllers\Controller` |
|
||||
| Request | 参数格式校验 | `BaseRequest` |
|
||||
| Service | 业务逻辑、属性配置、钩子处理 | `BaseService` / `BaseTenantService` |
|
||||
| Model | 数据库表映射 | Eloquent Model + Concerns Trait |
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
|------|------|------|
|
||||
| 认证方案 | JWT 双令牌 | 无状态,适合分布式部署 |
|
||||
| 删除策略 | 软删除(is_delete) | 数据可恢复,避免误删 |
|
||||
| 字段命名 | 数据库 snake_case / 前端 camelCase | 各端遵循各自惯例 |
|
||||
| 缓存策略 | 两级缓存(请求级 + 持久缓存) | 减少数据库查询 |
|
||||
| 数据库 | 多数据库支持 | 适配不同客户需求 |
|
||||
| 权限模型 | RBAC(用户→角色→菜单) | 灵活的权限配置 |
|
||||
| 认证方案 | JWT 双令牌(Redis 有状态) | 无状态便于水平扩展,有状态支持踢下线/改密全端失效 |
|
||||
| 删除策略 | 软删除(`is_delete`) | 数据可恢复,唯一性校验可排除已删数据 |
|
||||
| 字段命名 | 数据库 snake_case / 响应 camelCase | `API_CAMEL_SNAKE_CONVERT` 自动转换,各端遵循各自惯例 |
|
||||
| 缓存策略 | 两级缓存(请求级 + Redis) | 减少数据库查询,Observer 保证一致性 |
|
||||
| 数据库 | 五种数据库 + 五套脚本 | 适配不同客户需求 |
|
||||
| 权限模型 | RBAC(用户→角色→菜单,菜单即权限) | 灵活的权限配置,注解声明即生效 |
|
||||
| 安全基线 | JWT_SECRET fail-close、生产 APP_DEBUG=false | 敏感信息不泄露、弱密钥不启动 |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 4.2 请求处理链路
|
||||
|
||||
:::tip 链路概览
|
||||
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS 中间件 → Auth 中间件(JWT 认证 + RBAC 鉴权)→ Tenant 中间件 → Demo 中间件 → Log 中间件 → 控制器 → 逻辑层 → 模型层 → 数据库 → 响应序列化 → 浏览器
|
||||
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → Proxy 中间件(真实 IP)→ CORS → 强制 JSON → auth.jwt(JWT 认证 + RBAC 鉴权)→ tenant(租户上下文)→ log.operation(操作日志采集)→ FormRequest 校验 → 控制器 → 服务层 → 模型层 → 数据库 → 响应序列化 → 浏览器 →(响应后)操作日志落库
|
||||
:::
|
||||
|
||||
本节以 `POST /api/example/add` 添加案例为例,逐步拆解完整链路。
|
||||
本节以 `POST /api/example/add` 添加案例为例,逐步拆解完整链路(代码摘自项目实际源码)。
|
||||
|
||||
## 第一阶段:网络层
|
||||
|
||||
@@ -22,18 +22,17 @@ export function exampleAdd(data: any) {
|
||||
```
|
||||
|
||||
前端通过 Axios 发起 HTTP POST 请求,携带:
|
||||
|
||||
- `Authorization: Bearer <JWT Token>` 请求头(由请求拦截器自动添加)
|
||||
- `Content-Type: application/json` 请求头
|
||||
- JSON 请求体
|
||||
|
||||
### 2. Nginx 反向代理(生产环境)
|
||||
### 2. Nginx 反向代理(生产环境 / Docker)
|
||||
|
||||
```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;
|
||||
# docker/nginx.conf(本项目实际配置)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -53,112 +52,197 @@ server: {
|
||||
|
||||
## 第二阶段:路由解析
|
||||
|
||||
### 3. ThinkPHP 入口
|
||||
### 3. Laravel 入口
|
||||
|
||||
```text
|
||||
public/index.php → 引导框架 → 路由解析 route/app.php
|
||||
public/index.php → bootstrap/app.php(中间件别名/分组、健康检查)→ 路由解析 routes/api.php
|
||||
```
|
||||
|
||||
### 4. 路由匹配
|
||||
|
||||
```php
|
||||
// route/app.php
|
||||
// routes/api.php
|
||||
|
||||
// 公开接口(无需认证)
|
||||
Route::group('api', function () {
|
||||
Route::post('login', 'LoginController/login');
|
||||
Route::get('captcha', 'LoginController/captcha');
|
||||
// 公开接口(无需认证)——放在中间件组外
|
||||
Route::post('login', [Api\AuthController::class, 'login']);
|
||||
|
||||
// 认证接口
|
||||
Route::middleware(['auth.jwt', 'tenant', 'demo', 'log.operation'])->group(function () {
|
||||
// --- 案例演示 ---
|
||||
Route::post('example/add', [Api\ExampleController::class, 'add']); // ← 匹配到此处
|
||||
});
|
||||
```
|
||||
|
||||
// 需认证接口
|
||||
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, // 操作日志
|
||||
同时,`bootstrap/app.php` 的全局 `api` 中间件组已 prepend 三个前置中间件:
|
||||
|
||||
```php
|
||||
// bootstrap/app.php
|
||||
$middleware->alias([
|
||||
'proxy' => ProxyMiddleware::class,
|
||||
'cors' => Cors::class,
|
||||
'force.json' => ForceJsonResponse::class,
|
||||
'auth.jwt' => Authenticate::class,
|
||||
'tenant' => ResolveTenant::class,
|
||||
'demo' => DemoMiddleware::class,
|
||||
'log.operation' => LogOperation::class,
|
||||
]);
|
||||
|
||||
// API 中间件组(按顺序:Proxy → Cors → ForceJson)
|
||||
$middleware->api(prepend: [
|
||||
ProxyMiddleware::class,
|
||||
Cors::class,
|
||||
ForceJsonResponse::class,
|
||||
]);
|
||||
```
|
||||
|
||||
路由匹配到 `POST api/example/add` → `ExampleController::add()`,同时绑定四个中间件。
|
||||
完整链路:`ProxyMiddleware → Cors → ForceJsonResponse → auth.jwt → tenant → log.operation → 控制器`。
|
||||
|
||||
## 第三阶段:中间件链(按顺序执行)
|
||||
|
||||
### 5. CorsMiddleware(全局中间件)
|
||||
### 5. ProxyMiddleware(全局,真实 IP)
|
||||
|
||||
```php
|
||||
// app/middleware/CorsMiddleware.php
|
||||
// app/Http/Middleware/ProxyMiddleware.php(节选)
|
||||
|
||||
public function handle(Request $request, \Closure $next): Response
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 处理 OPTIONS 预检请求
|
||||
if ($request->isOptions()) {
|
||||
$response = Response::create('', 'html', 204);
|
||||
$remoteAddr = $request->server('REMOTE_ADDR', '');
|
||||
$proxyIps = $this->getTrustedProxies();
|
||||
|
||||
// 当前连接来源不在受信列表中,跳过(防止伪造代理头)
|
||||
if (! $this->isTrustedProxy($remoteAddr, $proxyIps)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// 按优先级提取真实 IP(X-Real-IP > X-Forwarded-For 第一个合法值)
|
||||
$realIp = $this->extractRealIp($request);
|
||||
|
||||
if (! empty($realIp) && $realIp !== $remoteAddr) {
|
||||
// 通过 server() 方法替换 REMOTE_ADDR,后续 ip() 即返回真实 IP
|
||||
$server = $request->server->all();
|
||||
$server['REMOTE_ADDR'] = $realIp;
|
||||
$request->server->replace($server);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
```
|
||||
|
||||
- 部署在 CDN / 负载均衡后时保证 `request()->ip()` 正确(日志、限流依赖)
|
||||
- 受信代理由 `.env` 的 `TRUSTED_PROXIES` 配置,未配置默认信任 `127.0.0.1` 与 `::1`
|
||||
|
||||
### 6. Cors(全局,跨域处理)
|
||||
|
||||
```php
|
||||
// app/Http/Middleware/Cors.php(节选)
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 预检请求直接返回,不进入业务逻辑(Authenticate 等中间件不会执行)
|
||||
if ($request->isMethod('OPTIONS')) {
|
||||
$response = response('', 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',
|
||||
]);
|
||||
$config = config('cors');
|
||||
$origin = $request->header('Origin', '');
|
||||
$allowOrigins = $config['allow_origin'] ?? ['*'];
|
||||
$allowCredentials = $config['allow_credentials'] ?? true;
|
||||
|
||||
$originAllowed = false;
|
||||
|
||||
if (in_array('*', $allowOrigins)) {
|
||||
if ($allowCredentials && $origin) {
|
||||
// 凭证模式下不能用 *,回显具体 Origin
|
||||
$response->headers->set('Access-Control-Allow-Origin', $origin);
|
||||
$response->headers->set('Vary', 'Origin'); // 防止 CDN 缓存错误的 CORS 响应
|
||||
} else {
|
||||
$response->headers->set('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
$originAllowed = true;
|
||||
} elseif ($origin && in_array($origin, $allowOrigins)) {
|
||||
// 白名单匹配:仅回显请求的 Origin
|
||||
$response->headers->set('Access-Control-Allow-Origin', $origin);
|
||||
$response->headers->set('Vary', 'Origin');
|
||||
$originAllowed = true;
|
||||
}
|
||||
|
||||
// 仅在 Origin 允许时设置其余 CORS 头
|
||||
if ($originAllowed) {
|
||||
$response->headers->set('Access-Control-Allow-Methods', $config['allow_methods'] ?? 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
||||
$response->headers->set('Access-Control-Allow-Headers', $config['allow_headers'] ?? 'Content-Type, Authorization');
|
||||
$response->headers->set('Access-Control-Expose-Headers', $config['expose_headers'] ?? 'Authorization');
|
||||
$response->headers->set('Access-Control-Max-Age', $config['max_age'] ?? 3600);
|
||||
|
||||
if ($allowCredentials) {
|
||||
$response->headers->set('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
```
|
||||
|
||||
- 每个请求都经过(全局中间件)
|
||||
- OPTIONS 预检请求直接返回 204
|
||||
- 普通请求附加 CORS 响应头
|
||||
|
||||
### 6. AuthMiddleware(JWT 认证 + 权限校验)
|
||||
### 7. ForceJsonResponse(全局,强制 JSON)
|
||||
|
||||
```php
|
||||
// app/middleware/AuthMiddleware.php
|
||||
// app/Http/Middleware/ForceJsonResponse.php
|
||||
|
||||
public function handle(Request $request, \Closure $next): Response
|
||||
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);
|
||||
}
|
||||
// 强制声明期望 JSON,确保异常与验证失败均以 JSON 返回
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
```
|
||||
|
||||
使后续 `Handler::render()` 的 `$request->wantsJson()` 恒为 true,框架错误页不会以 HTML 形式返回。
|
||||
|
||||
### 8. Authenticate(auth.jwt,JWT 认证 + 权限校验)
|
||||
|
||||
```php
|
||||
// app/Http/Middleware/Authenticate.php(节选)
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 获取 token:带 Bearer 前缀则剥离,裸 token 直接使用
|
||||
$header = $request->header('Authorization', '');
|
||||
if ($header === '') {
|
||||
return response()->json([
|
||||
'code' => 401, 'ok' => false,
|
||||
'msg' => '未提供认证令牌', 'data' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
// 2. 从请求头获取 Token
|
||||
$token = Jwt::getTokenFromHeader();
|
||||
if (empty($token)) {
|
||||
return json(['code' => 401, 'msg' => '未提供认证令牌']);
|
||||
}
|
||||
$prefix = config('jwt.header_prefix', 'Bearer ');
|
||||
$token = str_starts_with($header, $prefix) ? substr($header, strlen($prefix)) : $header;
|
||||
|
||||
// 3. 解析 Token,校验签名和有效期
|
||||
// 解析 token
|
||||
$jwtService = app(JwtService::class);
|
||||
try {
|
||||
$decoded = Jwt::decode($token);
|
||||
} catch (\RuntimeException $e) {
|
||||
return json(['code' => 401, 'msg' => $e->getMessage()]);
|
||||
$decoded = $jwtService->parseToken($token);
|
||||
} catch (\Exception $e) {
|
||||
// 令牌解析失败:body code=401,消息取自异常
|
||||
return response()->json([
|
||||
'code' => 401, 'ok' => false,
|
||||
'msg' => $e->getMessage(), 'data' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
// 4. 校验 token 类型必须为 access
|
||||
if (($decoded->type ?? '') !== 'access') {
|
||||
return json(['code' => 401, 'msg' => '无效的认证令牌']);
|
||||
}
|
||||
// 注入用户信息,供控制器通过 $request->attributes->get('userInfo') 读取
|
||||
$request->attributes->set('userInfo', $decoded);
|
||||
|
||||
// 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' => '无访问权限']);
|
||||
// 权限校验:仅当方法标注了 #[Permission] 时才校验
|
||||
$permissionCode = $this->getPermissionCode($request);
|
||||
if ($permissionCode !== null) {
|
||||
$uid = $decoded->uid ?? 0;
|
||||
if (! $this->checkPermission($uid, $permissionCode)) {
|
||||
return response()->json([
|
||||
'code' => 403, 'ok' => false,
|
||||
'msg' => '无访问权限:'.$permissionCode, 'data' => null,
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,57 +255,76 @@ public function handle(Request $request, \Closure $next): Response
|
||||
```text
|
||||
请求进入
|
||||
│
|
||||
├─ 路径在排除列表? → 直接放行
|
||||
├─ 提取 Authorization 头(带 Bearer 前缀则剥离,裸 token 亦可)
|
||||
│ └─ 头为空 → 401「未提供认证令牌」
|
||||
│
|
||||
├─ 提取 Authorization: Bearer <token>
|
||||
│ └─ 为空 → 返回 401
|
||||
├─ JwtService::parseToken($token) 解析验证
|
||||
│ └─ 失败(过期/签名错误)→ 401(消息取自异常)
|
||||
│
|
||||
├─ Jwt::decode($token) 解析验证
|
||||
│ └─ 失败(过期/签名错误)→ 返回 401
|
||||
├─ 注入 $request->attributes['userInfo'] = {uid, username, ...}
|
||||
│
|
||||
├─ 校验 type === 'access'
|
||||
│ └─ 不是 → 返回 401
|
||||
│
|
||||
├─ 注入 $request->userInfo = {uid, username, type, exp}
|
||||
│
|
||||
├─ 读取 #[Permission] 注解
|
||||
│ ├─ 无注解 → 跳过权限校验
|
||||
│ └─ 有注解 → checkPermission()
|
||||
├─ 反射读取控制器方法的 #[Permission] 注解(ReflectionMethod,微秒级)
|
||||
│ ├─ 无注解 → 跳过权限校验,仅完成认证
|
||||
│ └─ 有注解 → checkPermission(uid, code)
|
||||
│ ├─ uid === 1 → 直接放行(超级管理员)
|
||||
│ └─ 查询 UserRole → RoleMenu → Menu.permission
|
||||
│ ├─ 包含 → 放行
|
||||
│ └─ 不包含 → 返回 403
|
||||
│ ├─ Redis 读 perms:{uid}
|
||||
│ │ ├─ 命中 *:*:* 或含目标编码 → 通过
|
||||
│ │ └─ Redis 不可用 → 降级
|
||||
│ ├─ Laravel Cache 读 perms_fallback:{uid}(300 秒)
|
||||
│ └─ 未命中 → 查库 user_role → role_menu → menu.permission
|
||||
│ ├─ 包含 → 放行(回填二级缓存)
|
||||
│ └─ 不包含 → 403「无访问权限:sys:example:add」
|
||||
│
|
||||
└─ 放行 → 进入下一个中间件
|
||||
```
|
||||
|
||||
### 7. TenantMiddleware(租户上下文)
|
||||
:::tip 三级权限缓存
|
||||
Redis(`perms:{uid}`,一级)→ Laravel Cache(`perms_fallback:{uid}` 5 分钟,二级)→ 数据库(回填)。菜单/角色菜单变更时 Observer 清除相关缓存,保证权限调整即时生效。
|
||||
:::
|
||||
|
||||
### 9. ResolveTenant(tenant,租户上下文)
|
||||
|
||||
```php
|
||||
// app/middleware/TenantMiddleware.php
|
||||
// app/Http/Middleware/ResolveTenant.php(节选)
|
||||
|
||||
public function handle(Request $request, \Closure $next): Response
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$userInfo = $request->userInfo ?? null;
|
||||
$userId = $userInfo->uid ?? 0;
|
||||
$userInfo = $request->attributes->get('userInfo');
|
||||
if (! $userInfo) {
|
||||
// 未认证请求:不注入租户上下文,直接放行
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$uid = $userInfo->uid ?? 0;
|
||||
|
||||
// 超级管理员(uid=1):跳过租户校验,直接注入 tenantId=0
|
||||
if ($uid === 1) {
|
||||
$request->attributes->set('tenantId', 0);
|
||||
$request->attributes->set('isSuperAdmin', true);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// 非超管:查库取 tenant_id(JWT 不含 tenant_id,需查库)
|
||||
$user = User::find($uid);
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'code' => 401, 'ok' => false,
|
||||
'msg' => '用户不存在,请重新登录', 'data' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$user = User::find($userId);
|
||||
$tenantId = $user->tenant_id ?? 0;
|
||||
$request->attributes->set('tenantId', $tenantId);
|
||||
$request->attributes->set('isSuperAdmin', false);
|
||||
|
||||
// 注入租户上下文
|
||||
$request->tenantId = $tenantId;
|
||||
$request->isSuperAdmin = ($tenantId == 0);
|
||||
|
||||
// 非超级管理员校验租户状态
|
||||
// 平台用户(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;
|
||||
|
||||
// 租户不存在 / 被禁用(status != 1)/ 已过期(expire_time->isPast())
|
||||
// → 均返回 403 对应提示
|
||||
$request->attributes->set('tenantInfo', $tenant);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
@@ -229,208 +332,177 @@ public function handle(Request $request, \Closure $next): Response
|
||||
```
|
||||
|
||||
注入内容:
|
||||
- `$request->tenantId` — 租户 ID(0=超级管理员)
|
||||
- `$request->isSuperAdmin` — 是否超级管理员
|
||||
- `$request->tenantInfo` — 租户信息对象
|
||||
|
||||
### 8. DemoMiddleware(演示环境拦截)
|
||||
- `tenantId` — 租户 ID(0=平台用户)
|
||||
- `isSuperAdmin` — 是否超级管理员(uid=1)
|
||||
- `tenantInfo` — 租户模型实例(`tenantId > 0` 时)
|
||||
|
||||
### 10. LogOperation(log.operation,操作日志采集)
|
||||
|
||||
`handle` 阶段**只收集不写入**,`terminate` 阶段在响应完成后落库:
|
||||
|
||||
```php
|
||||
// app/middleware/DemoMiddleware.php
|
||||
// app/Http/Middleware/LogOperation.php(节选)
|
||||
|
||||
public function handle(Request $request, \Closure $next): Response
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 非演示环境直接放行
|
||||
if (!env('app_demo', false)) {
|
||||
return $next($request);
|
||||
$this->logContext = [
|
||||
'source' => 0, // 来源端:0=后台,1=前台
|
||||
'method' => strtoupper($request->method()), // HTTP 方法
|
||||
'request_method' => $request->route()?->getActionMethod() ?? '', // 控制器方法名
|
||||
'url' => $request->fullUrl(), // 完整请求地址
|
||||
'param' => mb_substr(json_encode($request->except(['password', 'token']), JSON_UNESCAPED_UNICODE), 0, 2000), // 排除敏感字段,截断 2000 字
|
||||
'ip' => $request->ip(),
|
||||
];
|
||||
|
||||
$this->logAttr = $this->getLogAttribute($request); // 反射读取 #[OperationLog]
|
||||
$this->shouldLog = $this->determineIfShouldLog($request->method(), $this->logAttr);
|
||||
|
||||
// 预创建请求信息服务(handle 阶段 request 信息完整,terminate 阶段可能已丢失)
|
||||
if ($this->shouldLog) {
|
||||
$this->requestInfo = RequestInfoService::create($request);
|
||||
}
|
||||
|
||||
// GET 请求放行(读操作)
|
||||
if (strtoupper($request->method()) === 'GET') {
|
||||
return $next($request);
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
public function terminate(Request $request, Response $response): void
|
||||
{
|
||||
if (! $this->shouldLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 #[DemoAllow] 注解
|
||||
if (AttributeService::hasDemoAllow($controllerClass, $action)) {
|
||||
return $next($request);
|
||||
}
|
||||
try {
|
||||
// 补充响应内容、IP 归属地、操作系统、浏览器、耗时、状态
|
||||
$this->logContext['result'] = mb_substr($response->getContent() ?? '', 0, 2000);
|
||||
$this->logContext['location'] = $this->requestInfo->getIpLocation();
|
||||
$this->logContext['os'] = $this->requestInfo->getOs();
|
||||
$this->logContext['browser'] = $this->requestInfo->getBrowser();
|
||||
$this->logContext['consume_time'] = defined('LARAVEL_START')
|
||||
? round((microtime(true) - LARAVEL_START) * 1000) : 0;
|
||||
$this->logContext['status'] = $this->detectStatus($response->getContent()); // code=0/200 → 0,其余 → 1
|
||||
$this->logContext['create_user'] = $request->attributes->get('userInfo')->username ?? '';
|
||||
|
||||
// 拦截写操作
|
||||
return json(['code' => 403, 'msg' => '演示环境,禁止操作']);
|
||||
OperationLog::create($this->logContext);
|
||||
} catch (\Exception $e) {
|
||||
// 日志写入失败不影响业务
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. LogMiddleware(操作日志)
|
||||
记录规则(`determineIfShouldLog`):
|
||||
|
||||
```php
|
||||
// app/middleware/LogMiddleware.php
|
||||
- `POST/PUT/DELETE/PATCH` 一律记录
|
||||
- `GET` 仅在注解类型为 `EXPORT / DOWNLOAD / IMPORT` 时记录
|
||||
- 其它方法(OPTIONS/HEAD)不记录
|
||||
|
||||
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;
|
||||
}
|
||||
```
|
||||
:::tip DemoMiddleware 说明
|
||||
演示模式中间件(`demo` 别名)已挂载于认证路由组(`auth.jwt` → `tenant` → `demo` → `log.operation`):非演示模式(`DEMO_MODE=false`)直接放行;演示模式下 GET 放行、写操作需 `#[DemoAllow]` 注解,否则 403。拦截发生在 `LogOperation` 之前,被拦截请求不产生操作日志。
|
||||
:::
|
||||
|
||||
## 第四阶段:控制器层
|
||||
|
||||
### 10. ExampleController::add()
|
||||
### 11. FormRequest 参数校验(进入方法前)
|
||||
|
||||
```php
|
||||
// app/controller/ExampleController.php
|
||||
// 控制器方法签名声明 ExampleRequest,框架在校验通过后才调用方法
|
||||
public function add(ExampleRequest $request): JsonResponse
|
||||
```
|
||||
|
||||
#[Log('案例演示-新增记录', Log::TYPE_ADD, '新增案例演示:{name}')]
|
||||
#[Permission('sys:example:add', '添加案例演示')]
|
||||
public function add(): Json
|
||||
- `rules()` 未通过 → `BaseRequest::failedValidation()` 直接返回
|
||||
`{"code":1,"ok":false,"msg":"参数验证失败","data":{"name":["案例名称不能为空"]}}`
|
||||
- 校验通过 → 控制器方法执行,`$request->validated()` 取已校验数据(键名转换在 Service 写入入口完成)
|
||||
|
||||
### 12. ExampleController::add()
|
||||
|
||||
```php
|
||||
// app/Http/Controllers/Api/ExampleController.php
|
||||
|
||||
#[OperationLog('示例管理-新增', OperationLog::TYPE_ADD, '新增示例:{name}')]
|
||||
#[Permission('sys:example:add', '添加示例')]
|
||||
public function add(ExampleRequest $request): JsonResponse
|
||||
{
|
||||
// getJsonBody() 获取前端 POST 提交的 JSON 数据
|
||||
// _add() 内部调用 $this->logic->add($data)
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
return $this->_add($this->exampleService, $request->validated());
|
||||
}
|
||||
```
|
||||
|
||||
控制器层职责:
|
||||
- 通过 `#[Permission]` 声明所需权限
|
||||
- 通过 `#[Log]` 声明日志信息
|
||||
- 获取参数,调用 Logic 层,返回响应
|
||||
控制器层职责:注解声明权限与日志 → 参数由 FormRequest 保证 → 调用快捷方法 → 返回统一响应。
|
||||
|
||||
## 第五阶段:逻辑层
|
||||
## 第五阶段:服务层
|
||||
|
||||
### 11. ExampleLogic::add()
|
||||
### 13. ExampleService::add()(HasCrudOperations 主流程)
|
||||
|
||||
```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;
|
||||
}
|
||||
```text
|
||||
$this->_add($service, $data)
|
||||
│
|
||||
▼
|
||||
Service::add($data) ← HasCrudOperations
|
||||
├─ beforeAdd($data) 钩子 ← 可改写数据、抛 BizException 拦截
|
||||
├─ checkUnique($data) ← uniqueFields 唯一性校验,重复抛 BizException
|
||||
├─ DB::transaction { ← 事务包裹(任一异常自动回滚)
|
||||
│ Model::create($data) ← Eloquent 写入
|
||||
│ }
|
||||
├─ afterAdd($id, $data) 钩子 ← 关联处理(抛异常同样回滚)
|
||||
└─ return $id
|
||||
```
|
||||
|
||||
:::tip 异常即控制流
|
||||
`BizException` 在钩子/唯一性校验中抛出 → 中断事务闭包回滚 → 由 `_add()` 捕获转为 `fail(消息, code)`,或由全局 `Handler` 兜底——任何路径前端拿到的结构一致。
|
||||
:::
|
||||
|
||||
## 第六阶段:模型层
|
||||
|
||||
### 12. BaseModel 自动处理
|
||||
### 14. HasAuditColumns 自动填充 + SQL 执行
|
||||
|
||||
```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
|
||||
// app/Models/Concerns/HasAuditColumns.php(节选)
|
||||
static::creating(function ($model) {
|
||||
$model->create_user = $model->create_user ?? Auth::username();
|
||||
$model->create_time = $model->create_time ?? now();
|
||||
});
|
||||
```
|
||||
|
||||
### 13. 数据库执行
|
||||
|
||||
ThinkPHP ORM 将操作转换为 SQL:
|
||||
Eloquent 将操作转换为 SQL:
|
||||
|
||||
```sql
|
||||
INSERT INTO think_example (name, status, sort, avatar, create_user, create_time)
|
||||
VALUES ('测试案例', 1, 0, '', 'admin', '2026-09-20 12:00:00');
|
||||
INSERT INTO laravel_example (name, status, sort, create_user, create_time)
|
||||
VALUES ('测试案例', 1, 0, 'admin', '2026-10-02 12:00:00');
|
||||
```
|
||||
|
||||
返回自增 ID(MySQL `lastInsertId`;Oracle 走序列 CURRVAL,见 `OracleProcessor`)。
|
||||
|
||||
## 第七阶段:响应返回
|
||||
|
||||
### 14. 响应序列化
|
||||
### 15. 响应序列化
|
||||
|
||||
```php
|
||||
// BaseController::_add()
|
||||
protected function _add(BaseLogic $logic, array $data): Json
|
||||
{
|
||||
$id = $logic->add($data);
|
||||
return $this->success(['id' => $id], '添加成功');
|
||||
}
|
||||
// Controller::_add() 内部
|
||||
$id = $service->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
|
||||
]);
|
||||
}
|
||||
// Support\Result::success()(节选)
|
||||
return response()->json([
|
||||
'code' => self::SUCCESS, // 0
|
||||
'ok' => true,
|
||||
'msg' => $msg,
|
||||
'data' => convert_to_camel($data), // snake_case → camelCase
|
||||
]);
|
||||
```
|
||||
|
||||
### 15. 浏览器接收响应
|
||||
### 16. 浏览器接收响应 + 日志落库
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"ok": true,
|
||||
"msg": "添加成功",
|
||||
"data": {
|
||||
"id": 42
|
||||
}
|
||||
"data": { "id": 42 }
|
||||
}
|
||||
```
|
||||
|
||||
前端 Axios 拦截器统一处理:`code === 0` 时展示成功提示,否则展示错误信息。
|
||||
响应发送后,Laravel 调用 `LogOperation::terminate()` 将本次操作写入 `laravel_operation_log`(耗时、状态、归属地等)。
|
||||
|
||||
前端 Axios 拦截器统一处理:`code === 0` 时展示成功提示,否则展示错误信息;401 触发登出跳转。
|
||||
|
||||
## 完整链路流程图
|
||||
|
||||
@@ -440,93 +512,75 @@ public static function success($data, string $msg = '操作成功'): Json
|
||||
│ Authorization: Bearer <token>
|
||||
▼
|
||||
[Nginx / Vite Proxy] ── 反向代理转发
|
||||
│
|
||||
▼
|
||||
[ThinkPHP 入口] ── public/index.php
|
||||
│
|
||||
[public/index.php] ── bootstrap/app.php 引导
|
||||
▼
|
||||
[路由解析] ── route/app.php 匹配 api/example/add
|
||||
│
|
||||
[路由解析] ── routes/api.php 匹配 api/example/add(挂 auth.jwt + tenant + demo + log.operation)
|
||||
▼
|
||||
[CorsMiddleware] ── 跨域校验通过(全局)
|
||||
│
|
||||
[ProxyMiddleware] ── 受信代理校验,替换真实 REMOTE_ADDR(全局)
|
||||
▼
|
||||
[AuthMiddleware]
|
||||
├─ 提取 Token → Jwt::decode()
|
||||
├─ 校验 type === 'access'
|
||||
├─ 注入 $request->userInfo
|
||||
├─ 读取 #[Permission] 注解
|
||||
├─ checkPermission() 权限校验
|
||||
│ ├─ uid=1 → 跳过(超级管理员)
|
||||
│ └─ UserRole → RoleMenu → Menu.permission
|
||||
└─ 放行
|
||||
│
|
||||
[Cors] ── OPTIONS 直接 204;响应追加 CORS 头(全局)
|
||||
▼
|
||||
[TenantMiddleware]
|
||||
├─ 查询用户 tenant_id
|
||||
├─ 注入 $request->tenantId
|
||||
└─ 校验租户状态(禁用/过期 → 403)
|
||||
│
|
||||
[ForceJsonResponse] ── Accept 强制 application/json(全局)
|
||||
▼
|
||||
[Authenticate]
|
||||
├─ 提取 Authorization token(头为空 → 401)
|
||||
├─ JwtService::parseToken()(过期/签名错 → 401)
|
||||
├─ 注入 attributes['userInfo']
|
||||
├─ 反射读取 #[Permission] 注解
|
||||
└─ checkPermission():uid=1 跳过 → Redis perms:{uid} → Cache 降级 → 查库
|
||||
└─ 不通过 → 403「无访问权限:sys:example:add」
|
||||
▼
|
||||
[ResolveTenant]
|
||||
├─ 无 userInfo → 放行
|
||||
├─ uid=1 → tenantId=0, isSuperAdmin=true
|
||||
└─ 非超管 → 查库 tenant_id → 状态/过期校验(禁用/过期/不存在 → 403)
|
||||
▼
|
||||
[DemoMiddleware]
|
||||
├─ 非演示环境 → 放行
|
||||
├─ 非演示模式(DEMO_MODE=false)→ 放行
|
||||
├─ GET 请求 → 放行
|
||||
└─ 有 #[DemoAllow] → 放行
|
||||
│
|
||||
└─ 写操作:有 #[DemoAllow] 放行,否则 403「演示模式下禁止操作」
|
||||
▼
|
||||
[LogMiddleware] ── 记录开始时间
|
||||
│
|
||||
[LogOperation::handle] ── 收集上下文、读 #[OperationLog]、判断是否记录
|
||||
▼
|
||||
[ExampleRequest] ── rules() 校验(失败 → code=1 + 字段明细)
|
||||
▼
|
||||
[ExampleController::add()]
|
||||
├─ #[Log] 注解元数据
|
||||
├─ #[Permission] 注解元数据
|
||||
└─ parent::_add($this->logic, $data)
|
||||
│
|
||||
└─ $this->_add($service, $request->validated())
|
||||
▼
|
||||
[ExampleLogic::add()]
|
||||
[ExampleService::add()]
|
||||
├─ beforeAdd() 钩子
|
||||
├─ array_camel_to_snake() 键名转换
|
||||
├─ processTenantId() 租户填充
|
||||
├─ processFileFieldsOnSave() 文件处理
|
||||
├─ filterTableFields() 字段过滤
|
||||
├─ checkUnique() 唯一性校验
|
||||
├─ Model::create() 写入数据库
|
||||
├─ checkUnique()(重复 → BizException → 回滚 → code=1)
|
||||
├─ DB::transaction { Model::create() }
|
||||
│ └─ HasAuditColumns:create_user / create_time 自动填充
|
||||
└─ afterAdd() 钩子
|
||||
│
|
||||
▼
|
||||
[BaseModel]
|
||||
├─ onBeforeInsert → create_user, create_time
|
||||
└─ 执行 SQL INSERT
|
||||
│
|
||||
▼
|
||||
[数据库] ── 返回自增 ID
|
||||
│
|
||||
▼ 逐层返回响应
|
||||
[Result::success()] ── {code:0, ok:true, msg:"添加成功", data:{id:42}}
|
||||
│
|
||||
▼
|
||||
[LogMiddleware] ── 记录操作日志(写操作)
|
||||
│
|
||||
[数据库] ── INSERT INTO laravel_example ... → 自增 ID
|
||||
▼ 逐层返回
|
||||
[Result::success] ── {code:0, ok:true, msg:"添加成功", data:{id:42}}(驼峰转换)
|
||||
▼
|
||||
[浏览器] ← JSON 响应
|
||||
▼(响应完成后)
|
||||
[LogOperation::terminate] ── 写入操作日志(失败静默,不影响业务)
|
||||
```
|
||||
|
||||
## 异常处理链路
|
||||
|
||||
当链路中发生异常时,ExceptionHandle 统一捕获并返回标准响应:
|
||||
当链路中发生异常时,`Handler` 统一捕获并返回标准响应:
|
||||
|
||||
| 异常类型 | 触发场景 | 响应 |
|
||||
|---------|---------|------|
|
||||
| `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:"操作失败"}`(生产环境隐藏详情) |
|
||||
| `HttpResponseException`(FormRequest) | 表单格式校验失败 | `{code:1, msg:"参数验证失败", data:{字段=>错误数组}}` |
|
||||
| `BizException` | 业务规则失败 | `{code:1(或自定义), msg:"业务提示"}` |
|
||||
| `ValidationException` | 代码内手动校验失败 | `{code:422, msg:"参数验证失败", data:{错误明细}}` |
|
||||
| `AuthenticationException` | 未登录 / 令牌失效 | `{code:401, msg:"未授权,请先登录"}` |
|
||||
| `NotFoundHttpException` | 路由不存在 | `{code:404, msg:"数据不存在"}` |
|
||||
| 其它异常 | 未捕获异常 | `{code:500, msg:调试模式为原文, 生产为"操作失败"}` |
|
||||
|
||||
:::warning 异常处理原则
|
||||
所有异常均返回 HTTP 200 状态码,通过响应体 `code` 字段区分业务成功(0)与失败(1/401/403/404/422)。前端统一按 `code` 判断,无需处理 HTTP 状态码差异。
|
||||
除 403(中间件直接返回时同时设置 HTTP 状态码)外,所有响应 HTTP 状态码为 200,通过响应体 `code` 字段区分业务成功(0)与失败。前端统一按 `code` 判断,`code=401` 时跳转登录,无需处理 HTTP 状态码差异。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
一个 API 请求从浏览器到数据库经过 7 个阶段、15+ 个处理节点。中间件链负责横切关注点(认证、权限、租户、日志),控制器层负责参数获取和响应返回,逻辑层负责业务处理和数据加工,模型层负责数据库操作。这种分层设计使得每一层职责单一、可独立测试。
|
||||
一个 API 请求从浏览器到数据库经过 7 个阶段、16+ 个处理节点。中间件链负责横切关注点(真实 IP、跨域、JSON 协商、认证、权限、租户、日志),FormRequest 负责参数格式,控制器层负责编排与响应封装,服务层负责业务处理与事务,模型层负责数据持久化。这种分层设计使得每一层职责单一、可独立测试。
|
||||
|
||||
@@ -4,42 +4,42 @@
|
||||
本章详细阐述系统的安全架构设计,围绕认证、授权、防护、审计四大安全领域展开,旨在帮助开发者全面理解系统的安全保障体系。
|
||||
:::
|
||||
|
||||
**认证(Authentication)**:系统采用基于 JWT 的无状态认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 `Authorization: Bearer <token>` 头传递,由 `AuthMiddleware` 统一拦截并验证 Token 的有效性(签名校验、有效期检查)。
|
||||
**认证(Authentication)**:系统采用基于 JWT 的双令牌认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 `Authorization: Bearer <token>` 头传递,由 `Authenticate` 中间件(别名 `auth.jwt`)统一拦截并验证 Token 的有效性(签名校验、有效期检查),签名密钥启动时 fail-close 校验。
|
||||
|
||||
**授权(Authorization)**:基于 RBAC 模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 `sys:user:add`),由 `#[Permission]` 注解在控制器层声明,`AuthMiddleware` 统一校验。管理员(`uid=1`)自动放行。
|
||||
**授权(Authorization)**:基于 RBAC 模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 `sys:user:add`),由 `#[Permission]` 注解在控制器层声明,`Authenticate` 中间件反射校验(三级缓存:Redis → Laravel Cache → 数据库)。超级管理员(`uid=1`)自动放行。
|
||||
|
||||
**防护(Protection)**:系统从多维度构建安全防护体系。参数校验通过 Validate 自动拦截非法输入;敏感密码采用 bcrypt 双重加盐哈希存储;演示模式下 `DemoMiddleware` 统一拦截写操作;跨域配置精确控制可信域名。
|
||||
**防护(Protection)**:系统从多维度构建安全防护体系。参数校验通过 FormRequest 在 HTTP 入口自动拦截非法输入;敏感密码采用 bcrypt 双重加盐哈希存储;演示模式下 `DemoMiddleware` 统一拦截写操作;跨域配置精确控制可信域名;Nginx 仅允许入口 PHP 执行。
|
||||
|
||||
**审计(Audit)**:系统提供完备的操作审计能力。`LogMiddleware` 自动记录所有写操作的请求路径、参数、操作人、IP 地址及执行结果;登录日志独立记录每次登录尝试,便于安全事件追溯。
|
||||
**审计(Audit)**:系统提供完备的操作审计能力。`LogOperation` 中间件自动记录所有写操作的请求路径、参数(排除敏感字段)、操作人、IP 归属地及执行结果;登录日志独立记录每次登录尝试,便于安全事件追溯。
|
||||
|
||||
:::tip 安全设计原则
|
||||
- **纵深防御**:认证、授权、参数校验多层防护,单一防线失效时仍有其他机制兜底
|
||||
- **最小权限**:用户仅能访问已授权接口,角色权限按需分配
|
||||
- **数据不落地**:敏感信息(Token、密码)不在日志中明文记录,密码哈希不可逆。**注意**:当前 LogMiddleware 会记录完整请求参数,敏感字段(如密码)建议在业务层脱敏后再传入
|
||||
- **安全默认**:框架默认配置为安全模式(DEBUG 关闭、错误信息隐藏)
|
||||
- **数据不落地**:敏感信息(Token、密码)不在日志中明文记录(操作日志 `param` 排除 `password`/`token`),密码哈希不可逆
|
||||
- **安全默认**:`JWT_SECRET` 弱密钥拒绝启动(fail-close),生产 `APP_DEBUG=false` 隐藏错误详情
|
||||
:::
|
||||
|
||||
## 安全架构总览
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 安全防御层次 │
|
||||
│ 安全防御层次 │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ 网络层防护 │ │
|
||||
│ │ ┌──────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ CORS │ │ 上传体积限制 │ │ │
|
||||
│ │ │ 跨域控制 │ │ DoS 防御 │ │ │
|
||||
│ │ └──────────┘ └──────────────┘ │ │
|
||||
│ │ ┌──────────┐ ┌──────────────┐ ┌────────────────────────┐ │ │
|
||||
│ │ │ CORS │ │ 上传体积限制 │ │ Nginx 仅入口 index.php │ │ │
|
||||
│ │ │ 跨域控制 │ │ DoS 防御 │ │ 可见性攻击防护 │ │ │
|
||||
│ │ └──────────┘ └──────────────┘ └────────────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ 认证层 │ │
|
||||
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||||
│ │ │ JWT 认证 │ │ 验证码 │ │ 双令牌 │ │ │
|
||||
│ │ │ Bearer │ │ 图形验证 │ │ access+ │ │ │
|
||||
│ │ │ │ │ │ │ refresh │ │ │
|
||||
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
|
||||
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ JWT 认证 │ │ 验证码 │ │ 双令牌 │ │ 密钥 fail- │ │ │
|
||||
│ │ │ Bearer │ │ 图形验证 │ │ access+ │ │ close 启动 │ │ │
|
||||
│ │ │ │ │ + IP限流 │ │ refresh │ │ 校验 │ │ │
|
||||
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
@@ -54,7 +54,7 @@
|
||||
│ │ 数据层 │ │
|
||||
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ 密码加密 │ │ 参数校验 │ │ SQL 注入 │ │ 操作日志 │ │ │
|
||||
│ │ │ bcrypt │ │ Validate │ │ ORM 防护 │ │ 审计追踪 │ │ │
|
||||
│ │ │ 双重加盐 │ │FormRequest│ │ ORM 防护 │ │ 审计追踪 │ │ │
|
||||
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
@@ -65,124 +65,145 @@
|
||||
### 登录流程
|
||||
|
||||
```text
|
||||
1. 前端提交 { username, password, code, key }
|
||||
1. 前端提交 { username, password, key, code }
|
||||
(验证码参数 key/code 与 /api/captcha 响应字段一致)
|
||||
│
|
||||
▼
|
||||
2. CaptchaService::check(code, key) ← 验证码校验
|
||||
2. CaptchaService::verify(key, code) ← 验证码校验(配置 CAPTCHA_LENGTH>0 时启用)
|
||||
│ 失败 → 返回"验证码错误"
|
||||
▼
|
||||
3. User::where('username', $username) ← 查询用户
|
||||
│ 不存在 → 统一提示"用户名或密码错误"
|
||||
3. JwtService::login(username, password) ← 查询用户 + 密码校验
|
||||
│ 用户不存在/密码错误 → 统一提示(不暴露账号是否存在)
|
||||
▼
|
||||
4. password_verify($password . $salt, $hashed) ← bcrypt 校验
|
||||
│ 不匹配 → 统一提示"用户名或密码错误"
|
||||
4. password_verify(password + salt, hash) ← bcrypt 双重加盐校验
|
||||
│ 不匹配 → 统一提示
|
||||
▼
|
||||
5. 检查 user.status == 1 ← 账号状态
|
||||
5. 检查 user.status == 1 ← 账号状态
|
||||
│ 非启用 → 返回"账号已被禁用"
|
||||
▼
|
||||
6. JwtService::createTokenPair($uid) ← 签发双令牌
|
||||
6. 签发双令牌 + 写登录日志(成功/失败均记录)
|
||||
│
|
||||
▼
|
||||
7. 返回 { access_token, refresh_token, expires_in }
|
||||
7. 返回 { access_token, refresh_token, token_type, expires_in, user }
|
||||
```
|
||||
|
||||
### 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} │
|
||||
```text
|
||||
[前端] [后端]
|
||||
│ │
|
||||
│── POST /api/login ──────────→│ (公开接口,中间件组外)
|
||||
│ {username, password, │── 验证码校验(key/code)
|
||||
│ key, code} │── 密码校验(双重加盐 bcrypt)
|
||||
│ │── 签发 access_token + refresh_token
|
||||
│←─ {access_token, ─────────────│
|
||||
│ refresh_token} │
|
||||
│ │
|
||||
│── GET /api/xxx ─────────────→│ (认证接口,auth.jwt 组内)
|
||||
│ Authorization: │── JwtService::parseToken
|
||||
│ Bearer <access_token> │── 校验签名 + 有效期 + type=access
|
||||
│ │── 注入 attributes['userInfo']
|
||||
│←─ {data} ────────────────────│
|
||||
│ │
|
||||
│── access_token 过期 ─────────→│
|
||||
│── POST /api/refresh ─────────→│ (公开接口)
|
||||
│ {refresh_token} │── 校验 refresh 类型 + Redis jti(一次性消费)
|
||||
│←─ {新令牌对} ─────────────────│
|
||||
```
|
||||
|
||||
### Token 设计
|
||||
|
||||
| Token 类型 | 有效期 | 用途 | payload.type |
|
||||
|-----------|--------|------|-------------|
|
||||
| access_token | 2 小时 | 接口认证 | `access` |
|
||||
| refresh_token | 7 天 | 刷新令牌 | `refresh` |
|
||||
| Token 类型 | 默认有效期 | 用途 | 存储 |
|
||||
|-----------|--------|------|------|
|
||||
| access_token | 2 小时(`JWT_ACCESS_TTL=7200`) | 接口认证 | 无状态(JWT) |
|
||||
| refresh_token | 7 天(`JWT_REFRESH_TTL=604800`) | 刷新令牌 | **Redis 有状态**(支持踢下线、改密全端失效、一次性消费) |
|
||||
|
||||
### 安全要点
|
||||
|
||||
1. **密码错误统一提示**:无论用户不存在还是密码错误,统一提示"用户名或密码错误",避免暴露账号是否存在
|
||||
1. **密码错误统一提示**:无论用户不存在还是密码错误,统一提示,避免暴露账号是否存在
|
||||
2. **账号状态校验**:`status != 1` 时禁止登录
|
||||
3. **Token 类型校验**:access_token 和 refresh_token 通过 `type` 字段区分,不可混用
|
||||
4. **密钥安全**:JWT 密钥通过 `.env` 配置,生产环境务必修改为强随机字符串
|
||||
3. **Token 类型校验**:access 与 refresh 通过 `type` 字段区分,不可混用(refresh 令牌不能访问业务接口)
|
||||
4. **密钥 fail-close**:`JWT_SECRET` 为空或不足 32 字符时 `AppServiceProvider` 抛异常**拒绝启动**——弱密钥无法上线
|
||||
5. **验证码限流**:按 IP 限流(`CAPTCHA_RATE_WINDOW` / `CAPTCHA_RATE_LIMIT`),万能码仅 `APP_DEBUG=true` 生效
|
||||
|
||||
## 二、授权机制
|
||||
|
||||
### RBAC 权限模型
|
||||
### RBAC 权限模型(菜单即权限)
|
||||
|
||||
```typescript
|
||||
```text
|
||||
┌─────────┐ ┌────────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐
|
||||
│ User │───►│ UserRole │───►│ Role │───►│ RoleMenu │───►│ Menu │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ id │ │ user_id │ │ id │ │ role_id │ │ id │
|
||||
│ username│ │ role_id │ │ name │ │ menu_id │ │ perm │
|
||||
└─────────┘ └────────────┘ │data_scope│ └────────────┘ │ type │
|
||||
│ username│ │ role_id │ │ name │ │ menu_id │ │permission│
|
||||
└─────────┘ └────────────┘ │ code │ └────────────┘ │ type │
|
||||
└─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### 权限校验流程
|
||||
- 菜单表的 `permission` 字段即权限编码(`sys:{模块}:{操作}`)
|
||||
- `type=1`(菜单)且 `permission` 非空的节点参与接口鉴权
|
||||
- 角色-菜单变更时 `RoleMenuObserver` / `MenuObserver` 自动清除权限缓存
|
||||
|
||||
### 权限校验流程(Authenticate::checkPermission)
|
||||
|
||||
```php
|
||||
// AuthMiddleware::checkPermission()
|
||||
// app/Http/Middleware/Authenticate.php(节选)
|
||||
|
||||
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;
|
||||
// 从 Redis 获取权限(一级缓存)
|
||||
try {
|
||||
$cached = Redis::get("perms:{$uid}");
|
||||
if ($cached) {
|
||||
$perms = json_decode($cached, true) ?? [];
|
||||
// 拥有通配权限或包含目标编码即通过
|
||||
return in_array('*:*:*', $perms) || in_array($permissionCode, $perms);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Redis 不可用时继续降级
|
||||
}
|
||||
|
||||
// 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');
|
||||
// 二级缓存:Laravel Cache(5 分钟)
|
||||
$cacheKey = "perms_fallback:{$uid}";
|
||||
$permissions = Cache::remember($cacheKey, 300, function () use ($uid) {
|
||||
return $this->loadPermissionsFromDb($uid);
|
||||
});
|
||||
|
||||
// 4. 判断是否包含目标权限
|
||||
return in_array($permissionCode, $permissions);
|
||||
return in_array('*:*:*', $permissions) || in_array($permissionCode, $permissions);
|
||||
}
|
||||
|
||||
// 数据库加载:user_role → role_menu join menu(未删除、type=1、permission 非空)
|
||||
protected function loadPermissionsFromDb(int $uid): array
|
||||
{
|
||||
$roleIds = UserRole::where('user_id', $uid)->pluck('role_id')->toArray();
|
||||
if (empty($roleIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return RoleMenu::join('menu', 'role_menu.menu_id', '=', 'menu.id')
|
||||
->whereIn('role_menu.role_id', $roleIds)
|
||||
->where('menu.is_delete', 0)
|
||||
->where('menu.type', 1)
|
||||
->where('menu.permission', '!=', '')
|
||||
->pluck('menu.permission')
|
||||
->toArray();
|
||||
}
|
||||
```
|
||||
|
||||
### 数据权限
|
||||
|
||||
| data_scope | 范围 | 实现 |
|
||||
`HasDataScope` 支持三种数据范围(当前 `laravel_role` 表不含 `data_scope` 字段,实际一律按"全部数据"执行):
|
||||
|
||||
| data_scope | 范围 | 实现(HasDataScope) |
|
||||
|-----------|------|------|
|
||||
| 1 | 全部数据 | 不过滤 |
|
||||
| 2 | 本部门数据 | WHERE dept_id = 用户部门ID |
|
||||
| 3 | 仅本人数据 | WHERE create_user = 用户名 |
|
||||
| 2 | 本部门数据 | `WHERE dept_id = 用户部门ID` |
|
||||
| 3 | 仅本人数据(默认) | `WHERE create_user = 用户名` |
|
||||
|
||||
## 三、防护机制
|
||||
|
||||
@@ -190,77 +211,64 @@ protected function checkPermission(int $uid, string $permissionCode): bool
|
||||
|
||||
```text
|
||||
密码存储流程:
|
||||
原始密码 + 外部 salt(10位随机字符串)
|
||||
原始密码 + 外部 salt(随机字符串,存 user.salt 字段)
|
||||
│
|
||||
▼ password_hash($combined, PASSWORD_BCRYPT, ['cost' => 12])
|
||||
▼ password_hash($password . $salt, PASSWORD_BCRYPT, ['cost' => 12])
|
||||
│
|
||||
▼ bcrypt 内部自动生成 salt 并嵌入哈希
|
||||
│
|
||||
▼ 存储:password 字段(60字符)+ salt 字段(10字符)
|
||||
▼ 存储:password 字段(60 字符哈希)+ salt 字段(独立存储)
|
||||
|
||||
密码校验流程:
|
||||
用户输入密码 + 数据库 salt
|
||||
│
|
||||
▼ password_verify($input . $salt, $hashed)
|
||||
│
|
||||
▼ true/false
|
||||
▼ true / false
|
||||
```
|
||||
|
||||
```php
|
||||
// 加密
|
||||
$password = '123456';
|
||||
$salt = 'xK7pMn3qRt'; // 外部盐(10位随机)
|
||||
$combined = $password . $salt; // 拼接
|
||||
$hashed = password_hash($combined, PASSWORD_BCRYPT, ['cost' => 12]);
|
||||
// 结果:$2y$12$...(60字符)
|
||||
- 成本因子固定为 12(`PasswordService::BCRYPT_ROUNDS`,对应 `.env` 的 `BCRYPT_ROUNDS=12`)
|
||||
- **双重加盐**:外部 salt 存独立字段,数据库泄露后仍需同时获取 salt 才能暴力破解;bcrypt 内部 salt 进一步增加彩虹表攻击难度
|
||||
|
||||
// 校验
|
||||
$inputCombined = $userInput . $dbSalt;
|
||||
$isValid = password_verify($inputCombined, $dbHashed);
|
||||
```
|
||||
### 参数校验(FormRequest 入口拦截)
|
||||
|
||||
:::tip 为什么双重加盐?
|
||||
外部 salt 存储在独立字段,即使数据库泄露,攻击者也需要同时获取 salt 才能进行暴力破解。bcrypt 内部 salt 进一步增加彩虹表攻击难度。
|
||||
:::
|
||||
|
||||
### 参数校验(自动拦截)
|
||||
|
||||
通过 Logic 层 `$validateClass` 配置,add/update 时自动校验参数格式:
|
||||
通过控制器方法类型注入 FormRequest,进入业务逻辑前完成格式校验:
|
||||
|
||||
```php
|
||||
class UserValidate extends Validate
|
||||
class UserRequest extends BaseRequest
|
||||
{
|
||||
protected $rule = [
|
||||
'username' => 'require|unique:user',
|
||||
'realname' => 'require',
|
||||
'mobile' => 'mobile',
|
||||
'email' => 'email',
|
||||
];
|
||||
protected $scene = [
|
||||
'add' => ['username', 'realname', 'mobile', 'email'],
|
||||
'update' => ['realname', 'mobile', 'email'],
|
||||
];
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => 'required|string|min:3|max:50',
|
||||
'realname' => 'required|string|max:50',
|
||||
'mobile' => 'nullable|max:20',
|
||||
'email' => 'nullable|email',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
校验失败直接返回 422,不进入业务逻辑层。
|
||||
校验失败由 `failedValidation()` 直接返回 `code=1` + 字段错误明细,**不进入控制器方法**;唯一性等业务规则由 Service 层兜底(防止绕过格式校验的组合攻击)。
|
||||
|
||||
### SQL 注入防护
|
||||
|
||||
ThinkPHP ORM 使用参数绑定,自动防止 SQL 注入:
|
||||
Eloquent 查询构造器使用参数绑定,自动防止 SQL 注入:
|
||||
|
||||
```php
|
||||
// ✅ ORM 查询(安全,自动参数绑定)
|
||||
User::where('username', $username)->find();
|
||||
// 生成:SELECT * FROM think_user WHERE username = ? [$username]
|
||||
User::where('username', $username)->first();
|
||||
// 生成:SELECT * FROM laravel_user WHERE username = ? [$username]
|
||||
|
||||
// ✅ 原生查询(手动绑定参数)
|
||||
Db::query("SELECT * FROM think_user WHERE username = ?", [$username]);
|
||||
DB::select("SELECT * FROM laravel_user WHERE username = ?", [$username]);
|
||||
|
||||
// ❌ 危险!字符串拼接(禁止)
|
||||
Db::query("SELECT * FROM think_user WHERE username = '$username'");
|
||||
DB::select("SELECT * FROM laravel_user WHERE username = '$username'");
|
||||
```
|
||||
|
||||
查询排序也受白名单保护:前端 `orderField` 必须命中 `pageOrderBy` 与 `orderFieldAllow` 的并集,否则忽略(防排序注入)。
|
||||
|
||||
:::warning 安全红线
|
||||
永远不要将用户输入直接拼接到 SQL 字符串中。即使使用原生 SQL,也必须使用参数绑定(`?` 占位符)。
|
||||
:::
|
||||
@@ -270,21 +278,23 @@ Db::query("SELECT * FROM think_user WHERE username = '$username'");
|
||||
```text
|
||||
上传文件安全检查链:
|
||||
│
|
||||
▼ 1. 文件大小限制(config/file.php → max_size,默认 10MB)
|
||||
▼ 1. 文件大小限制(config/file.php → FILE_MAX_SIZE,默认 10MB)
|
||||
│
|
||||
▼ 2. 文件扩展名白名单(config/file.php → allow_ext)
|
||||
▼ 2. 文件扩展名白名单(FILE_IMAGE_EXT / FILE_EXT)
|
||||
│ (svg 已移除:内嵌 JS 导致 XSS 风险)
|
||||
│
|
||||
▼ 3. MIME 类型检查(fileinfo 扩展)
|
||||
▼ 3. MIME 类型真实检测(fileinfo 扩展,_import 等场景强制校验)
|
||||
│
|
||||
▼ 4. 生成随机文件名(防止路径遍历攻击)
|
||||
▼ 4. 存储路径由服务端生成(防止路径遍历)
|
||||
│
|
||||
▼ 5. 存储到 Web 根目录之外(或 Nginx 禁止执行 PHP)
|
||||
▼ 5. Nginx 仅允许入口 index.php 执行 —— 上传目录中的 .php 一律 404
|
||||
```
|
||||
|
||||
```ini
|
||||
# Nginx 配置:上传目录禁止执行 PHP
|
||||
location ~* /uploads/.*\.php$ {
|
||||
deny all;
|
||||
```nginx
|
||||
# docker/nginx.conf:只有入口可执行,其余 .php 一律拒绝
|
||||
location = /index.php { /* FastCGI 转发 */ }
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -292,98 +302,99 @@ location ~* /uploads/.*\.php$ {
|
||||
|
||||
```text
|
||||
防护措施:
|
||||
1. 前端:用户输入转义显示(Vue 默认转义 {{ }} 插值)
|
||||
2. 后端:富文本内容通过 save_content() 处理,过滤危险标签
|
||||
3. 响应头:Content-Type: application/json(非 text/html)
|
||||
1. 前端:Vue 模板插值默认转义({{ }}),避免注入
|
||||
2. 后端:响应强制 Content-Type: application/json(ForceJsonResponse 中间件),
|
||||
浏览器不会按 HTML 渲染
|
||||
3. 富文本:字段独立处理链(contentFields),内容过滤与占位符机制
|
||||
4. 上传白名单移除 svg(内嵌脚本风险)
|
||||
```
|
||||
|
||||
### 演示模式保护
|
||||
|
||||
```php
|
||||
// DemoMiddleware 拦截写操作
|
||||
// .env: [APP] DEMO = true
|
||||
// .env: DEMO_MODE=true(读取 config('app.demo_mode'))
|
||||
|
||||
// 受保护:POST/PUT/DELETE 请求 → 返回 403
|
||||
// 放行:GET 请求 + 标注 #[DemoAllow] 的方法
|
||||
// 受保护:POST/PUT/DELETE/PATCH 请求 → 返回 403
|
||||
// 放行:GET 请求 + 标注 #[DemoAllow] 注解的方法
|
||||
```
|
||||
|
||||
```php
|
||||
// 标注 #[DemoAllow] 的方法在演示模式下仍可执行
|
||||
#[DemoAllow]
|
||||
public function login(): Json { ... }
|
||||
public function login(LoginRequest $request): JsonResponse { ... }
|
||||
```
|
||||
|
||||
### CORS 跨域控制
|
||||
|
||||
```ini
|
||||
; .env
|
||||
; 生产环境:限制具体域名
|
||||
[CORS]
|
||||
ALLOW_ORIGIN = https://admin.example.com
|
||||
# .env
|
||||
# 开发环境:允许所有(仅限开发)
|
||||
CORS_ALLOW_ORIGIN=*
|
||||
|
||||
; 开发环境:允许所有(仅限开发)
|
||||
[CORS]
|
||||
ALLOW_ORIGIN = *
|
||||
# 生产环境:限制具体域名(多个来源用逗号分隔)
|
||||
CORS_ALLOW_ORIGIN=https://admin.example.com
|
||||
```
|
||||
|
||||
```php
|
||||
// CorsMiddleware 处理逻辑:
|
||||
// 1. OPTIONS 预检请求 → 直接返回 204 + CORS 头
|
||||
// 2. 普通请求 → 校验 Origin 是否在白名单
|
||||
// 3. 设置 Access-Control-Allow-Origin / Allow-Methods / Allow-Headers
|
||||
// Cors 中间件处理逻辑(见 4.2 请求处理链路):
|
||||
// 1. OPTIONS 预检请求 → 直接返回 204 + CORS 头(不进业务逻辑)
|
||||
// 2. 凭证模式下不能用 * → 回显具体 Origin + Vary: Origin(防 CDN 缓存串扰)
|
||||
// 3. 仅 Origin 被允许时才设置其余 CORS 头
|
||||
```
|
||||
|
||||
## 四、审计机制
|
||||
|
||||
### 操作日志(think_operation_log)
|
||||
### 操作日志(laravel_operation_log)
|
||||
|
||||
`LogMiddleware` 基于 `#[Log]` 注解自动记录所有写操作:
|
||||
`LogOperation` 中间件基于 `#[OperationLog]` 注解自动记录:
|
||||
|
||||
```php
|
||||
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
|
||||
public function add(): Json { ... }
|
||||
#[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')]
|
||||
public function add(UserRequest $request): JsonResponse { ... }
|
||||
```
|
||||
|
||||
**记录字段:**
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `title` | 操作标题(来自注解) | 用户管理-新增记录 |
|
||||
| `type` | 操作类型(常量) | 1=新增 2=修改 3=删除 4=查询 5=导入 6=导出 |
|
||||
| `title` | 操作标题(来自注解,无注解回退请求路径) | 用户管理-新增 |
|
||||
| `type` | 操作类型(30 种常量,无注解按 HTTP 方法推断) | 1=新增 2=修改 3=删除 4=查询 5=导入 6=导出 |
|
||||
| `method` | HTTP 方法 | POST |
|
||||
| `request_method` | 控制器方法名 | add |
|
||||
| `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 |
|
||||
| `param` | 请求参数(JSON,**排除 password/token**,截断 2000 字) | {"username":"admin"} |
|
||||
| `result` | 响应内容(截断 2000 字) | {"code":0,...} |
|
||||
| `ip` / `location` | 客户端 IP / 归属地(ip2region) | 192.168.1.100 / 江苏省南京市 |
|
||||
| `os` / `browser` | 操作系统 / 浏览器 | Windows 11 / Chrome 128 |
|
||||
| `create_user` | 操作人(userInfo.username) | admin |
|
||||
| `status` | 执行状态(code=0/200 → 0,其余 → 1) | 0=成功 1=失败 |
|
||||
| `error` | 错误信息(失败时取 msg) | 名称已存在 |
|
||||
| `consume_time` | 执行耗时(ms) | 125 |
|
||||
|
||||
**记录规则:**
|
||||
- 写操作(POST/PUT/DELETE)始终记录
|
||||
- GET 查询操作仅在 `#[Log]` 指定 `TYPE_QUERY` 时记录
|
||||
- 排除列表:`config/middleware.php` → `log_except` 中的路径不记录
|
||||
|
||||
### 登录日志(think_login_log)
|
||||
- 写操作(POST/PUT/DELETE/PATCH)始终记录
|
||||
- GET 仅在注解类型为 `EXPORT / DOWNLOAD / IMPORT` 时记录
|
||||
- `handle` 阶段采集、`terminate` 阶段落库(响应后执行,不阻塞业务;写入失败静默)
|
||||
|
||||
### 登录日志(laravel_login_log)
|
||||
|
||||
```php
|
||||
// LoginLogic 中独立记录
|
||||
$requestInfo = RequestInfoService::create(request());
|
||||
|
||||
LoginLog::create([
|
||||
// AuthController::writeLoginLog(节选)—— 成功与失败均记录
|
||||
$data = [
|
||||
'username' => $username,
|
||||
'type' => 1, // 1=登录 2=登出
|
||||
'status' => $success ? 0 : 1, // 0=成功 1=失败
|
||||
'message' => $msg,
|
||||
'ip' => $requestInfo->getIp(),
|
||||
'log_type' => 1, // 1=登录 2=登出
|
||||
'source' => 0, // 来源端:0=后台
|
||||
'method' => $request->method(),
|
||||
'request_method' => $entry, // login / refresh 等入口
|
||||
'url' => $request->fullUrl(),
|
||||
'param' => json_encode(['username' => $username], ...), // 只记录用户名
|
||||
'result' => json_encode(['code' => $success ? 0 : 1, 'msg' => $message], ...),
|
||||
'ip' => $requestInfo->getIp(),
|
||||
'location' => $requestInfo->getIpLocation(),
|
||||
'os' => $requestInfo->getOs(),
|
||||
'browser' => $requestInfo->getBrowser(),
|
||||
]);
|
||||
// + os / browser / 耗时
|
||||
];
|
||||
```
|
||||
|
||||
**记录字段:**
|
||||
@@ -391,22 +402,19 @@ LoginLog::create([
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `username` | 登录用户名(无论成功失败都记录) |
|
||||
| `type` | 1=登录 2=登出 |
|
||||
| `status` | 0=成功 1=失败 |
|
||||
| `message` | 结果消息(如"登录成功"、"密码错误") |
|
||||
| `ip` | 客户端 IP |
|
||||
| `location` | IP 归属地 |
|
||||
| `os` | 操作系统 |
|
||||
| `browser` | 浏览器 |
|
||||
| `log_type` | 1=登录 2=登出 |
|
||||
| `status` / `result` | 0=成功 1=失败 + 结果消息 |
|
||||
| `ip` / `location` | 客户端 IP / 归属地 |
|
||||
| `os` / `browser` | 操作系统 / 浏览器 |
|
||||
|
||||
### 日志安全要点
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| 密码不记录 | 登录日志不记录密码字段 |
|
||||
| 参数可脱敏 | 敏感字段建议在 Controller 层处理后再传入 Logic |
|
||||
| 密码与令牌不记录 | 操作日志 `param` 排除 `password`、`token`;登录日志只记录用户名 |
|
||||
| 参数截断 | `param` / `result` 截断 2000 字,防止超长内容撑爆字段 |
|
||||
| 独立存储 | 操作日志和登录日志分别存储在不同表 |
|
||||
| 不可篡改 | 日志仅 INSERT,无 UPDATE/DELETE 接口 |
|
||||
| 失败不阻断 | 日志写入异常静默处理,不影响主业务 |
|
||||
|
||||
## 五、安全配置清单
|
||||
|
||||
@@ -414,38 +422,36 @@ LoginLog::create([
|
||||
|
||||
| 配置项 | 开发环境 | 生产环境 | 说明 |
|
||||
|--------|---------|---------|------|
|
||||
| `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` | 驼峰转换开关 |
|
||||
| `APP_DEBUG` | `true` | **`false`** | 关闭调试模式,异常不返回真实消息 |
|
||||
| `JWT_SECRET` | 强随机 | **强随机(≥32 字符)** | 不足 32 字符**拒绝启动**(fail-close) |
|
||||
| `CORS_ALLOW_ORIGIN` | `*` | **指定域名** | 如 `https://admin.example.com` |
|
||||
| `DEMO_MODE` | `false` | `false` | 演示环境按需开启 |
|
||||
| `FILE_MAX_SIZE` | 10 | 合理限制 | 防止大文件 DoS |
|
||||
| `CAPTCHA_BYPASS_CODE` | 可设 | **必须留空** | 万能验证码仅 debug 生效,生产留空 |
|
||||
| `TRUSTED_PROXIES` | 空 | CDN/负载均衡地址 | 防止伪造代理头获取错误 IP |
|
||||
| `BCRYPT_ROUNDS` | 12 | 12+ | 密码哈希成本因子 |
|
||||
| `API_CAMEL_SNAKE_CONVERT` | `true` | `true` | 驼峰转换开关 |
|
||||
|
||||
### Nginx 配置
|
||||
|
||||
```nginx
|
||||
# 1. 启用 HTTPS
|
||||
# 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;
|
||||
}
|
||||
# 2. 仅入口 PHP 可执行(docker/nginx.conf 已内置)
|
||||
location = /index.php { /* FastCGI 转发 */ }
|
||||
location ~ \.php$ { return 404; }
|
||||
|
||||
# 3. 禁止访问隐藏文件
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
# 3. 禁止访问隐藏文件(.env / .git 等,docker/nginx.conf 已内置)
|
||||
location ~ /\. { deny all; }
|
||||
|
||||
# 4. 安全响应头
|
||||
# 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;
|
||||
```
|
||||
|
||||
@@ -455,18 +461,18 @@ add_header Referrer-Policy strict-origin-when-cross-origin;
|
||||
上线前逐项检查:
|
||||
|
||||
□ .env 中 APP_DEBUG = false
|
||||
□ .env 中 SHOW_ERROR_MSG = false
|
||||
□ .env 中 JWT.SECRET 已更换为强随机密钥
|
||||
□ .env 中 CORS.ALLOW_ORIGIN 指定具体域名
|
||||
□ .env 中 JWT_SECRET 已更换为 ≥32 字符强随机密钥(否则应用拒绝启动)
|
||||
□ .env 中 CORS_ALLOW_ORIGIN 指定具体域名
|
||||
□ .env 中 CAPTCHA_BYPASS_CODE 留空
|
||||
□ Nginx 已配置 HTTPS
|
||||
□ Nginx 上传目录已禁止执行 PHP
|
||||
□ 数据库密码为强密码
|
||||
□ Redis 已设置密码(如使用)
|
||||
□ 默认管理员密码已修改
|
||||
□ Nginx 确认仅入口 index.php 可执行、隐藏文件已拒绝
|
||||
□ 数据库密码为强密码,Redis 已设置 requirepass 并同步 REDIS_PASSWORD
|
||||
□ 默认管理员密码已修改(admin / 123456)
|
||||
□ 演示模式已关闭(除非需要)
|
||||
□ 后端 /api/* 路由确认在认证中间件组内(公开接口仅 login/refresh/captcha/oauth2)
|
||||
□ 日志中不包含密码等敏感信息
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
系统通过认证、授权、防护、审计四层安全架构,实现了从网络层到数据层的纵深防御。JWT 双令牌认证保证无状态安全,RBAC 权限控制细化到按钮级,bcrypt 双重加盐保护密码安全,操作日志实现全链路审计。各层安全机制相互独立又协同配合,确保系统在企业级应用场景下的安全可靠性。
|
||||
系统通过认证、授权、防护、审计四层安全架构,实现了从网络层到数据层的纵深防御。JWT 双令牌 + Redis 有状态保证认证安全且支持会话管控,RBAC 权限控制细化到按钮级(三级缓存保证性能),bcrypt 双重加盐保护密码安全,操作日志与登录日志实现全链路审计。各层安全机制相互独立又协同配合,确保系统在企业级应用场景下的安全可靠性。
|
||||
|
||||
Reference in New Issue
Block a user