# 7.4 数据导入导出 :::tip 概述 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); ``` | 参数 | 类型 | 说明 | |------|------|------| | `$filePath` | string | Excel 文件的绝对路径 | | `$headerMap` | array | `[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); ``` | 参数 | 类型 | 说明 | |------|------|------| | `$headers` | array | `[字段名 => Excel表头]` 映射 | | `$callback` | callable | 分批回调,接收 `$page` 参数,返回该批数据 | | `$fileName` | string | 导出文件名 | | `$pageSize` | int | 每批数据量(默认 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 ``` ### 导出 ```typescript // 导出接口返回文件 URL,前端直接下载 const handleExport = async () => { const res = await exportExample(params); window.open(res); // res 是文件 URL }; ``` ## 性能建议 | 场景 | 方式 | 说明 | |------|------|------| | 数据量 < 1000 | `export()` 一次性导出 | 简单直接 | | 数据量 ≥ 1000 | `chunkExport()` 分批导出 | 避免内存溢出 | | 导入大量数据 | 逐行调用 `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`。