Skip to content

3.5 逻辑层开发 ​

概述

逻辑层是业务核心,通过"属性配置 + 生命周期钩子"实现声明式 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',
    ];
    // ... 其他属性配置
}

属性配置即全部

子类只需声明以上属性,无需编写任何 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);
    }
}

属性配置速查 ​

属性类型说明本例配置
modelClassstring关联模型类名(必须)Example::class
validateClassstring验证器类名(自动参数校验)ExampleValidate::class
fileFieldsarray单文件字段列表['avatar']
multiFileFieldsarray多文件字段列表(逗号分隔的多个URL)['images']
fileSaveDirstring文件保存子目录'example'
pageLikeFieldsarrayLIKE 模糊匹配字段['name']
pageEqFieldsarray精确匹配字段['type', 'status']
pageOrderByarray默认排序规则['field'=>'sort','type'=>'asc']
uniqueFieldsarray唯一性校验字段[](本例未配置)
serializeMapsarray枚举显示名映射['type'=>'example_type', 'status'=>'example_status']
contentFieldsarray富文本字段[](本例未配置)

生命周期钩子 ​

钩子时机用途
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;
    }
}

与 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;
    }
}

小蚂蚁云团队 · 提供技术支持