Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_AntdVue/zh/tools/excel.md
T
2026-10-08 17:50:29 +08:00

12 KiB
Raw Blame History

7.4 数据导入导出

:::tip 概述 ExcelService 基于 PhpSpreadsheet(phpoffice/phpspreadsheet ^5.10)封装导入导出能力:importWithMap 表头映射导入、export 一次性导出、chunkExport 分批导出(大数据量)。导入支持枚举文字反查,导出支持字典显示名自动翻译。 :::

导入流程

完整流程图

前端上传 Excel 文件
    │
    ▼
Controller::_import($service)          ← 基类快捷方法
    │ 1. 文件存在校验("请选择文件")
    │ 2. 扩展名校验(仅 xls / xlsx)
    │ 3. 大小校验(10MB 上限)
    │ 4. MIME 类型真实检测(finfo)
    ▼
Service::import($filePath)
    │
    ├─ ExcelService::importWithMap($filePath, $headerMap)
    │   读取活动工作表 → 第一行表头按 headerMap 映射为字段名
    │   第二行起逐行取值,跳过整行为空的行
    │
    ├─ 枚举字段反向映射(文字 → 数字)
    │   DictCacheService::getValue('example_status', '在用') → '1'
    │
    ├─ 逐行调用 $this->add($row)
    │   ├─ 成功 → 计数 +1
    │   └─ 失败 → 收集错误(行号 + 字段 + 原因),不中断整体
    │
    ▼
返回 { count: 成功数, errors: [失败行列表] }

Service 层实现(来自项目实际代码)

// ExampleService::import()

public function import(string $filePath): array
{
    // 1. 定义表头映射:Excel 表头 → 数据库字段
    $headerMap = [
        '名称' => 'name',
        '状态' => 'status',
        '排序' => 'sort',
    ];

    // 2. 解析 Excel 为关联数组
    $data = ExcelService::importWithMap($filePath, $headerMap);

    // 3. 枚举字段反向映射:文字 → 数字
    foreach ($data as &$row) {
        if (! empty($row['status']) && ! is_numeric($row['status'])) {
            $row['status'] = DictCacheService::getValue('example_status', $row['status']);
        }
    }
    unset($row);

    // 4. 逐行导入
    $count = 0;
    $errors = [];
    foreach ($data as $index => $row) {
        try {
            $this->add($row);   // 走完整 add 流程(钩子 / 唯一校验 / 事务)
            $count++;
        } catch (\Exception $e) {
            $errors[] = [
                'row' => $index + 2,   // +2 因为第 1 行是表头,索引从 0 开始
                'name' => $row['name'] ?? '',
                'reason' => $e->getMessage(),
            ];
        }
    }

    return ['count' => $count, 'errors' => $errors];
}

Controller 层实现(基类快捷方法)

// 控制器无需手写文件校验 —— _import() 已内置
#[OperationLog('示例管理-导入数据', OperationLog::TYPE_IMPORT, '通过Excel导入示例数据')]
#[Permission('sys:example:import', '导入示例')]
public function import(): JsonResponse
{
    return $this->_import($this->exampleService);
}

Controller::_import() 内置校验顺序(见 3.6 控制器开发):

  1. 文件是否存在("请选择文件")
  2. 扩展名白名单(仅 xls、xlsx)
  3. 大小上限(10MB)
  4. MIME 真实检测(finfo,防伪造扩展名)
  5. 调用 Service::import(),按 errors 是否为空组装成功/部分失败消息

导入响应示例

全部成功:

{
    "code": 0,
    "msg": "成功导入 10 条数据",
    "data": { "count": 10, "errors": [] }
}

部分失败:

{
    "code": 0,
    "msg": "成功导入 8 条,失败 2 条",
    "data": {
        "count": 8,
        "errors": [
            { "row": 3, "name": "示例A", "reason": "名称已存在" },
            { "row": 7, "name": "", "reason": "案例名称不能为空" }
        ]
    }
}

