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

21 KiB
Raw Blame History

4.6 核心基类体系

:::tip 概述 项目有四个核心基类/骨架——Controller、BaseService、BaseTenantService、模型 Concerns Trait——构成"控制器 → 服务 → 模型"三层架构的骨架。所有业务模块都必须继承对应的基类,通过属性配置 + 生命周期钩子实现声明式 CRUD,子类只需声明"做什么"而不用写"怎么做"。 :::

基类继承关系

:::warning 继承规则

  • 系统共享模块(菜单、字典、配置等)继承 BaseService
  • 租户隔离模块(用户、角色、文章、部门、职级等)继承 BaseTenantService
  • 所有模型继承 Eloquent Model 并组合 SoftDeletesByFlag + HasAuditColumns Trait
  • 所有控制器继承 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 最小为 1
  • pageSize 限制在 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 获得横切能力:

  1. 软删除:SoftDeletesByFlag(基于 is_delete 字段,不使用 Laravel 内置 SoftDeletes 的 deleted_at 机制)
  2. 审计字段自动写入: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

详见 3.4 FormRequest 验证器。


基类选择指南

场景 继承 原因
系统级共享数据(菜单、字典、配置) BaseService 不需要租户隔离
租户业务数据(用户、角色、文章、部门、职级) BaseTenantService 自动 tenant_id 隔离
所有数据模型 Model + 双 Trait 软删除 + 审计字段
所有控制器 Controller 统一响应 + 通用 CRUD
所有表单请求 BaseRequest 统一失败响应 + 键名转换

:::tip 快速判断 问自己:这个模块的数据是否需要按租户隔离?

  • 是 → 继承 BaseTenantService
  • 否 → 继承 BaseService

文件/富文本/数据权限等能力按需 use 对应 Trait,未启用时占位方法自动空操作。 :::