Files
easygoadmin-wiki/EasyGoAdmin_Iris_Layui/zh/guide/logic.md
T
2026-10-20 21:19:44 +08:00

319 lines
11 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.
# 3.5 逻辑层开发
:::tip 概述
逻辑层是业务核心,通过"属性配置 + 生命周期钩子"实现声明式 CRUD。子类只需配置属性和按需重写钩子,即可获得完整的增删改查能力。
:::
## 逻辑层文件位置
```text
app/logic/ExampleLogic.php
```
## 完整代码(来自项目实际代码)
```php
<?php
declare(strict_types=1);
namespace app\logic;
use app\BaseLogic; // 系统共享模块基类
use app\model\Example; // 案例演示模型
/**
* 案例演示业务逻辑类
*
* 继承 BaseLogic,通过属性配置声明式地定义案例演示模块的通用行为,
* 并按需生成导入、导出方法。
*
* 配置覆盖:文件字段、查询条件、排序、唯一性校验、枚举显示名映射。
*/
class ExampleLogic extends BaseLogic
{
/**
* 关联的模型类
*
* 指定该逻辑层操作的数据模型为 Example。
* BaseLogic 的所有 CRUD 方法都基于此模型。
*
* @var string
*/
protected string $modelClass = Example::class;
/**
* 验证器类名
*
* 设置后,add/update 时自动调用对应验证器的 add/update 场景进行参数校验。
* 校验失败抛出 ValidateException,由 ExceptionHandle 统一返回。
*
* @var string
*/
protected string $validateClass = \app\validate\ExampleValidate::class;
/**
* 文件字段
*
* 新增/修改时迁移临时文件并去掉域名,
* 查询时自动补全完整访问 URL。
*
* @var array
*/
protected array $fileFields = ['avatar'];
/**
* 多文件字段(逗号分隔的多个URL)
*
* 与 fileFields 不同,这些字段的值是逗号分隔的多个文件URL,
* 新增/修改时逐个迁移临时文件并去掉域名,
* 查询时逐个补全完整访问 URL。
*
* @var array
*/
protected array $multiFileFields = ['images'];
/**
* 文件保存子目录
*
* 文件字段上传后保存到该子目录下。
*
* @var string
*/
protected string $fileSaveDir = 'example';
/**
* LIKE 模糊匹配字段
*
* 分页查询时,前端传入的 name 参数会自动使用 LIKE 查询。
* 例如:GET /api/example/page?name=测试
* 生成 SQL:WHERE name LIKE '%测试%'
*
* @var array
*/
protected array $pageLikeFields = ['name'];
/**
* 精确匹配字段
*
* 分页查询时,前端传入的 type 和 status 参数
* 会自动使用 = 查询。
* 例如:GET /api/example/page?type=1&status=1
* 生成 SQL:WHERE type = 1 AND status = 1
*
* @var array
*/
protected array $pageEqFields = ['type', 'status'];
/**
* 默认排序规则
*
* 支持两种格式:
* - 单字段:['field' => 'sort', 'type' => 'asc']
* - 多字段:[['field' => 'sort', 'type' => 'asc'], ['field' => 'id', 'type' => 'desc']]
*
* 前端可通过 orderField/orderType 参数覆盖。
*
* @var array
*/
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
/**
* 枚举显示名映射
*
* 查询数据时自动根据字典编码翻译值为描述文本,补 {字段名}Text 字段。
* 例如:type=1 时,typeText='类型一'(从字典 example_type 读取)。
* status=1 时,statusText='启用'(从字典 example_status 读取)。
*
* @var array
*/
protected array $serializeMaps = [
'type' => 'example_type',
'status' => 'example_status',
];
// ... 其他属性配置
}
```
:::tip 属性配置即全部
子类只需声明以上属性,无需编写任何 CRUD 代码,即可获得完整的分页查询、详情、新增、修改、删除能力。这是 BaseLogic 模板方法模式的核心优势。
:::
## 扩展方法(导入导出示例)
```php
/**
* 导入案例演示数据
*
* 处理流程:
* 1. 按表单字段生成"中文表头 => 字段名"的映射,跳过图片与富文本字段;
* 2. 调用 ExcelService::importWithMap 解析文件为关联数组;
* 3. 枚举字段反向映射:把文字通过 DictService 还原为数值;
* 4. 逐行调用 add 方法写入,成功计数,失败收集行号与原因;
* 5. 返回成功数与错误列表。
*
* @param string $filePath 文件路径
* @return array ['count' => 成功数, 'errors' => [失败行信息]]
* @throws \Exception 文件解析失败时抛出
*/
public function import(string $filePath): array
{
// 定义表头映射:Excel 表头名 → 数据库字段名
$headerMap = [
'案例名称' => 'name',
'案例类型' => 'type',
'案例状态' => 'status',
'案例排序' => 'sort',
];
// 解析 Excel 文件为关联数组
$data = \app\service\ExcelService::importWithMap($filePath, $headerMap);
// 枚举字段反向映射:文字转数字
// 例如 Excel 中"启用" → 数据库中 1
foreach ($data as &$row) {
if (!empty($row['type']) && !is_numeric($row['type'])) {
$row['type'] = \app\service\DictService::getValue('example_type', $row['type']);
}
if (!empty($row['status']) && !is_numeric($row['status'])) {
$row['status'] = \app\service\DictService::getValue('example_status', $row['status']);
}
}
unset($row);
// 逐行导入
$count = 0;
$errors = [];
foreach ($data as $index => $row) {
try {
$this->add($row); // 调用 BaseLogic::add,触发 beforeAdd/afterAdd 钩子
$count++;
} catch (\Exception $e) {
// 单行失败不影响其它行,收集错误信息
$errors[] = [
'row' => $index + 2, // +2 因为第1行是表头,索引从0开始
'reason' => $e->getMessage(),
];
}
}
return ['count' => $count, 'errors' => $errors];
}
/**
* 导出案例演示数据
*
* 处理流程:
* 1. 按列表字段生成导出表头,跳过图片与富文本字段;
* 带选项的字段使用 {字段名}Text,以导出显示名而非原始值;
* 2. 追加创建时间列;
* 3. 调用 ExcelService::chunkExport 分批导出,每批 500 条,
* 避免一次性加载全部数据导致内存溢出。
*
* @param array $params 查询参数
* @return string 导出文件的 URL
* @throws \Exception 导出失败时抛出
*/
public function export(array $params = []): string
{
// 定义导出表头:字段名 → Excel 表头名
// 带 Text 后缀的字段取枚举显示名(如 typeText='类型一')
$headers = [
'name' => '案例名称',
'typeText' => '案例类型', // 枚举显示名
'statusText' => '案例状态', // 枚举显示名
'sort' => '案例排序',
'create_time' => '创建时间',
];
// 分批导出,每批 500 条
return \app\service\ExcelService::chunkExport($headers, function (int $page) use ($params) {
$chunk = [];
// chunkList 是 BaseLogic 提供的分批查询方法
$this->chunkList($params, function (array $data) use (&$chunk) {
$chunk = $data;
}, 500);
return $chunk;
}, '案例演示数据.xlsx', 500);
}
}
```
## 属性配置速查
| 属性 | 类型 | 说明 | 本例配置 |
|------|------|------|----------|
| `modelClass` | string | 关联模型类名(必须) | `Example::class` |
| `validateClass` | string | 验证器类名(自动参数校验) | `ExampleValidate::class` |
| `fileFields` | array | 单文件字段列表 | `['avatar']` |
| `multiFileFields` | array | 多文件字段列表(逗号分隔的多个URL) | `['images']` |
| `fileSaveDir` | string | 文件保存子目录 | `'example'` |
| `pageLikeFields` | array | LIKE 模糊匹配字段 | `['name']` |
| `pageEqFields` | array | 精确匹配字段 | `['type', 'status']` |
| `pageOrderBy` | array | 默认排序规则 | `['field'=>'sort','type'=>'asc']` |
| `uniqueFields` | array | 唯一性校验字段 | `[]`(本例未配置) |
| `serializeMaps` | array | 枚举显示名映射 | `['type'=>'example_type', 'status'=>'example_status']` |
| `contentFields` | array | 富文本字段 | `[]`(本例未配置) |
## 生命周期钩子
| 钩子 | 时机 | 用途 |
|------|------|------|
| `beforeAdd($data)` | 新增前 | 修改数据、校验、抛异常拦截 |
| `afterAdd($id, $data)` | 新增后 | 保存关联表、发送通知 |
| `beforeUpdate($id, $data)` | 修改前 | 修改数据、校验 |
| `afterUpdate($id, $data)` | 修改后 | 更新关联表 |
| `beforeDelete($id)` | 删除前 | 检查是否有子级、抛异常拦截 |
| `afterDelete($id)` | 删除后 | 清理关联数据 |
| `afterDetail($id, &$data)` | 详情查询后 | 补充关联数据、格式化输出 |
| `afterPageList(&$records, $params)` | 列表查询后 | 批量补充关联数据(分页和全量列表均触发) |
### afterPageList 示例
当列表需要补充关联统计数据时,`afterPageList` 比逐条 `afterDetail` 更高效,只需一次 SQL 查询:
```php
/**
* 列表查询后处理:批量补充评论数
*
* @param array &$records 记录列表(引用传递)
* @param array $params 查询参数
* @return void
*/
protected function afterPageList(array &$records, array $params): void
{
if (empty($records)) return;
// 一次查询获取所有记录的评论数,避免 N+1
$ids = array_column($records, 'id');
$counts = Comment::whereIn('article_id', $ids)
->group('article_id')
->column('count(*)', 'article_id');
foreach ($records as &$record) {
$record['commentCount'] = $counts[$record['id']] ?? 0;
}
}
```
:::tip 与 afterDetail 的区别
- `afterDetail`:单条详情查询后触发,适合补充单条记录的关联数据
- `afterPageList`:列表查询后触发(分页和全量均适用),适合批量补充关联数据,避免 N+1 查询
:::
## 事务处理
```php
public function addWithRelated(array $data): int
{
$this->startTrans(); // 开启事务
try {
$id = $this->add($data);
// 保存关联数据...
$this->commit(); // 提交事务
return $id;
} catch (\Exception $e) {
$this->rollback(); // 回滚事务
throw $e;
}
}
```