# 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

# 🔧 支持的 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

# 配置项详解

配置项 类型 默认值 说明
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. 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

# 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

# 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

# 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

# 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

# 💡 使用示例

# 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. 多 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

# 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

# 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

# 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

# 🔧 实体特性配置

# 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. 忽略导出/导入

使用 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

# 3. 指定日期格式

[Description("创建时间|yyyy-MM-dd HH:mm:ss")]
public DateTime CreateTime { get; set; }
1
2

# 4. 指定数字格式

[Description("价格|0.00")]
public decimal Price { get; set; }
1
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. 导入时验证数据

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

# 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

# 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

# 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

# 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

# 🔍 常见问题

# Q1: 中文乱码如何解决?

解决方案:

确保文件保存时使用正确的编码:

var model = new ExportModel
{
    FileName = "用户列表.xlsx",
    Encoding = Encoding.UTF8  // 如果需要
};
1
2
3
4
5

# Q2: 日期格式问题?

解决方案:

使用 Description 特性指定格式:

[Description("创建时间|yyyy-MM-dd HH:mm:ss")]
public DateTime CreateTime { get; set; }
1
2

# Q3: 数字格式问题?

解决方案:

使用 Description 特性指定格式:

[Description("价格|0.00")]
public decimal Price { get; set; }
1
2

# Q4: 大文件导出内存溢出?

解决方案:

使用流式导出或分页导出:

// 分页导出
for (int page = 1; page <= totalPages; page++)
{
    var users = await _userService.GetPageAsync(page, 1000);
    // 写入文件
}
1
2
3
4
5
6

# Q5: 导入数据验证失败?

解决方案:

  1. 检查 Excel 列名是否与实体属性匹配
  2. 检查数据类型是否正确
  3. 查看错误信息定位问题行
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

# 📚 相关文档


# 🔗 参考链接


最后更新: 2024-09-20