Files
easygoadmin-wiki/EasyGoAdmin_Gin_AntdVue/zh/architecture/extensibility.md
T
2026-10-20 21:19:44 +08:00

613 lines
19 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 概述
系统通过六种机制保证良好的扩展性:属性配置、生命周期钩子、自动参数校验、中间件链、注解系统、服务层复用。新增模块无需修改框架代码,只需继承基类 + 配置属性 + 按需重写钩子。
:::
## 扩展机制总览
| 机制 | 位置 | 作用 | 扩展方式 |
|------|------|------|----------|
| 属性配置 | Logic 子类 | 声明式行为定义 | 设置属性值 |
| 生命周期钩子 | Logic 子类 | 注入定制逻辑 | 重写钩子方法 |
| 自动参数校验 | Validate + Logic | 参数格式校验 | 创建 Validate + 配置 `$validateClass` |
| 中间件链 | middleware/ | 请求拦截处理 | 创建中间件 + 注册 |
| 注解系统 | attribute/ | 声明式元数据 | 创建注解 + 中间件读取 |
| 服务层复用 | service/ | 跨模块通用能力 | 创建 Service 类 |
## 1. 属性配置模式
Logic 层通过属性配置声明行为,新增模块只需设置属性,无需编写 CRUD 代码。
### 完整属性清单
```php
class NewModuleLogic extends BaseLogic
{
/**
* 关联的模型类
*
* 所有增删改查操作都会基于此模型执行,子类必须指定
*
* @var string
*/
protected string $modelClass = NewModule::class;
/**
* 参数验证器类
*
* 配置后,新增 / 修改等操作会自动调用该验证器进行参数校验,
* 校验不通过会直接抛出异常,无需在业务代码中手动验证
*
* @var string
*/
protected string $validateClass = NewModuleValidate::class;
/**
* 需要自动处理的文件上传字段
*
* 提交数据中若包含这些字段,框架会自动完成文件上传、存储与路径回填
*
* @var array
*/
protected array $fileFields = ['cover', 'avatar'];
/**
* 文件保存的子目录
*
* 上传的文件会保存在该子目录下(相对于文件存储根目录)
*
* @var string
*/
protected string $fileSaveDir = 'new_module';
/**
* 富文本字段
*
* 这些字段会做富文本特殊处理(如 XSS 过滤、图片路径修正等)
*
* @var array
*/
protected array $contentFields = ['content'];
/**
* 列表查询中需要 LIKE 模糊匹配的字段
*
* 请求参数中若带有这些字段,会自动以 LIKE '%value%' 的方式参与查询
*
* @var array
*/
protected array $pageLikeFields = ['name', 'title'];
/**
* 列表查询中需要精确匹配的字段
*
* 请求参数中若带有这些字段,会自动以 = 的方式参与查询
*
* @var array
*/
protected array $pageEqFields = ['status', 'type'];
/**
* 列表查询的默认排序规则
*
* field:排序字段;type:排序方式(asc / desc)
*
* @var array
*/
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
/**
* 唯一性校验字段
*
* 支持两种写法:
* - 字符串:单字段全局唯一,如 'code'
* - 数组:多字段组合唯一,如 ['name', 'pid'] 表示同层级下 name 唯一
*
* 新增 / 修改时会自动校验,冲突则抛出异常
*
* @var array
*/
protected array $uniqueFields = ['code', ['name', 'pid']];
/**
* 枚举字段与显示名的映射
*
* 键为数据表字段名,值为对应的枚举字典 key,
* 返回数据时会自动附加对应的枚举显示名(如 status_text)
*
* @var array
*/
protected array $serializeMaps = ['status' => 'module_status'];
/**
* 数据权限-按创建人过滤的字段
*
* 配置后,非管理员用户只能看到自己创建的数据
*
* @var string
*/
protected string $dataScopeUserField = 'create_user';
/**
* 数据权限-按部门过滤的字段
*
* 配置后,非管理员用户只能看到本部门(及子部门)的数据
*
* @var string
*/
protected string $dataScopeDeptField = 'dept_id';
/**
* 租户隔离字段
*
* 继承 BaseTenantLogic 时自动启用,所有查询会自动追加 tenant_id 条件,
* 保证多租户之间的数据完全隔离
*
* @var string
*/
// protected string $tenantScopeField = 'tenant_id';
/**
* 树形结构的父级字段
*
* 配置后,列表接口会自动将平铺数据组装成树形结构
*
* @var string
*/
protected string $treeParentField = 'parent_id';
/**
* 树形结构的搜索字段
*
* 树形搜索时对该字段做模糊匹配,命中节点会自动保留其父链
*
* @var string
*/
protected string $treeLikeField = 'name';
}
```
### 属性驱动的自动行为
| 属性 | 触发的自动行为 |
|------|---------------|
| `validateClass` | add/update 时自动调用验证器校验参数 |
| `fileFields` | add/update 时迁移临时文件,查询时补全域名 |
| `contentFields` | add/update 时处理富文本媒体文件,查询时替换占位符 |
| `pageLikeFields` | pageList 时自动构建 LIKE 查询 |
| `pageEqFields` | pageList 时自动构建精确查询 |
| `pageOrderBy` | pageList/allList 时自动应用默认排序 |
| `uniqueFields` | add/update 时自动校验唯一性 |
| `serializeMaps` | 查询时自动补全 `{字段名}Text` 枚举显示名 |
| `dataScopeUserField` | 查询时自动按创建人过滤 |
| `dataScopeDeptField` | 查询时自动按部门过滤 |
| `tenantScopeField` | 查询时自动叠加 tenant_id 条件,新增时自动填充 |
## 2. 生命周期钩子
通过重写钩子注入定制逻辑,无需修改基类。钩子方法在基类中定义为空实现或默认返回,子类按需覆盖。
### 钩子清单
| 钩子 | 时机 | 参数 | 返回值 | 可拦截 |
|------|------|------|--------|--------|
| `beforeAdd` | 新增前 | `$data` | 处理后的 `$data` | ✅ 抛异常 |
| `afterAdd` | 新增后 | `$id, $data` | void | ❌ |
| `beforeUpdate` | 修改前 | `$id, $data` | 处理后的 `$data` | ✅ 抛异常 |
| `afterUpdate` | 修改后 | `$id, $data` | void | ❌ |
| `beforeDelete` | 删除前 | `$id` | void | ✅ 抛异常 |
| `afterDelete` | 删除后 | `$id` | void | ❌ |
| `beforeBatchDelete` | 批量删除前 | `$ids` | void | ✅ 抛异常 |
| `afterBatchDelete` | 批量删除后 | `$ids` | void | ❌ |
| `afterDetail` | 详情查询后 | `$id, &$data` | void(引用传递) | ❌ |
| `afterPageList` | 列表查询后 | `&$records, $params` | void(引用传递,分页与全量列表均触发) | ❌ |
### 典型用法
```php
class ArticleLogic extends BaseTenantLogic
{
/**
* 新增前处理:设置默认值
*
* @param array $data 待新增的数据
* @return array 处理后的数据
*/
protected function beforeAdd(array $data): array
{
// sort:排序字段,若未传则默认为 0
$data['sort'] = $data['sort'] ?? 0;
// author:作者字段,若未传则尝试从当前登录用户信息中取用户名
$data['author'] = $data['author'] ?? request()->userInfo->username ?? '';
return $data;
}
/**
* 新增后处理:保存关联数据
*
* @param int $id 新增文章的主键 ID
* @param array $data 新增时提交的原始数据
* @return void
*/
protected function afterAdd(int $id, array $data): void
{
// 若提交数据中带有 tags(标签数组),则批量写入文章-标签关联表
if (!empty($data['tags'])) {
ArticleTag::insertAll(array_map(fn($tag) => [
'article_id' => $id, 'tag' => $tag
], $data['tags']));
}
}
/**
* 修改前处理:过滤不可修改字段
*
* @param int $id 文章 ID
* @param array $data 待更新的数据
* @return array 过滤后的数据
*/
protected function beforeUpdate(int $id, array $data): array
{
// author(作者)字段不允许通过更新接口修改,直接剔除
unset($data['author']);
return $data;
}
/**
* 删除前处理:业务约束校验
*
* @param int $id 文章 ID
* @return void
* @throws \Exception 文章下存在评论时抛出
*/
protected function beforeDelete(int $id): void
{
// 若该文章下存在评论,则不允许删除
$commentCount = Comment::where('article_id', $id)->count();
if ($commentCount > 0) {
throw new \Exception('该文章下存在评论,不可删除');
}
}
/**
* 详情查询后处理:补充单条记录的关联数据
*
* @param int $id 文章 ID
* @param array &$data 详情数据(引用传递)
* @return void
*/
protected function afterDetail(int $id, array &$data): void
{
$data['tags'] = ArticleTag::where('article_id', $id)->column('tag');
$data['commentCount'] = Comment::where('article_id', $id)->count();
}
/**
* 列表查询后处理:批量补充关联数据
*
* @param array &$records 记录列表(引用传递)
* @param array $params 查询参数
* @return void
*/
protected function afterPageList(array &$records, array $params): void
{
if (empty($records)) return;
// 提取当前页所有文章 ID
$ids = array_column($records, 'id');
// 一次性统计每篇文章的评论数,返回 [article_id => count] 结构
$counts = Comment::whereIn('article_id', $ids)
->group('article_id')
->column('count(*)', 'article_id');
// 将统计结果回填到每条记录
foreach ($records as &$record) {
$record['commentCount'] = $counts[$record['id']] ?? 0;
}
}
}
```
### 执行顺序
**add 流程:**
`beforeAdd` → `validateData` → `camel_to_snake` → `processTenantId` → `processFile` → `processContent` → `filterTableFields` → `checkUnique` → `Model::create` → `afterAdd`
**update 流程:**
`beforeUpdate` → `validateData` → `camel_to_snake` → `processFile` → `processContent` → `filterTableFields` → `checkUnique` → `Model::find + save` → `afterUpdate`
**delete 流程:**
`beforeDelete` → `is_delete = 1` → `afterDelete`
**pageList/allList 流程:**
`buildQuery` → `排序` → `分页` → `processFile` → `processContent` → `processSerializeMaps` → `filterFields` → `afterPageList`
## 3. 自动参数校验
通过在 Logic 中配置 `$validateClass`,add/update 时自动调用验证器,无需手动调用。
### 扩展方式
```php
// 1. 创建验证器
// app/validate/NewModuleValidate.php
class NewModuleValidate extends Validate
{
protected $rule = [
'name' => 'require|max:100',
'code' => 'require|unique:new_module',
];
protected $message = [
'name.require' => '名称不能为空',
'code.unique' => '编码已存在',
];
protected $scene = [
'add' => ['name', 'code'],
'update' => ['name'],
];
}
// 2. 在 Logic 中配置
class NewModuleLogic extends BaseLogic
{
protected string $validateClass = NewModuleValidate::class;
// ...
}
```
详见 [3.4 验证器开发](/zh/guide/validator.html)。
## 4. 中间件机制
通过中间件链式处理请求,可灵活添加新的拦截逻辑。
### 现有中间件
```text
请求进入
│
▼
CorsMiddleware(全局) ← OPTIONS 预检 + 跨域响应头
│
▼
AuthMiddleware(路由级) ← JWT 认证 + 权限校验 → 401/403
│ 注入 userInfo
▼
TenantMiddleware(路由级) ← 租户上下文注入 → 403
│ 注入 tenantId
▼
DemoMiddleware(路由级) ← 演示环境拦截写操作 → 403
│
▼
LogMiddleware(路由级) ← 操作日志记录
│
▼
控制器方法
```
### 创建自定义中间件
```php
// 1. 创建中间件类
// app/middleware/RateLimitMiddleware.php
namespace app\middleware;
use think\facade\Cache;
class RateLimitMiddleware
{
public function handle($request, \Closure $next)
{
$ip = $request->ip();
$key = 'rate_limit_' . $ip;
$count = Cache::get($key, 0);
if ($count > 100) {
return json(['code' => 1, 'msg' => '请求过于频繁'], 429);
}
Cache::set($key, $count + 1, 60);
return $next($request);
}
}
// 2. 注册中间件
// 方式一:全局注册(app/middleware.php)
return [
\app\middleware\RateLimitMiddleware::class,
];
// 方式二:路由级注册(route/app.php)
Route::group('api', function () {
// ...
})->middleware(\app\middleware\RateLimitMiddleware::class);
// 方式三:控制器级注册
class SomeController extends BaseController
{
protected $middleware = [
\app\middleware\RateLimitMiddleware::class,
];
}
```
## 5. 注解系统
通过 PHP 8 注解声明元数据,中间件通过反射读取,业务代码零侵入。
### 现有注解
| 注解 | 目标 | 说明 | 读取方 |
|------|------|------|--------|
| `#[Log]` | 方法 | 操作日志(标题、类型、描述) | LogMiddleware |
| `#[Permission]` | 方法/类 | 权限校验(权限编码) | AuthMiddleware |
| `#[DemoAllow]` | 方法 | 演示环境放行标记 | DemoMiddleware |
### 使用示例
```php
// 操作日志
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
// 权限校验
#[Permission('sys:user:add', '添加用户')]
// 演示环境放行(允许在演示环境执行写操作)
#[DemoAllow]
// 组合使用
#[Log('文章管理-删除记录', Log::TYPE_DELETE, '删除文章ID:{id}')]
#[Permission('sys:article:delete', '删除文章')]
public function delete(int $id): Json
{
return parent::_remove($this->logic, $id);
}
```
### 创建自定义注解
```php
// 1. 定义注解类
// app/attribute/Cacheable.php
namespace app\attribute;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Cacheable
{
public function __construct(
public string $key = '',
public int $ttl = 3600,
) {}
}
// 2. 在控制器方法上使用
#[Cacheable(key: 'article_list', ttl: 1800)]
public function list(): Json { ... }
// 3. 在中间件中读取
// 通过 AttributeService 或反射获取注解实例
$ref = new \ReflectionMethod($controller, $method);
$attrs = $ref->getAttributes(Cacheable::class);
if (!empty($attrs)) {
$cacheable = $attrs[0]->newInstance();
// $cacheable->key, $cacheable->ttl
}
```
### 注解读取服务
`AttributeService` 提供统一的注解读取接口(带缓存):
```php
AttributeService::getLog($class, $method); // 获取 Log 注解
AttributeService::getPermission($class, $method); // 获取 Permission 注解
```
## 6. 服务层复用
通用能力封装为 Service,跨模块复用,无状态、无副作用。
### 现有服务清单
| 服务 | 文件 | 能力 |
|------|------|------|
| `JwtService` | `app/service/JwtService.php` | 登录、刷新、解析 Token |
| `PasswordService` | `app/service/PasswordService.php` | bcrypt 双重加盐加密 |
| `DictService` | `app/service/DictService.php` | 字典查询(两级缓存) |
| `ParamService` | `app/service/ParamService.php` | 系统参数查询(两级缓存) |
| `CaptchaService` | `app/service/CaptchaService.php` | 图片验证码生成与校验 |
| `RequestInfoService` | `app/service/RequestInfoService.php` | 客户端 IP、UA、归属地解析 |
| `AttributeService` | `app/service/AttributeService.php` | 注解反射读取 + 缓存 |
| `GeneratorService` | `app/service/GeneratorService.php` | 代码生成 |
| `ExcelService` | `app/service/ExcelService.php` | Excel 导入导出 |
| `DbMigrateService` | `app/service/DbMigrateService.php` | 跨数据库迁移引擎 |
| `DbSchemaBuilder` | `app/service/DbSchemaBuilder.php` | DDL 生成器(跨库映射) |
### 使用示例
```php
// 字典服务
DictService::getText('gender', 1); // → '男'
DictService::getValue('user_status', '启用'); // → 1
DictService::getOptions('gender'); // → [['value'=>0,'label'=>'女'], ...]
// 密码服务
$encrypted = PasswordService::encrypt('123456'); // → ['password'=>'...', 'salt'=>'...']
$default = PasswordService::getDefaultPasswordPlain(); // → '123456'
// Excel 服务
ExcelService::importWithMap($filePath, $headerMap);
ExcelService::chunkExport($headers, $callback, '导出.xlsx', 500);
// 请求信息
$requestInfo = RequestInfoService::create(request());
$ip = $requestInfo->getIp(); // 客户端 IP(经 ProxyMiddleware 还原真实 IP)
$location = $requestInfo->getIpLocation(); // IP 归属地(基于 zoujingli/ip2region)
$os = $requestInfo->getOs(); // 操作系统
$browser = $requestInfo->getBrowser(); // 浏览器
```
### 创建自定义服务
```php
// app/service/NotificationService.php
namespace app\service;
class NotificationService
{
// 发送站内通知
public static function send(int $userId, string $title, string $content): void
{
\app\model\Notice::create([
'user_id' => $userId,
'title' => $title,
'content' => $content,
'status' => 0,
]);
}
// 批量发送
public static function batchSend(array $userIds, string $title, string $content): void
{
foreach ($userIds as $userId) {
self::send($userId, $title, $content);
}
}
}
// 使用
NotificationService::send($userId, '系统通知', '您有一条新消息');
```
## 扩展性设计原则
### 开闭原则(OCP)
| 扩展场景 | 做法 | 不应该做 |
|----------|------|----------|
| 新增 CRUD 模块 | 创建 Controller/Logic/Model/Validate | 修改基类 |
| 新增查询条件 | 设置 `pageLikeFields`/`pageEqFields` | 重写 `buildQuery` |
| 新增文件处理 | 设置 `fileFields` | 手动调用 `save_file` |
| 新增校验规则 | 创建 Validate + 配置 `validateClass` | 在 Controller 手动 validate |
| 新增请求拦截 | 创建中间件 | 修改现有中间件 |
| 新增操作日志 | 添加 `#[Log]` 注解 | 在方法内手动记录日志 |
| 新增权限节点 | 添加 `#[Permission]` 注解 | 在方法内手动校验权限 |
| 新增通用能力 | 创建 Service | 在 Logic 中写静态方法 |
### 最小改动原则
新增一个完整的业务模块,只需要:
1. 建表(含公共字段)
2. 创建 Model(继承 BaseModel,声明表名)
3. 创建 Validate(定义规则 + 场景)
4. 创建 Logic(继承 BaseLogic/BaseTenantLogic,配置属性)
5. 创建 Controller(继承 BaseController,初始化 Logic,CRUD 一行调用)
6. 注册路由
**不需要修改的文件:** 基类、中间件、配置文件、服务层。