Skip to content

7.4 数据导入导出 ​

概述

ExcelService 基于 PhpSpreadsheet 封装导入导出能力,支持普通导入导出和分批导出(大数据量)。导入支持表头映射,导出支持枚举显示名自动翻译。

导入流程 ​

完整流程图 ​

text
前端上传 Excel 文件
    │
    ▼
Controller::import()
    │ 校验文件扩展名(xls/xlsx)
    ▼
ExcelService::importWithMap($filePath, $headerMap)
    │ 读取 Excel → 按表头映射转为关联数组
    ▼
Logic::import()
    │
    ├─ 枚举字段反向映射(文字 → 数字)
    │   DictService::getValue('gender', '男') → '1'
    │
    ├─ 逐行调用 $this->add($row)
    │   ├─ 成功 → 计数 +1
    │   └─ 失败 → 收集错误(行号 + 原因)
    │
    ▼
返回 { count: 成功数, errors: [失败行列表] }

Logic 层实现 ​

php
// ExampleLogic::import()

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

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

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

    // 4. 逐行导入
    $count = 0;
    $errors = [];
    foreach ($data as $index => $row) {
        try {
            $this->add($row);  // 调用 BaseLogic::add(),触发 beforeAdd/validateData/afterAdd
            $count++;
        } catch (\Exception $e) {
            $errors[] = [
                'row'    => $index + 2,  // +2 因为第1行是表头,索引从0开始
                'reason' => $e->getMessage(),
            ];
        }
    }

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

Controller 层实现 ​

php
// ExampleController::import()

#[Log('案例演示-导入数据', Log::TYPE_IMPORT)]
#[Permission('sys:example:import', '导入案例演示')]
public function import(): Json
{
    $file = $this->request->file('file');
    if (!$file) {
        return $this->fail('请选择文件');
    }

    $ext = strtolower($file->getOriginalExtension());
    if (!in_array($ext, ['xls', 'xlsx'])) {
        return $this->fail('仅支持 xls、xlsx 格式的文件');
    }

    try {
        $result = $this->logic->import($file->getRealPath());

        $msg = empty($result['errors'])
            ? "成功导入 {$result['count']} 条数据"
            : "成功导入 {$result['count']} 条,失败 " . count($result['errors']) . " 条";

        return $this->success($result, $msg);
    } catch (\Exception $e) {
        return $this->fail('导入失败: ' . $e->getMessage());
    }
}

导入响应示例 ​

全部成功:

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

部分失败:

json
{
    "code": 0,
    "msg": "成功导入 8 条,失败 2 条",
    "data": {
        "count": 8,
        "errors": [
            { "row": 3, "reason": "标题不能为空" },
            { "row": 7, "reason": "编码已存在" }
        ]
    }
}

导出流程 ​

分批导出(推荐) ​

php
// ExampleLogic::export()

public function export(array $params = []): string
{
    // 1. 定义导出表头:字段名 → Excel 表头
    //    枚举字段使用 {字段名}Text(自动翻译后的字段)
    $headers = [
        'name'         => '案例名称',
        'typeText'     => '案例类型',      // 枚举显示名
        'statusText'   => '案例状态',      // 枚举显示名
        'sort'         => '案例排序',
        'create_time'  => '创建时间',
    ];

    // 2. 分批导出(每批 500 条)
    return ExcelService::chunkExport($headers, function (int $page) use ($params) {
        $chunk = [];
        $this->chunkList($params, function (array $data) use (&$chunk) {
            $chunk = $data;
        }, 500);
        return $chunk;
    }, '案例演示数据.xlsx', 500);
}

Controller 层实现 ​

php
// ExampleController::export()

#[Log('案例演示-导出数据', Log::TYPE_EXPORT)]
#[Permission('sys:example:export', '导出案例演示')]
public function export(): Json
{
    try {
        $filePath = $this->logic->export($this->getParams());
        return $this->success($filePath, '导出成功');
    } catch (\Exception $e) {
        return $this->fail('导出失败: ' . $e->getMessage());
    }
}

一次性导出(小数据量) ​

php
// 适用于数据量较小(< 1000 条)的场景
$data = $this->allList($params);
return ExcelService::export($data, $headers, '导出数据.xlsx');

ExcelService 方法清单 ​

