# 4.6 核心基类体系 :::tip 概述 项目有四个核心基类/骨架——`Controller`、`BaseService`、`BaseTenantService`、模型 Concerns Trait——构成"控制器 → 服务 → 模型"三层架构的骨架。所有业务模块都必须继承对应的基类,通过属性配置 + 生命周期钩子实现声明式 CRUD,子类只需声明"做什么"而不用写"怎么做"。 ::: ## 基类继承关系 :::warning 继承规则 - 系统共享模块(菜单、字典、配置等)继承 `BaseService` - 租户隔离模块(用户、角色、文章、部门、职级等)继承 `BaseTenantService` - 所有模型继承 Eloquent `Model` 并组合 `SoftDeletesByFlag` + `HasAuditColumns` Trait - 所有控制器继承 `App\Http\Controllers\Controller` - 所有请求类继承 `BaseRequest` ::: ```text Controller(控制器基类) └── 所有 Api/Controller BaseService(服务基类,组合 6 个 Concerns Trait 的占位) ├── ExampleService(系统共享/示例) ├── MenuService、DictCacheService、ConfigService ... │ └── BaseTenantService(租户服务基类,use HasDataScope + tenantScopeField='tenant_id') ├── UserService ├── RoleService ├── ArticleService └── ... Illuminate\Database\Eloquent\Model(Eloquent 模型) └── 所有模型(use SoftDeletesByFlag, HasAuditColumns) FormRequest(Laravel 表单请求) └── BaseRequest(统一失败响应) └── 所有 Request ``` ## 请求处理全链路 理解基类体系的关键是理解一次请求如何流经各层: ```text HTTP 请求 │ ▼ Controller(编排、响应封装) │ FormRequest 类型注入 → 格式校验(validated()) │ success() / fail() / _page() / _add() │ ▼ BaseService(业务编排、属性驱动) │ 入口键名转换 → array_camel_to_snake() │ beforeAdd() → 生命周期钩子 │ checkUnique() → 唯一性校验(BizException) │ DB::transaction → 事务包裹 │ processAfterRead → 读取后处理链(文件/字典翻译) │ ▼ Model(数据持久化,Concerns Trait) │ HasAuditColumns → 审计字段自动填充(creating/updating 事件) │ SoftDeleteFlagScope → 全局过滤 is_delete=1 │ ▼ 数据库 ``` --- ## Controller **文件**:`app/Http/Controllers/Controller.php` ### 设计要点 - 通用 CRUD 方法以 `_` 前缀命名,避免与子类路由方法冲突 - 子类通过**构造函数注入** Service(Laravel 依赖注入),无需 `initialize()` - 当前登录用户通过 `Support\Auth` 工具类获取(`Auth::userId()` 等) - `BizException` 由 `_add/_update/_delete` 等方法捕获转为失败响应 ### 方法分类 ```text ┌─────────────────────────────────────────────────────────────┐ │ Controller 基类 │ ├─────────────────────────────────────────────────────────────┤ │ 统一响应(委托 Support\Result) │ │ success($data, $msg, $raw) 成功响应 │ │ paginate($result) 分页响应 │ │ fail($msg, $code, $data) 失败响应(code 默认 1) │ ├─────────────────────────────────────────────────────────────┤ │ 参数获取 │ │ getPageParams() 分页参数(pageNo≥1, │ │ pageSize 1~100,默认 20) │ │ getJsonBody() JSON 请求体 │ │ getParams() 全部参数(query + body 合并) │ ├─────────────────────────────────────────────────────────────┤ │ 通用 CRUD(子类通过 $this-> 调用) │ │ _page($service, $where, $with) 分页查询 │ │ _list($service, $where) 全量列表 │ │ _treeList($service, $where) 树形列表 │ │ _detail($service, $id) 详情(空 → 404) │ │ _add($service, $data) 新增(返回 id) │ │ _update($service, $id, $data) 修改 │ │ _delete($service, $id) 删除 │ │ _batchDelete($service, $ids) 批量删除 │ │ _export($service) 导出(返回文件地址) │ │ _import($service) 导入(文件校验 + 结果汇总) │ └─────────────────────────────────────────────────────────────┘ ``` ### 通用 CRUD 方法与 Service 层的对应关系 | Controller 方法 | Service 方法 | 说明 | |---|---|---| | `_page($service)` | `pageList()` | 分页查询 | | `_list($service)` | `allList()` | 全量列表 | | `_treeList($service)` | `treeList()` | 树形列表 | | `_detail($service, $id)` | `detail($id)` | 详情查询(空返回 404) | | `_add($service, $data)` | `add($data)` | 新增(捕获 BizException) | | `_update($service, $id, $data)` | `update($id, $data)` | 修改(id 缺失直接失败提示) | | `_delete($service, $id)` | `delete($id)` | 删除(BizException→业务提示;其它异常记日志) | | `_batchDelete($service, $ids)` | `batchDelete($ids)` | 批量删除(ids 为空直接提示) | | `_export($service)` | `export()` | 导出 | | `_import($service)` | `import()` | 导入(扩展名/大小/MIME 三重校验) | ### 分页参数 `getPageParams()` 返回 `['pageNo' => int, 'pageSize' => int]`,带边界限制: - `pageNo` 最小为 1 - `pageSize` 限制在 1~100,默认 20(防止一次拉取过多数据) 前端传参示例:`GET /api/example/page?pageNo=1&pageSize=10&name=测试&status=1` ### 子类实现示例 ```php class ExampleController extends Controller { public function __construct(protected ExampleService $exampleService) {} // 分页查询 — 一行搞定 #[Permission('sys:example:page', '示例分页')] public function page(): JsonResponse { return $this->_page($this->exampleService); } // 详情 #[Permission('sys:example:detail', '示例详情')] public function detail(int $id): JsonResponse { return $this->_detail($this->exampleService, $id); } // 新增(参数由 ExampleRequest 类型注入校验) #[OperationLog('示例管理-新增', OperationLog::TYPE_ADD, '新增示例:{name}')] #[Permission('sys:example:add', '添加示例')] public function add(ExampleRequest $request): JsonResponse { return $this->_add($this->exampleService, $request->validated()); } // 删除 — 一行搞定 #[Permission('sys:example:delete', '删除示例')] public function delete(int $id): JsonResponse { return $this->_delete($this->exampleService, $id); } } ``` --- ## BaseService **文件**:`app/Services/BaseService.php` BaseService 是整个项目的核心,通过"属性配置 + 生命周期钩子"实现声明式 CRUD。子类只需配置属性和按需重写钩子,即可获得完整的增删改查能力。 ### 属性配置清单(全部集中声明于 BaseService) | 属性 | 类型 | 说明 | 示例 | |---|---|---|---| | `modelClass` | string | 关联模型类名(**必须**) | `Example::class` | | `defaultWith` | array | 查询默认预加载关联 | `['creator']` | | `fileFields` | array | 单文件字段列表(需 use HasFileProcessing) | `['cover', 'avatar']` | | `multiFileFields` | array | 多文件字段列表(逗号分隔的多个URL) | `['images']` | | `contentFields` | array | 富文本字段 | `['content']` | | `fileSaveDir` | string | 文件保存子目录 | `'article'` | | `pageLikeFields` | array | LIKE 模糊匹配字段 | `['title', 'author']` | | `pageEqFields` | array | 精确匹配字段 | `['status', 'category_id']` | | `pageOrderBy` | array | 默认排序规则 | `['field'=>'sort','type'=>'asc']` | | `orderFieldAllow` | array | 允许前端动态排序的字段白名单 | `['create_time']` | | `uniqueFields` | array | 唯一性校验规则(字符串全局 / 数组分组) | `['username', ['code','pid']]` | | `serializeMaps` | array | 字典显示名映射 | `['status'=>'article_status']` | | `dataScopeUserField` | string | 数据权限:归属字段(需 use HasDataScope) | `'create_user'` | | `dataScopeDeptField` | string | 数据权限:部门字段(需 use HasDataScope) | `'dept_id'` | | `tenantScopeField` | string | 租户隔离字段(BaseTenantService 预设) | `'tenant_id'` | | `treeParentField` | string | 树形结构父级字段 | `'parent_id'` | | `treeLikeField` | string | 树形列表模糊搜索字段 | `'name'` | ### 生命周期钩子(HasCrudOperations 提供 10 个) | 钩子 | 时机 | 用途 | 可拦截 | |---|---|---|---| | `beforeAdd($data)` | 新增前 | 修改数据、预处理,**返回处理后的 data** | ✅ 抛异常 | | `afterAdd($id, $data)` | 新增后 | 保存关联表、发送通知 | ✅(事务内) | | `beforeUpdate($id, $data)` | 修改前 | 修改数据、预处理,**返回处理后的 data** | ✅ 抛异常 | | `afterUpdate($id, $data)` | 修改后 | 更新关联表 | ✅(事务内) | | `beforeDelete($id)` | 删除前 | 检查子级、业务约束 | ✅ 抛异常 | | `afterDelete($id)` | 删除后 | 清理关联数据 | ✅(事务内) | | `beforeBatchDelete($ids)` | 批量删除前 | 整体校验 | ✅ 抛异常 | | `afterBatchDelete($ids)` | 批量删除后 | 批量清理 | ✅(事务内) | | `afterDetail($id, &$data)` | 详情查询后 | 补充关联数据、格式化输出(引用传递) | ❌ | | `afterPageList(&$records, $params)` | 列表查询后 | 批量补充关联数据,分页 pageList 与全量 allList 均触发(引用传递) | ❌ | ### 新增流程(add) ```text array_camel_to_snake($data) ← 入口键名统一转为 snake_case │ ▼ beforeAdd($data) ← 钩子:可修改数据,抛异常可拦截 │ ▼ checkUnique($data) ← 唯一性校验(uniqueFields,BizException 拦截) │ ▼ DB::transaction { ← 事务包裹(钩子抛异常自动回滚) │ ▼ Model::create($data) ← 写入数据库(HasAuditColumns 自动填充审计字段) │ ▼ afterAdd($id, $data) ← 钩子:保存关联表等副作用 } │ ▼ return $id ``` :::tip 职责划分 参数格式校验**不在 Service 内执行**——由控制器的 FormRequest 在入口完成(见 [3.4 FormRequest 验证器](/zh/guide/validator.html));键名转换由 Service 写入入口(doAdd/doUpdate)统一完成。Service 只做业务规则(唯一性、状态依赖)。 ::: ### 修改流程(update) ```text beforeUpdate($id, $data) ← 钩子 │ ▼ checkUnique($data, $excludeId) ← 唯一性校验(排除自身) │ ▼ DB::transaction { Model::find($id)->update($data) ← 更新(HasAuditColumns 强制刷新审计字段) afterUpdate($id, $data) ← 钩子 } ``` ### 删除流程(delete) ```text beforeDelete($id) ← 钩子:抛异常可拦截(如存在关联数据) │ ▼ DB::transaction { is_delete = 1 ← 软删除(SoftDeletesByFlag) afterDelete($id) } ``` ### 查询流程(pageList / allList) ```text buildQuery($model) ← 构建查询条件(额外条件 + LIKE + 精确匹配 │ + 数据权限 + 租户隔离,camel→snake 转换参数) ▼ applyOrderBy($model) ← 排序(前端 orderField/orderType 优先, │ 白名单校验,否则用 pageOrderBy 配置) ▼ $page / select ← 分页 / 全量查询 │ ▼ processAfterRead 处理链 ├─ processFileFieldsOnReadList ← 文件字段补全域名(HasFileProcessing 启用时) ├─ processSerializeMapsList ← 字典显示名补全(status → statusText) └─ filterFields ← 字段过滤(fields 参数指定返回字段) │ ▼ afterPageList(&$records) ← 钩子:批量补充关联数据(引用传递) ``` ### 唯一性校验(uniqueFields,HasUniqueCheck) ```php // 全局唯一 protected array $uniqueFields = ['username', 'mobile']; // 分组内唯一:code 在同一 pid 下唯一 protected array $uniqueFields = [ 'username', // 字符串 = 全局唯一 ['code', 'pid'], // 数组 = [唯一字段, 分组字段],分组内唯一 ]; ``` 校验规则(`checkUnique`): - add/update 时自动执行;值为空时跳过(FormRequest 的 `required` 兜底) - 更新时自动排除自身(`excludeId`) - 重复时抛出 `BizException("{字段中文名}已存在")` - 字段中文名查找顺序:`$fieldLabels` 属性 → Trait 内置映射 → 字段名本身 (子类声明 `protected array $fieldLabels = ['code' => '角色编码'];` 可自定义提示) ### Trait 占位覆盖机制 `BaseService` 底部为 6 个 Trait 的所有方法提供**占位实现**(如 `processFileFieldsOnSave()` 直接 `return $data`)。子类 `use` 对应 Trait 后,PHP Trait 机制自动以真实实现覆盖占位——未启用的 Trait 方法保持空操作,避免调用未定义方法。 | Trait | 提供的方法族 | |------|-------------| | `HasCrudOperations` | add / update / delete / batchDelete(事务 + 钩子) | | `HasQueryScopes` | buildQuery / applyOrderBy / 排序白名单 | | `HasFileProcessing` | 文件/多文件/富文本字段的保存迁移与读取补全 | | `HasDataScope` | applyDataScope / applyTenantScope / processTenantId | | `HasSerializeMaps` | processSerializeMaps(字典翻译) | | `HasUniqueCheck` | checkUnique / getFieldLabel | | `HasConfigValueProcessing` | processConfigValueOnSave / buildConfigValueList / formatConfigValueOnRead | `HasConfigValueProcessing` 不依赖 `BaseService` 的占位实现,由 `ConfigService` 与 `ConfigItemService` 直接 `use`。 --- ## BaseTenantService **文件**:`app/Services/BaseTenantService.php` ### 设计说明 BaseTenantService 继承 BaseService,**仅做两件事**:`use HasDataScope` 引入数据权限 + 租户隔离能力,并将 `tenantScopeField` 预设为 `'tenant_id'`,子类自动获得租户隔离: ```php abstract class BaseTenantService extends BaseService { use Concerns\HasDataScope; /** * 租户隔离字段 * * 预设为 tenant_id,子类如需使用其它字段名可覆盖。 */ protected string $tenantScopeField = 'tenant_id'; } ``` ### 隔离机制(由 HasDataScope 实现) | 场景 | 行为 | |---|---| | **查询时** | `applyTenantScope` 自动叠加 `WHERE tenant_id = ?` | | **新增时** | `processTenantId` 自动填充租户字段:平台用户代入 `config('tenant.default_tenant_id')`,租户用户代入当前 `tenantId` | | **超级管理员** | `tenantId = 0`(平台用户)不过滤,可跨租户查看数据 | ### 租户上下文来源 `ResolveTenant` 中间件解析并注入 `request->attributes['tenantId']`(详见 [4.2 请求处理链路](/zh/architecture/request-flow.html))。 ### 数据权限 `BaseTenantService` **默认未启用数据权限**,子类声明字段后 `HasDataScope` 才会叠加过滤(摘自 `app/Services/UserService.php` 实际代码): ```php class UserService extends BaseTenantService { protected string $dataScopeDeptField = 'dept_id'; // 启用按部门过滤 } ``` ### 使用约定 - 需要租户隔离的模块继承此类(如 User、Article、Dept、Level 等) - 系统共享表不继承此类,直接继承 BaseService(如 Menu、Dict、Config 等) --- ## 模型层(Eloquent Model + Concerns Trait) **文件**:`app/Models/*.php` + `app/Models/Concerns/*.php` ### 设计说明 本项目**没有单一模型基类**,模型直接继承 Eloquent `Model`,通过组合两个 Trait 获得横切能力: 1. **软删除**:`SoftDeletesByFlag`(基于 `is_delete` 字段,不使用 Laravel 内置 `SoftDeletes` 的 `deleted_at` 机制) 2. **审计字段自动写入**:`HasAuditColumns`(`create_user` / `create_time` / `update_user` / `update_time`) ### 软删除机制 ```text ┌─────────────────────────────────────────────────────────┐ │ 软删除机制 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 全局查询范围 SoftDeleteFlagScope: │ │ 所有查询自动叠加 WHERE is_delete = 0 │ │ → 已删除数据默认不可见 │ │ │ │ 软删除操作: │ │ $model->softDelete() → UPDATE SET is_delete = 1 │ │ Model::batchSoftDelete($ids) → 批量软删除 │ │ │ │ 查询已删除数据: │ │ Model::withoutGlobalScope(SoftDeleteFlagScope::class) │ │ │ └─────────────────────────────────────────────────────────┘ ``` **方法列表:** | 方法 | 类型 | 说明 | |---|---|---| | `softDelete()` | 实例方法 | 设置 `is_delete = 1` | | `batchSoftDelete($ids)` | 静态方法 | 批量软删除(先关闭全局范围再 whereIn 更新) | | `SoftDeleteFlagScope` | 全局范围 | 查询自动追加 `is_delete = 0` | ### 审计字段自动写入 通过模型事件自动填充,**无需手动处理**: | 事件 | 自动填充 | 说明 | |---|---|---| | `creating`(创建前) | `create_user`、`create_time` | 调用方已显式传入则不覆盖(`??` 逻辑) | | `updating`(更新前) | `update_user`、`update_time` | 每次强制覆盖 | 用户来源:`Auth::username()`(当前登录用户,来自 `auth.jwt` 中间件注入的 `userInfo`)。无登录用户(如队列/命令行)时用户字段保留空值,时间字段仍写入。 ### 数据库字段规范 所有表建议包含以下公共字段: | 字段名 | 类型 | 说明 | |---|---|---| | `id` | int | 主键,自增 | | `is_delete` | tinyint(1) | 软删除标记,0=正常 1=已删除 | | `create_user` | varchar(50) | 创建人 | | `create_time` | datetime | 创建时间 | | `update_user` | varchar(50) | 更新人 | | `update_time` | datetime | 更新时间 | ### 使用示例 ```php // 模型定义 — 最简形式 class Example extends Model { use HasAuditColumns, SoftDeletesByFlag; /** * 数据表名(不含前缀) * * @var string */ protected $table = 'example'; protected $fillable = ['name', 'avatar', 'type', 'status', 'sort']; } // 软删除 $example = Example::find(1); $example->softDelete(); // UPDATE laravel_example SET is_delete = 1 WHERE id = 1 // 批量软删除 Example::batchSoftDelete([1, 2, 3]); // 查询已删除数据(临时关闭全局范围) $all = Example::withoutGlobalScope(SoftDeleteFlagScope::class)->get(); // 审计字段自动写入(无需手动处理) Example::create(['name' => '测试']); // 自动填充: create_user='admin', create_time='2026-10-02 12:00:00' ``` --- ## BaseRequest(表单请求基类) **文件**:`app/Http/Requests/BaseRequest.php` | 能力 | 说明 | |---|---| | `authorize()` | 统一返回 true(鉴权由中间件与 `#[Permission]` 负责) | | `failedValidation()` | 统一返回 `code=1` + 字段错误明细的 JSON | 详见 [3.4 FormRequest 验证器](/zh/guide/validator.html)。 --- ## 基类选择指南 | 场景 | 继承 | 原因 | |---|---|---| | 系统级共享数据(菜单、字典、配置) | `BaseService` | 不需要租户隔离 | | 租户业务数据(用户、角色、文章、部门、职级) | `BaseTenantService` | 自动 `tenant_id` 隔离 | | 所有数据模型 | `Model` + 双 Trait | 软删除 + 审计字段 | | 所有控制器 | `Controller` | 统一响应 + 通用 CRUD | | 所有表单请求 | `BaseRequest` | 统一失败响应 + 键名转换 | :::tip 快速判断 问自己:这个模块的数据是否需要按租户隔离? - **是** → 继承 `BaseTenantService` - **否** → 继承 `BaseService` 文件/富文本/数据权限等能力按需 `use` 对应 Trait,未启用时占位方法自动空操作。 :::