Files
rxthinkcmf-wiki/RXThinkCMF_ThinkPhp8_AntdVue/zh/tools/excel.md
T
2026-09-29 10:14:04 +08:00

452 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<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
};
```
## 性能建议
| 场景 | 方式 | 说明 |
|------|------|------|
| 数据量 < 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`。