21 KiB
4.6 核心基类体系
:::tip 概述
项目有四个核心基类/骨架——Controller、BaseService、BaseTenantService、模型 Concerns Trait——构成"控制器 → 服务 → 模型"三层架构的骨架。所有业务模块都必须继承对应的基类,通过属性配置 + 生命周期钩子实现声明式 CRUD,子类只需声明"做什么"而不用写"怎么做"。
:::
基类继承关系
:::warning 继承规则
- 系统共享模块(菜单、字典、配置等)继承
BaseService - 租户隔离模块(用户、角色、文章、部门、职级等)继承
BaseTenantService - 所有模型继承 Eloquent
Model并组合SoftDeletesByFlag+HasAuditColumnsTrait - 所有控制器继承
App\Http\Controllers\Controller - 所有请求类继承
BaseRequest:::
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
请求处理全链路
理解基类体系的关键是理解一次请求如何流经各层:
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等方法捕获转为失败响应
方法分类
┌─────────────────────────────────────────────────────────────┐
│ 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最小为 1pageSize限制在 1~100,默认 20(防止一次拉取过多数据)
前端传参示例:GET /api/example/page?pageNo=1&pageSize=10&name=测试&status=1
子类实现示例
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)
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 验证器);键名转换由 Service 写入入口(doAdd/doUpdate)统一完成。Service 只做业务规则(唯一性、状态依赖)。 :::
修改流程(update)
beforeUpdate($id, $data) ← 钩子
│
▼
checkUnique($data, $excludeId) ← 唯一性校验(排除自身)
│
▼
DB::transaction {
Model::find($id)->update($data) ← 更新(HasAuditColumns 强制刷新审计字段)
afterUpdate($id, $data) ← 钩子
}
删除流程(delete)
beforeDelete($id) ← 钩子:抛异常可拦截(如存在关联数据)
│
▼
DB::transaction {
is_delete = 1 ← 软删除(SoftDeletesByFlag)
afterDelete($id)
}
查询流程(pageList / allList)
buildQuery($model) ← 构建查询条件(额外条件 + LIKE + 精确匹配
│ + 数据权限 + 租户隔离,camel→snake 转换参数)
▼
applyOrderBy($model) ← 排序(前端 orderField/orderType 优先,
│ 白名单校验,否则用 pageOrderBy 配置)
▼
$page / select ← 分页 / 全量查询
│
▼
processAfterRead 处理链
├─ processFileFieldsOnReadList ← 文件字段补全域名(HasFileProcessing 启用时)
├─ processSerializeMapsList ← 字典显示名补全(status → statusText)
└─ filterFields ← 字段过滤(fields 参数指定返回字段)
│
▼
afterPageList(&$records) ← 钩子:批量补充关联数据(引用传递)
唯一性校验(uniqueFields,HasUniqueCheck)
// 全局唯一
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',子类自动获得租户隔离:
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 请求处理链路)。
数据权限
BaseTenantService 默认未启用数据权限,子类声明字段后 HasDataScope 才会叠加过滤(摘自 app/Services/UserService.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 获得横切能力:
- 软删除:
SoftDeletesByFlag(基于is_delete字段,不使用 Laravel 内置SoftDeletes的deleted_at机制) - 审计字段自动写入:
HasAuditColumns(create_user/create_time/update_user/update_time)
软删除机制
┌─────────────────────────────────────────────────────────┐
│ 软删除机制 │
├─────────────────────────────────────────────────────────┤
│ │
│ 全局查询范围 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 | 更新时间 |
使用示例
// 模型定义 — 最简形式
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 |
基类选择指南
| 场景 | 继承 | 原因 |
|---|---|---|
| 系统级共享数据(菜单、字典、配置) | BaseService |
不需要租户隔离 |
| 租户业务数据(用户、角色、文章、部门、职级) | BaseTenantService |
自动 tenant_id 隔离 |
| 所有数据模型 | Model + 双 Trait |
软删除 + 审计字段 |
| 所有控制器 | Controller |
统一响应 + 通用 CRUD |
| 所有表单请求 | BaseRequest |
统一失败响应 + 键名转换 |
:::tip 快速判断 问自己:这个模块的数据是否需要按租户隔离?
- 是 → 继承
BaseTenantService - 否 → 继承
BaseService
文件/富文本/数据权限等能力按需 use 对应 Trait,未启用时占位方法自动空操作。
:::