import{_ as i,o as a,c as h,a2 as n}from"./chunks/framework.BbmSSMjL.js";const g=JSON.parse('{"title":"4.11 扩展性设计","description":"","frontmatter":{},"headers":[],"relativePath":"zh/architecture/extensibility.md","filePath":"zh/architecture/extensibility.md"}'),t={name:"zh/architecture/extensibility.md"};function k(l,s,p,e,d,r){return a(),h("div",null,[...s[0]||(s[0]=[n(`

4.11 扩展性设计 ​

概述

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

扩展机制总览 ​

机制位置作用扩展方式
属性配置Service 子类声明式行为定义设置属性值
生命周期钩子Service 子类注入定制逻辑重写钩子方法
参数格式校验FormRequestHTTP 入口拦截非法输入创建 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 核心基类体系。

属性驱动的自动行为 ​

属性触发的自动行为
fileFieldsadd/update 时迁移临时文件,查询时补全域名
multiFileFields逗号分隔多文件逐个迁移/补全
contentFieldsadd/update 时处理富文本媒体文件,查询时替换占位符
pageLikeFieldspageList 时自动构建 LIKE 查询
pageEqFieldspageList 时自动构建精确查询
pageOrderBy / orderFieldAllow自动应用默认排序 / 动态排序白名单
uniqueFieldsadd/update 时自动校验唯一性(BizException 拦截)
serializeMaps查询时自动补全 {字段}Text 字典显示名
dataScopeUserField / dataScopeDeptField查询时自动按创建人/部门过滤
tenantScopeField查询时自动叠加 tenant_id 条件,新增时自动填充
treeParentField / treeLikeField树形列表自动组装 / 子树模糊搜索

未启用的属性是空操作

BaseService 为 6 个 Trait 的方法提供占位实现,子类 use 对应 Trait 后真实实现才生效(占位覆盖机制)。属性声明了但未 use 对应 Trait 时,对应自动行为不会执行。

2. 生命周期钩子 ​

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

钩子清单 ​

钩子时机参数返回值可拦截
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
// 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 验证器。

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)
#[WithoutOperationLog]方法跳过操作日志标记(标注后写操作也不记录)LogOperation 中间件(ReflectionMethod)
#[Permission]方法权限校验(权限编码、名称)Authenticate 中间件(ReflectionMethod)
#[DemoAllow]方法演示环境放行标记DemoMiddleware(ReflectionMethod)

使用示例 ​

php
// 操作日志(TYPE 常量 30 种,描述支持 {param} 占位符)
#[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')]

// 权限校验
#[Permission('sys:user:add', '添加用户')]

// 演示环境放行(允许在演示环境执行写操作)
#[DemoAllow]

// 跳过操作日志(已有专门日志载体的写接口,如登出写入 login_log)
#[WithoutOperationLog]

// 组合使用
#[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() 读取即可。

注解读取性能

反射读取开销为微秒级,Authenticate / LogOperation 均直接使用 ReflectionMethod 读取(无缓存),避免开发阶段改注解后需等缓存过期。

6. 服务层复用 ​

通用能力封装为 Service,跨模块复用,无状态、无副作用。

现有服务清单 ​

服务文件(app/Services/)能力
JwtServiceJwtService.php登录、刷新、解析 Token(Redis 有状态)
PasswordServicePasswordService.phpbcrypt 双重加盐加密
DictCacheServiceDictCacheService.php字典查询(两级缓存)
CaptchaServiceCaptchaService.php图片验证码生成与校验(IP 限流)
RequestInfoServiceRequestInfoService.php客户端 IP、UA、归属地解析
GeneratorServiceGeneratorService.php代码生成
ExcelServiceExcelService.phpExcel 导入导出
DbMigrateServiceDbMigrateService.php跨数据库迁移引擎
DbSchemaBuilderDbSchemaBuilder.phpDDL 生成器(跨库映射、Oracle 序列)
ConfigServiceConfigService.php系统配置读写与缓存
DictServiceDictService.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 章 代码生成器 一键生成以上全部。)

`,65)])])}const y=i(t,[["render",k]]);export{g as __pageData,y as default};