Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/zh/architecture/extensibility.md
T
2026-10-08 17:50:29 +08:00

559 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 4.11 扩展性设计
:::tip 概述
系统通过六种机制保证良好的扩展性:属性配置、生命周期钩子、FormRequest 参数校验、中间件链、注解系统、服务层复用。新增模块无需修改框架代码,只需继承基类 + 配置属性 + 按需重写钩子。
:::
## 扩展机制总览
| 机制 | 位置 | 作用 | 扩展方式 |
|------|------|------|----------|
| 属性配置 | Service 子类 | 声明式行为定义 | 设置属性值 |
| 生命周期钩子 | Service 子类 | 注入定制逻辑 | 重写钩子方法 |
| 参数格式校验 | FormRequest | HTTP 入口拦截非法输入 | 创建 Request + 类型注入 |
| 中间件链 | `Http/Middleware/` | 请求拦截处理 | 创建中间件 + 注册别名 |
| 注解系统 | `Attributes/` | 声明式元数据 | 创建注解 + 中间件反射读取 |
| 服务层复用 | `Services/` | 跨模块通用能力 | 创建 Service 类 |
## 1. 属性配置模式
Service 层通过属性配置声明行为,新增模块只需设置属性,无需编写 CRUD 代码。
### 完整属性清单(摘自 `app/Services/ArticleService.php` 实际代码)
```php
class ArticleService extends BaseTenantService
{
use HasCrudOperations, HasFileProcessing, HasQueryScopes, HasSerializeMaps;
// 数据权限能力按需 use HasDataScope
/**
* 绑定的模型类
*/
protected string $modelClass = Article::class;
/**
* 单文件字段:保存时迁移临时文件,读取时补全域名
*
* @var array<int, string>
*/
protected array $fileFields = ['cover'];
/**
* 多文件字段:保存时逐个迁移,读取时逐个补全域名
*
* @var array<int, string>
*/
protected array $multiFileFields = ['images'];
/**
* 文件保存子目录
*/
protected string $fileSaveDir = 'article';
/**
* 分页时模糊匹配的字段
*
* @var array<int, string>
*/
protected array $pageLikeFields = ['title', 'author'];
/**
* 分页时精确匹配的字段
*
* @var array<int, string>
*/
protected array $pageEqFields = ['category_id', 'status'];
/**
* 默认排序规则(多字段)
*
* @var array<int, array<string, string>>
*/
protected array $pageOrderBy = [
['field' => 'tenant_id', 'type' => 'asc'],
['field' => 'sort', 'type' => 'asc'],
];
/**
* 字典翻译配置:[字段 => 字典编码]
*
* @var array<string, string>
*/
protected array $serializeMaps = [
'status' => 'article_status',
];
/**
* 数据权限的用户字段(data_scope=3 仅本人时按此字段过滤)
*/
protected string $dataScopeUserField = 'create_user';
}
```
其它可声明属性(`uniqueFields`、`contentFields`、`dataScopeDeptField`、`tenantScopeField`、`treeParentField` / `treeLikeField` 等)均在 `BaseService` 中声明,完整清单见 [4.6 核心基类体系](/zh/architecture/base-classes.html)。
### 属性驱动的自动行为
| 属性 | 触发的自动行为 |
|------|---------------|
| `fileFields` | add/update 时迁移临时文件,查询时补全域名 |
| `multiFileFields` | 逗号分隔多文件逐个迁移/补全 |
| `contentFields` | add/update 时处理富文本媒体文件,查询时替换占位符 |
| `pageLikeFields` | pageList 时自动构建 LIKE 查询 |
| `pageEqFields` | pageList 时自动构建精确查询 |
| `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(事务内) | ✅ 抛异常回滚 |
| `beforeUpdate` | 修改前 | `$id, $data` | 处理后的 `$data` | ✅ 抛异常 |
| `afterUpdate` | 修改后 | `$id, $data` | void(事务内) | ✅ 抛异常回滚 |
| `beforeDelete` | 删除前 | `$id` | void | ✅ 抛异常 |
| `afterDelete` | 删除后 | `$id` | void(事务内) | ✅ 抛异常回滚 |
| `beforeBatchDelete` | 批量删除前 | `$ids` | void | ✅ 抛异常 |
| `afterBatchDelete` | 批量删除后 | `$ids` | void(事务内) | ✅ 抛异常回滚 |
| `afterDetail` | 详情查询后 | `$id, &$data` | void(引用传递) | ❌ |
| `afterPageList` | 列表查询后 | `&$records, $params` | void(引用传递,分页与全量列表均触发) | ❌ |
### 典型用法(摘自项目实际代码)
```php
// app/Services/DeptService.php —— 删除前校验(抛 BizException 拦截删除并回滚)
protected function beforeDelete(int $id): void
{
$hasChildren = Dept::where('parent_id', $id)->exists();
if ($hasChildren) {
throw new BizException('存在子部门,不可删除');
}
$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;
}
// 批量获取用户角色(仅查所需字段)
$userRoles = [];
$roleData = UserRole::whereIn('user_id', $userIds)->select('user_id', 'role_id')->get();
foreach ($roleData as $rd) {
$userRoles[$rd->user_id][] = $rd->role_id;
}
$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']);
}
}
```
### 执行顺序
**add 流程:**
`beforeAdd` → `checkUnique` → `DB::transaction { processTenantId → processFile → processMultiFile → processContent → Model::create(审计填充)→ afterAdd }`
**update 流程:**
`beforeUpdate` → `checkUnique(excludeId)` → `DB::transaction { 同上处理 → update(审计刷新)→ afterUpdate }`
**delete 流程:**
`beforeDelete` → `DB::transaction { is_delete = 1 → afterDelete }`
**pageList/allList 流程:**
`buildQuery` → `排序` → `分页/全量` → `processAfterRead(文件/字典翻译/字段过滤)` → `afterPageList`
(参数格式校验在入口 FormRequest 完成,不在此流程内。)
## 3. 参数格式校验(FormRequest)
通过控制器方法类型注入 FormRequest,进入方法前自动校验,无需手动调用。
### 扩展方式(摘自 `app/Http/Requests/ExampleRequest.php` 实际代码)
```php
// 1. 创建请求类
// app/Http/Requests/ExampleRequest.php
class ExampleRequest extends BaseRequest
{
/**
* 校验规则
*
* @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. 控制器类型注入(校验通过才进入方法)
public function add(ExampleRequest $request): JsonResponse
{
return $this->_add($this->exampleService, $request->validated());
}
```
详见 [3.4 FormRequest 验证器](/zh/guide/validator.html)。
## 4. 中间件机制
通过中间件链式处理请求,可灵活添加新的拦截逻辑。
### 现有中间件
```text
请求进入
│
▼
ProxyMiddleware(API 组前置) ← 受信代理真实 IP 还原
│
▼
Cors(API 组前置) ← OPTIONS 预检 + 跨域响应头
│
▼
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
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class DemoMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// 非演示模式直接放行
if (! config('app.demo_mode', false)) {
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);
}
}
```
注册方式(`bootstrap/app.php` 中的三种挂载点):
```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,
]);
// 方式二: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. 注解系统
通过 PHP 8 注解声明元数据,中间件通过反射读取,业务代码零侵入。
### 现有注解
| 注解 | 目标 | 说明 | 读取方 |
|------|------|------|--------|
| `#[OperationLog]` | 方法 | 操作日志(标题、类型、描述模板) | `LogOperation` 中间件(ReflectionMethod) |
| `#[Permission]` | 方法 | 权限校验(权限编码、名称) | `Authenticate` 中间件(ReflectionMethod) |
| `#[DemoAllow]` | 方法 | 演示环境放行标记 | `DemoMiddleware`(ReflectionMethod) |
### 使用示例
```php
// 操作日志(TYPE 常量 30 种,描述支持 {param} 占位符)
#[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')]
// 权限校验
#[Permission('sys:user:add', '添加用户')]
// 演示环境放行(允许在演示环境执行写操作)
#[DemoAllow]
// 组合使用
#[OperationLog('文章管理-删除', OperationLog::TYPE_DELETE, '删除文章ID:{id}')]
#[Permission('sys:article:delete', '删除文章')]
public function delete(int $id): JsonResponse
{
return $this->_delete($this->service, $id);
}
```
### 注解结构与读取方式(摘自项目实际代码)
```php
// 1. 定义注解类(app/Attributes/Permission.php 实际代码)
#[Attribute(Attribute::TARGET_METHOD)]
class Permission
{
/**
* @param string $code 权限编码(鉴权唯一依据,如 sys:user:add)
* @param string $name 权限名称(用于展示,如"添加用户")
* @param string $description 权限描述(可选,补充说明)
*/
public function __construct(
public string $code,
public string $name = '',
public string $description = '',
) {}
}
// 2. 在控制器方法上使用
#[Permission('sys:user:add', '添加用户')]
public function add(UserRequest $request): JsonResponse { ... }
// 3. 在中间件中读取(DemoMiddleware 实际代码同款反射方式)
$reflection = new \ReflectionMethod($controllerClass, $method);
$attrs = $reflection->getAttributes(DemoAllow::class);
if (! empty($attrs)) {
return $next($request); // 命中注解即放行
}
```
新增自定义注解时,按同样结构在 `app/Attributes/` 下定义类,再在中间件中用 `ReflectionMethod::getAttributes()` 读取即可。
:::tip 注解读取性能
反射读取开销为微秒级,`Authenticate` / `LogOperation` 均直接使用 `ReflectionMethod` 读取(无缓存),避免开发阶段改注解后需等缓存过期。
:::
## 6. 服务层复用
通用能力封装为 Service,跨模块复用,无状态、无副作用。
### 现有服务清单
| 服务 | 文件(`app/Services/`) | 能力 |
|------|------|------|
| `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
// 字典服务(静态调用,内部两级缓存)
DictCacheService::getText('gender', 1); // → '男'
DictCacheService::getValue('user_status', '启用'); // → 1
// 密码服务
$encrypted = PasswordService::encrypt('123456'); // → ['password'=>'...', 'salt'=>'...']
// Excel 服务
ExcelService::importWithMap($filePath, $headerMap);
ExcelService::export($list, $headers, '导出.xlsx');
// 请求信息(IP 经 ProxyMiddleware 还原真实值)
$requestInfo = RequestInfoService::create(request());
$location = $requestInfo->getIpLocation(); // 基于 zoujingli/ip2region
```
### 创建自定义服务
服务结构参考(摘自 `app/Services/ParamService.php` 实际代码——无状态静态方法 + 请求级缓存的典型写法):
```php
namespace App\Services;
use App\Models\Param;
/**
* 系统参数服务
*
* 从 param 表获取系统参数配置。
* 数据量小,不使用持久缓存,仅保留请求级缓存避免同请求内重复查库。
*/
class ParamService
{
/**
* 请求级缓存:[参数编码 => 参数值]
*
* @var array<string, mixed>
*/
protected static array $cache = [];
/**
* 获取参数值
*
* 读取顺序:请求级缓存 → 查库。
*/
public static function get(string $code): mixed
{
if (array_key_exists($code, static::$cache)) {
return static::$cache[$code];
}
$value = /* 查库逻辑 */ null;
static::$cache[$code] = $value;
return $value;
}
/**
* 清除缓存(参数变更后调用)
*/
public static function clearCache(string $code = ''): void
{
if ($code !== '') {
unset(static::$cache[$code]);
return;
}
static::$cache = [];
}
}
```
新增服务时按此结构放在 `app/Services/` 下:无状态、单一职责、静态方法或构造注入均可;跨模块复用的能力才提升为 Service,业务专属逻辑留在业务 Service 中。
## 扩展性设计原则
### 开闭原则(OCP)
| 扩展场景 | 做法 | 不应该做 |
|----------|------|----------|
| 新增 CRUD 模块 | 创建 Request/Service/Model/Controller | 修改基类 |
| 新增查询条件 | 设置 `pageLikeFields` / `pageEqFields` | 重写 `buildQuery` |
| 新增文件处理 | `use HasFileProcessing` + 设置 `fileFields` | 手动搬运临时文件 |
| 新增校验规则 | 创建 FormRequest + 类型注入 | 在 Controller 手动校验 |
| 新增请求拦截 | 创建中间件 + 注册 | 修改现有中间件 |
| 新增操作日志 | 添加 `#[OperationLog]` 注解 | 在方法内手动记录日志 |
| 新增权限节点 | 添加 `#[Permission]` 注解 | 在方法内手动校验权限 |
| 新增通用能力 | 创建 Service | 在业务 Service 里写静态方法 |
### 最小改动原则
新增一个完整的业务模块,只需要:
1. 建表(含公共字段)
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) 一键生成以上全部。)