# 3.5 逻辑层开发 :::tip 概述 逻辑层是业务核心,通过"属性配置 + 生命周期钩子"实现声明式 CRUD。子类只需配置属性和按需重写钩子,即可获得完整的增删改查能力。 ::: ## 逻辑层文件位置 ```text app/logic/ExampleLogic.php ``` ## 完整代码(来自项目实际代码) ```php '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; } } ```