方法说明参数返回值
import($filePath, $sheetIndex, $startRow)原始导入文件路径, Sheet索引, 起始行array 二维数组(数字下标)
importWithMap($filePath, $headerMap)映射导入文件路径, 表头映射array 关联数组列表
export($data, $headers, $fileName)一次性导出数据, 表头映射, 文件名string 文件 URL
chunkExport($headers, $callback, $fileName, $pageSize)分批导出表头映射, 回调函数, 文件名, 每批数量string 文件 URL

importWithMap 参数详解 ​

php
ExcelService::importWithMap($filePath, $headerMap);
参数类型说明
$filePathstringExcel 文件的绝对路径
$headerMaparray[Excel表头 => 数据库字段] 映射

表头映射示例:

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

chunkExport 参数详解 ​

php
ExcelService::chunkExport($headers, $callback, $fileName, $pageSize);
参数类型说明
$headersarray[字段名 => Excel表头] 映射
$callbackcallable分批回调,接收 $page 参数,返回该批数据
$fileNamestring导出文件名
$pageSizeint每批数据量(默认 500)

枚举字段处理 ​

导入时:文字 → 值 ​

php
// Excel 中"男" → 数据库中 1
foreach ($data as &$row) {
    if (!empty($row['gender']) && !is_numeric($row['gender'])) {
        $row['gender'] = DictService::getValue('gender', $row['gender']);
    }
}

导出时:值 → 文字 ​

php
// 配置 serializeMaps 后自动翻译

/**
 * 枚举显示名映射
 *
 * @var array
 */
protected array $serializeMaps = [
    'gender' => 'gender',
    'status' => 'example_status',
];

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

完整示例:用户导入导出 ​

导入 ​

php
// UserLogic::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'] = DictService::getValue('gender', $row['gender']);
        }
        if (!empty($row['status']) && !is_numeric($row['status'])) {
            $row['status'] = DictService::getValue('user_status', $row['status']);
        }
    }
    unset($row);

    $count = 0;
    $errors = [];
    foreach ($data as $index => $row) {
        try {
            $this->add($row);
            $count++;
        } catch (\Exception $e) {
            $errors[] = [
                'row'      => $index + 2,
                'username' => $row['username'] ?? '',
                'reason'   => $e->getMessage(),
            ];
        }
    }

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

导出 ​

php
// UserLogic::export()

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

    // 预加载名称映射(避免 N+1 查询)
    $levelMap = Level::getNameMap();
    $positionMap = Position::getNameMap();
    $deptMap = Dept::getNameMap();

    return ExcelService::chunkExport($headers, function (int $page) use ($params, $levelMap, $positionMap, $deptMap) {
        $chunk = [];
        $this->chunkList($params, function (array $data) use (&$chunk, $levelMap, $positionMap, $deptMap) {
            foreach ($data as &$item) {
                $item['deptName'] = $deptMap[$item['dept_id']] ?? '';
                $item['levelName'] = $levelMap[$item['level_id']] ?? '';
                $item['positionName'] = $positionMap[$item['position_id']] ?? '';
            }
            unset($item);
            $chunk = $data;
        }, 500);
        return $chunk;
    }, '用户数据.xlsx', 500);
}

前端集成 ​

导入 ​

vue
<template>
  <el-upload
    :action="'/api/example/import'"
    :headers="{ Authorization: `Bearer ${token}` }"
    :on-success="handleImportSuccess"
    :on-error="handleImportError"
    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} 条`);
      // 可以展示错误详情
    }
    refreshTable(); // 刷新列表
  }
};
</script>

导出 ​

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

性能建议 ​

场景方式说明
数据量 < 1000export() 一次性导出简单直接
数据量 ≥ 1000chunkExport() 分批导出避免内存溢出
导入大量数据逐行调用 add()单行失败不影响其他行
关联字段导出预加载名称映射避免 N+1 查询

常见问题 ​

问题 1:导入提示"文件格式不正确" ​

原因: 文件扩展名不是 .xls 或 .xlsx。

解决: Controller 中校验扩展名,前端 accept=".xls,.xlsx" 限制文件类型。

问题 2:导入时中文乱码 ​

原因: Excel 文件编码问题(旧版 .xls 可能是 GBK 编码)。

解决: PhpSpreadsheet 自动处理编码,通常不需要手动处理。如仍有问题,检查 Excel 文件是否损坏。

问题 3:导出内存溢出 ​

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

解决: 改用 chunkExport() 分批导出,每批 500 条。

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

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

解决: 确保 Logic 中配置了 serializeMaps,导出表头使用 {字段名}Text。

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