# Excel 导入导出 (12_Excel)
最后更新: 2024-09-20
# 📚 概述
DarkM Excel 模块提供了统一的 Excel 导入导出功能,支持多种 Excel 库(EPPlus、NPOI、Aspose),通过抽象接口实现,可以根据需求灵活切换。
源代码位置: DarkM/src/Framework/Excel
# 🏗️ 模块架构
# 完整目录结构
Excel/
├── Excel.Abstractions/
│ ├── IExcelHandler.cs # Excel 处理主接口
│ ├── IExcelExportHandler.cs # Excel 导出接口
│ ├── IExcelImportHandler.cs # Excel 导入接口
│ ├── ExcelHandlerAbstract.cs # Excel 处理器抽象基类
│ ├── ExcelProvider.cs # Excel 提供器枚举
│ └── ExcelConfig.cs # Excel 配置
│
├── Excel.EPPlus/
│ ├── EPPlusExcelHandler.cs # EPPlus Excel 处理器
│ ├── EPPlusExcelExportHandler.cs # EPPlus Excel 导出处理器
│ └── EPPlusExcelImportHandler.cs # EPPlus Excel 导入处理器
│
├── Excel.NPOI/
│ ├── NPOIExcelHandler.cs # NPOI Excel 处理器
│ ├── NPOIExcelExportHandler.cs # NPOI Excel 导出处理器
│ └── NPOIExcelImportHandler.cs # NPOI Excel 导入处理器
│
└── Excel.Integration/
└── ServiceCollectionExtensions.cs # DI 扩展
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 🔧 支持的 Excel 提供器
| 提供器 | 枚举值 | 说明 | 适用场景 |
|---|---|---|---|
| EPPlus | 0 | 开源免费,推荐用于.NET Core | 一般业务场景 |
| NPOI | 1 | 开源,支持.NET Framework 和.NET Core | 兼容性要求高的场景 |
| Aspose | 2 | 商业库,功能最强大 | 企业级应用 |
# 💡 配置说明
# appsettings.json 配置
{
"Excel": {
// Excel 类库:0=EPPlus, 1=NPOI, 2=Aspose
"Provider": 0,
// Excel 操作时产生的临时文件存储根路径
"TempPath": "/tmp/excel"
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 配置项详解
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| Provider | ExcelProvider | 0 | Excel 提供器 |
| TempPath | string | /tmp/excel | 临时文件存储路径 |
# 🔧 核心接口
# 1. IExcelHandler(Excel 处理主接口)
文件位置: Excel.Abstractions/IExcelHandler.cs
public interface IExcelHandler
{
/// <summary>
/// 导出(单 Sheet)
/// </summary>
ExcelExportResultModel Export<T>(ExportModel model, IList<T> entities) where T : class, new();
/// <summary>
/// 导出 Excel(多 Sheet)
/// </summary>
ExcelExportResultModel Export<T>(ExportModel model, IList<T> entities, params IList<object>[] otherSheet) where T : class, new();
/// <summary>
/// 导出 Excel(多 Sheet)
/// </summary>
ExcelExportResultModel Export<T>(ExportModel model, IList<T> entities, params ExportExcelSheet[] otherSheet) where T : class, new();
/// <summary>
/// 导出多 Sheet Excel
/// </summary>
ExcelExportResultModel Export<T>(params ExportExcelSheet[] datas) where T : class, new();
/// <summary>
/// 导入
/// </summary>
ExcelImportResultModel<T> Import<T>(ImportModel model, Stream excelFile) where T : class, new();
/// <summary>
/// 获取 Excel 文件的内容
/// </summary>
IList<string> GetExcelContent(string fileName, Stream stream, int topLine = 0, bool removeDup = true);
/// <summary>
/// 合并 Excel 文件
/// </summary>
void CombineFiles(FileInfo combineFile, params FileInfo[] otherFiles);
/// <summary>
/// 合并 Excel 文件
/// </summary>
ExcelExportResultModel CombineFiles(string fileName, string combineFileUrl, params string[] otherUrls);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 2. ExportModel(导出模型)
public class ExportModel
{
/// <summary>
/// 文件名
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Sheet 名称
/// </summary>
public string SheetName { get; set; }
/// <summary>
/// 列配置(可选,不配置则自动根据实体属性生成)
/// </summary>
public List<ExportColumn> Columns { get; set; }
/// <summary>
/// 是否自动列宽
/// </summary>
public bool AutoColumnWidth { get; set; } = true;
/// <summary>
/// 是否包含表头
/// </summary>
public bool HasHeader { get; set; } = true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 3. ExportExcelSheet(导出工作表)
public class ExportExcelSheet
{
/// <summary>
/// 导出模型
/// </summary>
public ExportModel Model { get; set; }
/// <summary>
/// 数据列表
/// </summary>
public IList<object> Data { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 4. ImportModel(导入模型)
public class ImportModel
{
/// <summary>
/// Sheet 索引(从 0 开始)
/// </summary>
public int SheetIndex { get; set; } = 0;
/// <summary>
/// 是否包含表头
/// </summary>
public bool HasHeader { get; set; } = true;
/// <summary>
/// 从第几行开始读取(0 表示从第一行)
/// </summary>
public int StartRow { get; set; } = 1;
/// <summary>
/// 列映射(Excel 列名 -> 实体属性名)
/// </summary>
public Dictionary<string, string> ColumnMapping { get; set; }
/// <summary>
/// 是否验证数据
/// </summary>
public bool Validate { get; set; } = true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 5. ExcelExportResultModel(导出结果)
public class ExcelExportResultModel
{
/// <summary>
/// 是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 文件路径
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// 文件 URL(用于下载)
/// </summary>
public string FileUrl { get; set; }
/// <summary>
/// 文件大小(字节)
/// </summary>
public long FileSize { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMsg { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 6. ExcelImportResultModel(导入结果)
public class ExcelImportResultModel<T>
{
/// <summary>
/// 是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 导入的数据列表
/// </summary>
public List<T> Data { get; set; }
/// <summary>
/// 导入行数
/// </summary>
public int TotalRows { get; set; }
/// <summary>
/// 成功行数
/// </summary>
public int SuccessRows { get; set; }
/// <summary>
/// 错误行数
/// </summary>
public int ErrorRows { get; set; }
/// <summary>
/// 错误信息列表
/// </summary>
public List<ImportErrorInfo> Errors { get; set; }
/// <summary>
/// 错误信息(汇总)
/// </summary>
public string ErrorMsg { get; set; }
}
public class ImportErrorInfo
{
/// <summary>
/// 行号
/// </summary>
public int RowIndex { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string Error { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 💡 使用示例
# 1. 单 Sheet 导出
using DarkM.Lib.Excel.Abstractions;
using DarkM.Lib.Data.Query;
public class UserService
{
private readonly IExcelHandler _excelHandler;
public UserService(IExcelHandler excelHandler)
{
_excelHandler = excelHandler;
}
public ExcelExportResultModel ExportUsers()
{
var users = _userService.GetAll();
var model = new ExportModel
{
FileName = "用户列表.xlsx",
SheetName = "用户数据"
};
return _excelHandler.Export(model, users);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 2. 多 Sheet 导出
// 方式 1:使用 ExportExcelSheet
var exportResult = _excelHandler.Export(
new ExportExcelSheet
{
Model = new ExportModel
{
FileName = "综合报表.xlsx",
SheetName = "用户数据"
},
Data = users
},
new ExportExcelSheet
{
Model = new ExportModel
{
SheetName = "订单数据"
},
Data = orders
}
);
// 方式 2:直接传入多个数据集
var exportResult = _excelHandler.Export<UserEntity>(
new ExportModel
{
FileName = "综合报表.xlsx",
SheetName = "用户数据"
},
users,
orders, // 第二个 Sheet
products // 第三个 Sheet
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 3. 基本导入
using DarkM.Lib.Excel.Abstractions;
public class UserService
{
private readonly IExcelHandler _excelHandler;
public UserService(IExcelHandler excelHandler)
{
_excelHandler = excelHandler;
}
public async Task<ExcelImportResultModel<UserEntity>> ImportUsers(Stream excelFile)
{
var model = new ImportModel
{
SheetIndex = 0, // 读取第几个 Sheet(从 0 开始)
HasHeader = true, // 是否包含表头
StartRow = 1 // 从第几行开始读取(0 表示从第一行)
};
return await _excelHandler.Import<UserEntity>(model, excelFile);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 4. Controller 导出示例
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using DarkM.Lib.Excel.Abstractions;
using DarkM.Lib.Utils.Core.Result;
[Description("用户管理")]
public class UserController : ModuleController
{
private readonly IUserService _userService;
private readonly IExcelHandler _excelHandler;
public UserController(IUserService userService, IExcelHandler excelHandler)
{
_userService = userService;
_excelHandler = excelHandler;
}
/// <summary>
/// 导出用户列表
/// </summary>
[HttpGet]
[Description("导出用户列表")]
public async Task<IActionResult> Export()
{
var users = await _userService.GetAllAsync();
var model = new ExportModel
{
FileName = $"用户列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx",
SheetName = "用户数据",
AutoColumnWidth = true
};
var result = _excelHandler.Export(model, users);
if (result.Success)
{
return PhysicalFile(
result.FilePath,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
result.FileName
);
}
return Json(ResultModel.Failed(result.ErrorMsg));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 5. Controller 导入示例
using Microsoft.AspNetCore.Mvc;
using DarkM.Lib.Excel.Abstractions;
using DarkM.Lib.Utils.Core.Result;
[Description("用户管理")]
public class UserController : ModuleController
{
private readonly IUserService _userService;
private readonly IExcelHandler _excelHandler;
public UserController(IUserService userService, IExcelHandler excelHandler)
{
_userService = userService;
_excelHandler = excelHandler;
}
/// <summary>
/// 导入用户列表
/// </summary>
[HttpPost]
[Description("导入用户列表")]
public async Task<IResultModel> Import(IFormFile file)
{
if (file == null || file.Length == 0)
return ResultModel.Failed("请选择文件");
var model = new ImportModel
{
SheetIndex = 0,
HasHeader = true,
Validate = true
};
using var stream = file.OpenReadStream();
var result = await _excelHandler.Import<UserEntity>(model, stream);
if (!result.Success)
return ResultModel.Failed(result.ErrorMsg);
// 批量插入导入的数据
await _userService.BatchInsertAsync(result.Data);
return ResultModel.Success($"导入成功 {result.SuccessRows} 条,失败 {result.ErrorRows} 条");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 🔧 实体特性配置
# 1. Description 特性(列名)
使用 Description 特性定义 Excel 列名:
using System.ComponentModel;
public class UserEntity
{
[Description("用户 ID")]
public int Id { get; set; }
[Description("用户名")]
public string Name { get; set; }
[Description("年龄")]
public int Age { get; set; }
[Description("邮箱")]
public string Email { get; set; }
[Description("创建时间")]
public DateTime CreateTime { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2. 忽略导出/导入
使用 Ignore 特性忽略某些属性:
using DarkM.Lib.Data.Abstractions;
public class UserEntity
{
[Description("用户名")]
public string Name { get; set; }
// 导出时忽略
[Ignore]
public string Password { get; set; }
// 导入时忽略
[Ignore(ImportIgnore = true)]
public int Id { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3. 指定日期格式
[Description("创建时间|yyyy-MM-dd HH:mm:ss")]
public DateTime CreateTime { get; set; }
1
2
2
# 4. 指定数字格式
[Description("价格|0.00")]
public decimal Price { get; set; }
1
2
2
# 📚 最佳实践
# 1. 使用 Description 特性定义列名
public class UserExportModel
{
[Description("用户 ID")]
public int Id { get; set; }
[Description("用户名")]
public string Name { get; set; }
[Description("邮箱")]
public string Email { get; set; }
[Description("创建时间")]
public DateTime CreateTime { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2. 导入时验证数据
var model = new ImportModel
{
Validate = true,
SheetIndex = 0,
HasHeader = true
};
var result = await _excelHandler.Import<UserEntity>(model, stream);
if (!result.Success)
{
// 处理错误
foreach (var error in result.Errors)
{
Console.WriteLine($"第{error.RowIndex}行:{error.Error}");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3. 大数据量导出使用分页
var page = 1;
var pageSize = 1000;
var allUsers = new List<UserEntity>();
while (true)
{
var users = await _userService.GetPageAsync(page, pageSize);
if (!users.Any()) break;
allUsers.AddRange(users);
page++;
}
var result = _excelHandler.Export(new ExportModel
{
FileName = "用户列表.xlsx"
}, allUsers);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4. 多 Sheet 导出
var result = _excelHandler.Export(
new ExportExcelSheet
{
Model = new ExportModel { SheetName = "用户数据" },
Data = users
},
new ExportExcelSheet
{
Model = new ExportModel { SheetName = "订单数据" },
Data = orders
},
new ExportExcelSheet
{
Model = new ExportModel { SheetName = "产品数据" },
Data = products
}
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5. 前端导出组件
<template>
<el-button
type="success"
icon="el-icon-download"
@click="handleExport">
导出 Excel
</el-button>
</template>
<script>
export default {
methods: {
async handleExport() {
const response = await this.$api.user.export();
// 下载文件
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = '用户列表.xlsx';
link.click();
window.URL.revokeObjectURL(url);
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 6. 前端导入组件
<template>
<el-upload
ref="upload"
action="/api/user/import"
:before-upload="beforeUpload"
:on-success="onSuccess"
:on-error="onError"
accept=".xlsx,.xls">
<el-button type="primary" icon="el-icon-upload2">
导入 Excel
</el-button>
</el-upload>
</template>
<script>
export default {
methods: {
beforeUpload(file) {
const isExcel = file.type === 'application/vnd.ms-excel' ||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
if (!isExcel) {
this.$message.error('只能上传 Excel 文件!');
return false;
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
this.$message.error('上传文件大小不能超过 2MB!');
return false;
}
return true;
},
onSuccess(response) {
if (response.success) {
this.$message.success(response.data);
this.$emit('refresh');
} else {
this.$message.error(response.msg);
}
},
onError(error) {
this.$message.error('上传失败');
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 🔍 常见问题
# Q1: 中文乱码如何解决?
解决方案:
确保文件保存时使用正确的编码:
var model = new ExportModel
{
FileName = "用户列表.xlsx",
Encoding = Encoding.UTF8 // 如果需要
};
1
2
3
4
5
2
3
4
5
# Q2: 日期格式问题?
解决方案:
使用 Description 特性指定格式:
[Description("创建时间|yyyy-MM-dd HH:mm:ss")]
public DateTime CreateTime { get; set; }
1
2
2
# Q3: 数字格式问题?
解决方案:
使用 Description 特性指定格式:
[Description("价格|0.00")]
public decimal Price { get; set; }
1
2
2
# Q4: 大文件导出内存溢出?
解决方案:
使用流式导出或分页导出:
// 分页导出
for (int page = 1; page <= totalPages; page++)
{
var users = await _userService.GetPageAsync(page, 1000);
// 写入文件
}
1
2
3
4
5
6
2
3
4
5
6
# Q5: 导入数据验证失败?
解决方案:
- 检查 Excel 列名是否与实体属性匹配
- 检查数据类型是否正确
- 查看错误信息定位问题行
var result = await _excelHandler.Import<UserEntity>(model, stream);
if (!result.Success)
{
foreach (var error in result.Errors)
{
Logger.Error($"第{error.RowIndex}行:{error.Error}");
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 📚 相关文档
# 🔗 参考链接
- 源代码 (opens new window) -
src/Framework/Excel - EPPlus 文档 (opens new window)
- NPOI GitHub (opens new window)
- Aspose.Cells (opens new window)
最后更新: 2024-09-20