Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/zh/architecture/base-classes.md
T
2026-10-08 17:50:29 +08:00

517 lines
21 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.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,未启用时占位方法自动空操作。
:::