导出流程

一次性导出(中小数据量,来自项目实际代码)

// ExampleService::export()

public function export(array $params = []): string
{
    $list = $this->allList($params);

    $headers = [
        'name' => '名称',
        'statusText' => '状态',      // 枚举显示名(serializeMaps 已补全)
        'sort' => '排序',
        'create_user' => '创建人',
        'create_time' => '创建时间',
    ];

    return ExcelService::export($list, $headers, '示例数据.xlsx');
}

分批导出(大数据量)

// ExcelService::chunkExport —— 数据由回调按页提供,取空即终止
return ExcelService::chunkExport(
    $headers,
    function (int $page) use ($params): array {
        // 按页取数(示例:每页 500 条,返回空数组即导出结束)
        $list = $this->pageList(array_merge($params, ['pageNo' => $page, 'pageSize' => 500]));

        return $list['records'] ?? [];
    },
    '导出数据.xlsx'
);

:::tip 终止条件 chunkExport 的终止条件是 $chunkProvider 返回空数组——不依赖条数与页大小的关系,过滤后数量不固定也能正确处理。 :::

Controller 层实现

#[OperationLog('示例管理-导出数据', OperationLog::TYPE_EXPORT)]
#[Permission('sys:example:export', '导出示例')]
public function export(): JsonResponse
{
    return $this->_export($this->exampleService);   // 返回文件地址
}

导出文件

  • 存放位置:{upload_dir}/temp/{Ymd}/(upload_dir 取 config/file.php,支持绝对路径)
  • 文件名:原文件名_YmdHis.xlsx,同秒重复导出自动追加 _1、_2 序号,避免同名文件被占用
  • 返回值:file.domain_url 拼接的完整地址,接口 data 直接返回该地址字符串,前端 window.open(res) 即可下载

导出响应示例:

{
    "code": 0,
    "ok": true,
    "msg": "导出成功",
    "data": "http://file.laravel13.elevue/temp/20261006/用户数据_20261006050143.xlsx"
}

ExcelService 方法清单

方法 说明 返回值
importWithMap($filePath, $headerMap) 表头映射导入(未声明的列忽略、缺少的列补空串、跳过空行) array [字段名 => 值, ...]
export($data, $headers, $fileName) 一次性导出 string 文件 URL
chunkExport($headers, $chunkProvider, $fileName) 分批导出(回调 fn(int $page): array,返回空数组终止) string 文件 URL

importWithMap 参数详解

参数 类型 说明
$filePath string Excel 文件的绝对路径
$headerMap array ['Excel表头' => '数据库字段'] 映射

表头映射示例:

$headerMap = [
    '用户名' => 'username',    // Excel 中"用户名"列 → $row['username']
    '姓名'   => 'realname',    // Excel 中"姓名"列 → $row['realname']
    '性别'   => 'gender',      // Excel 中"性别"列 → $row['gender']
    '手机号' => 'mobile',      // Excel 中"手机号"列 → $row['mobile']
];

chunkExport 参数详解

参数 类型 说明
$headers array ['字段名' => '中文表头'] 映射
$chunkProvider callable 分页取数回调 fn(int $page): array,返回空数组终止
$fileName string 导出文件名(默认 export.xlsx)

枚举字段处理

导入时:文字 → 值

// Excel 中"在用" → 数据库中 1
foreach ($data as &$row) {
    if (! empty($row['status']) && ! is_numeric($row['status'])) {
        $row['status'] = DictCacheService::getValue('example_status', $row['status']);
    }
}

导出时:值 → 文字

// 配置 serializeMaps 后自动翻译(HasSerializeMaps)
protected array $serializeMaps = [
    'gender' => 'gender',
    'status' => 'example_status',
];

// 导出表头:使用 Text 后缀字段,自动翻译枚举值
$headers = [
    'name'       => '姓名',
    'genderText' => '性别',
    'statusText' => '状态',
];

完整示例:用户导入导出

导入

