签入版本

This commit is contained in:
小蚂蚁云
2026-10-25 17:24:27 +08:00
parent 48911cc08f
commit fdbeae092a
1010 changed files with 30682 additions and 95274 deletions
@@ -1,612 +1,87 @@
# 4.11 扩展性设计
# 4.12 扩展性设计
:::tip 概述
系统通过六种机制保证良好的扩展性:属性配置、生命周期钩子、自动参数校验、中间件链、注解系统、服务层复用。新增模块无需修改框架代码,只需继承基类 + 配置属性 + 按需重写钩子。
系统的扩展点都是显式声明的:钩子承接业务差异、生成器模板可自定义、任务自注册、驱动可注册、中间件能力可接线裁剪。本页汇总"想加东西时从哪下手"。
:::
## 扩展机制总览
## 扩展点地图
| 机制 | 位置 | 作用 | 扩展方式 |
|------|------|------|----------|
| 属性配置 | Logic 子类 | 声明式行为定义 | 设置属性值 |
| 生命周期钩子 | Logic 子类 | 注入定制逻辑 | 重写钩子方法 |
| 自动参数校验 | Validate + Logic | 参数格式校验 | 创建 Validate + 配置 `$validateClass` |
| 中间件链 | middleware/ | 请求拦截处理 | 创建中间件 + 注册 |
| 注解系统 | attribute/ | 声明式元数据 | 创建注解 + 中间件读取 |
| 服务层复用 | service/ | 跨模块通用能力 | 创建 Service 类 |
| 想做什么 | 扩展点 | 位置 |
|---|---|---|
| 模块特有 CRUD 差异 | ModuleSpec 钩子 | service 各模块 |
| 标准方法覆盖不了的逻辑 | 手写方法(合法逃生舱) | service 各模块 |
| 新定时任务 | init 自注册 | internal/task |
| 新数据库方言 | RegisterDriver | internal/model/db |
| 生成产物样式 | 模板替换 | templates/*.tpl |
| 中间件依赖的业务能力 | Set*Loader / SetLogSink 接线 | middleware + service init |
| 新基础设施能力 | 新建 utility 包 | utility/ |
| 新配置节 | config.yaml + .env.example 同步 | config/ |
## 1. 属性配置模式
## ModuleSpec 钩子(最常用扩展点)
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. 中间件机制
通过中间件链式处理请求,可灵活添加新的拦截逻辑。
### 现有中间件
十个钩子覆盖 CRUD 全生命周期:
```text
请求进入
│
▼
CorsMiddleware(全局) ← OPTIONS 预检 + 跨域响应头
│
▼
AuthMiddleware(路由级) ← JWT 认证 + 权限校验 → 401/403
│ 注入 userInfo
▼
TenantMiddleware(路由级) ← 租户上下文注入 → 403
│ 注入 tenantId
▼
DemoMiddleware(路由级) ← 演示环境拦截写操作 → 403
│
▼
LogMiddleware(路由级) ← 操作日志记录
│
▼
控制器方法
BeforeAdd / AfterAdd 新增前后
BeforeUpdate / AfterUpdate 修改前后
BeforeDelete / AfterDelete 删除前后
BeforeBatchDelete / AfterBatchDelete
AfterPageList / AfterDetail 读侧后处理
```
### 创建自定义中间件
约束:错误原样上抛、禁 HTTP/文件 IO、禁 append。超出约束的逻辑放 service 手写方法,不要硬塞钩子。
```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,
];
}
```text
templates/
├── dto.tpl / repository.tpl / service.tpl / handler.tpl 后端四件套
├── ui/ 标准页面模板(api.ts/columns.ts/index/edit/detail/querySchemas)
└── ui2/ 树形表页面模板
```
## 5. 注解系统
替换对应 tpl 即改变全项目生成风格;生成器同时负责菜单幂等创建与 router.go 声明块追加。见 [10.5 模板体系](/zh/generator/templates.html)。
通过 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);
}
```go
// internal/task/xxx.go
func init() { Register("任务名", spec) } // 调度器启动时自动纳入
```
### 创建自定义注解
调度器、执行锁、任务日志由 task 包统一提供,任务实现只写业务。
```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
}
```go
db.RegisterDriver("oracle", factory) // 扩展方言经构建标签隔离
```
### 注解读取服务
内置四方言在包内注册;Oracle 等重依赖方言用构建标签(`-tags oracle`)避免拖累默认构建。
`AttributeService` 提供统一的注解读取接口(带缓存):
## 中间接线与裁剪
```php
AttributeService::getLog($class, $method); // 获取 Log 注解
AttributeService::getPermission($class, $method); // 获取 Permission 注解
```go
middleware.SetIdentityLoader(...) // auth 模块
middleware.SetTenantLoader(...) // auth 模块
middleware.SetLogSink(...) // operation_log 模块
```
## 6. 服务层复用
未接线的能力被 WireCheck 探测到只告警降级——按模块裁剪部署形态(如去掉内容管理模块)不会引发启动失败。
通用能力封装为 Service,跨模块复用,无状态、无副作用。
## 前端扩展
### 现有服务清单
| 扩展点 | 方式 |
|---|---|
| 公共组件 | ui/src/components 新增,全局注册 |
| 布局插槽 | layout components 按区域扩展 |
| 主题 | styles 主题变量 + designSetting store |
| 路由 | 动态(菜单 API)+ constant 静态补充 |
| 服务 | 文件 | 能力 |
|------|------|------|
| `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 生成器(跨库映射) |
## 扩展的边界
### 使用示例
扩展不应破坏的稳定面:dto 契约字段名、RouteSpec 声明语义、统一信封结构、`gin_` 表列约定。生成器与前端都依赖这些稳定面,改动前先评估连锁影响。
```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. 注册路由
**不需要修改的文件:** 基类、中间件、配置文件、服务层。
→ [第 5 章 核心功能](/zh/core/jwt.html)