Skip to content

4.11 扩展性设计 ​

概述

系统通过六种机制保证良好的扩展性:属性配置、生命周期钩子、自动参数校验、中间件链、注解系统、服务层复用。新增模块无需修改框架代码,只需继承基类 + 配置属性 + 按需重写钩子。

扩展机制总览 ​

机制位置作用扩展方式
属性配置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';
}

属性驱动的自动行为 ​

属性触发的自动行为
validateClassadd/update 时自动调用验证器校验参数
fileFieldsadd/update 时迁移临时文件,查询时补全域名
contentFieldsadd/update 时处理富文本媒体文件,查询时替换占位符
pageLikeFieldspageList 时自动构建 LIKE 查询
pageEqFieldspageList 时自动构建精确查询
pageOrderBypageList/allList 时自动应用默认排序
uniqueFieldsadd/update 时自动校验唯一性
serializeMaps查询时自动补全 {字段名}Text 枚举显示名
dataScopeUserField查询时自动按创建人过滤
dataScopeDeptField查询时自动按部门过滤
tenantScopeField查询时自动叠加 tenant_id 条件,新增时自动填充

2. 生命周期钩子 ​

通过重写钩子注入定制逻辑,无需修改基类。钩子方法在基类中定义为空实现或默认返回,子类按需覆盖。

钩子清单 ​

钩子时机参数返回值可拦截
beforeAdd新增前$data处理后的 $data✅ 抛异常
afterAdd新增后$id, $datavoid❌
beforeUpdate修改前$id, $data处理后的 $data✅ 抛异常
afterUpdate修改后$id, $datavoid❌
beforeDelete删除前$idvoid✅ 抛异常
afterDelete删除后$idvoid❌
beforeBatchDelete批量删除前$idsvoid✅ 抛异常
afterBatchDelete批量删除后$idsvoid❌
afterDetail详情查询后$id, &$datavoid(引用传递)❌
afterPageList列表查询后&$records, $paramsvoid(引用传递,分页与全量列表均触发)❌

典型用法 ​

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 验证器开发。

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,跨模块复用,无状态、无副作用。

现有服务清单 ​

服务文件能力
JwtServiceapp/service/JwtService.php登录、刷新、解析 Token
PasswordServiceapp/service/PasswordService.phpbcrypt 双重加盐加密
DictServiceapp/service/DictService.php字典查询(两级缓存)
ParamServiceapp/service/ParamService.php系统参数查询(两级缓存)
CaptchaServiceapp/service/CaptchaService.php图片验证码生成与校验
RequestInfoServiceapp/service/RequestInfoService.php客户端 IP、UA、归属地解析
AttributeServiceapp/service/AttributeService.php注解反射读取 + 缓存
GeneratorServiceapp/service/GeneratorService.php代码生成
ExcelServiceapp/service/ExcelService.phpExcel 导入导出
DbMigrateServiceapp/service/DbMigrateService.php跨数据库迁移引擎
DbSchemaBuilderapp/service/DbSchemaBuilder.phpDDL 生成器(跨库映射)

使用示例 ​

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. 注册路由

不需要修改的文件: 基类、中间件、配置文件、服务层。

小蚂蚁云团队 · 提供技术支持