// UserService::import()(形态示例)
public function import(string $filePath): array
{
    $headerMap = [
        '用户名' => 'username',
        '姓名'   => 'realname',
        '性别'   => 'gender',
        '手机号' => 'mobile',
        '邮箱'   => 'email',
        '状态'   => 'status',
    ];

    $data = ExcelService::importWithMap($filePath, $headerMap);

    // 枚举反向映射
    foreach ($data as &$row) {
        if (! empty($row['gender']) && ! is_numeric($row['gender'])) {
            $row['gender'] = DictCacheService::getValue('gender', $row['gender']);
        }
        if (! empty($row['status']) && ! is_numeric($row['status'])) {
            $row['status'] = DictCacheService::getValue('user_status', $row['status']);
        }
    }
    unset($row);

    // 逐行 add(),失败明细汇总(同 ExampleService::import 模式)
    // ...
}

导出

// UserService::export()(形态示例)
public function export(array $params = []): string
{
    $headers = [
        'username'     => '用户名',
        'realname'     => '姓名',
        'genderText'   => '性别',
        'mobile'       => '手机号',
        'email'        => '邮箱',
        'statusText'   => '状态',
        'deptName'     => '部门',
        'levelName'    => '职级',
        'positionName' => '岗位',
        'create_time'  => '创建时间',
    ];

    // afterPageList 已批量补全 deptName/levelName/positionName
    return ExcelService::export($this->allList($params), $headers, '用户数据.xlsx');
}

前端集成

导入

<template>
  <el-upload
    :action="'/api/example/import'"
    :headers="{ Authorization: `Bearer ${token}` }"
    :on-success="handleImportSuccess"
    accept=".xls,.xlsx"
  >
    <el-button type="primary">导入</el-button>
  </el-upload>
</template>

<script setup>
const handleImportSuccess = (response) => {
  if (response.code === 0) {
    const { count, errors } = response.data;
    if (errors.length === 0) {
      ElMessage.success(`成功导入 ${count} 条数据`);
    } else {
      ElMessage.warning(`成功 ${count} 条,失败 ${errors.length} 条`);
      // 可展开 errors 查看行号与原因
    }
    refreshTable();
  }
};
</script>

导出

// 导出接口返回文件 URL,前端直接下载
const handleExport = async () => {
  const res = await exampleExport(params);   // 文件地址
  window.open(res);
};

性能建议

场景 方式 说明
数据量 < 1000 export() 一次性导出 简单直接
数据量 ≥ 1000 chunkExport() 分批导出 回调按页取数,避免内存溢出
导入大量数据 逐行调用 add() 单行失败不影响其他行(事务隔离)
关联字段导出 依赖 afterPageList 补全名称 批量映射避免 N+1 查询
PHP 内存 memory_limit=256M Docker 镜像已配置

常见问题

问题 1:导入提示"仅支持 xls、xlsx 格式的文件"

原因: 文件扩展名不是 .xls 或 .xlsx(如 csv、xlsx.zip)。

解决: 前端 accept=".xls,.xlsx" 限制文件类型;_import() 会依次校验扩展名、大小(10MB)、MIME。

问题 2:导入时中文乱码

原因: 旧版 .xls 可能是 GBK 编码。

解决: PhpSpreadsheet 自动处理编码,通常无需手动处理;优先使用 .xlsx 格式。

问题 3:导出内存溢出

原因: 使用 export() 一次性导出大数据量。

解决: 改用 chunkExport() 分批导出(回调按页取数,取空自动终止)。

问题 4:枚举字段导出为数字

原因: 导出表头未使用 Text 后缀字段。

解决: 确认 Service 中配置了 serializeMaps(且 use HasSerializeMaps),导出表头使用 {字段名}Text。

问题 5:导入成功但下拉框值显示异常

原因: Excel 中的枚举文字在字典里不存在。

解决: DictCacheService::getValue() 反查不到会保留原值——检查字典项标签与 Excel 文字是否完全一致(含空格)。