Skip to content

Excel 导入导出 ​

这篇解决什么 ​

列表要下载成 .xls,或按模板把账号批量灌进系统。读完能改 ExcelUtils、导出 / 导入 VO 上的 @ExcelProperty,以及前端的 downloadFileFromBlobPart。

默认端口 48080,管理端前缀 /admin-api。范文走系统管理 → 用户管理。

Excel 分层示意图

示意图:页面只负责下发请求和落盘;读写文件在 ExcelUtils,字典标签在 DictConvert。

Starter 依赖 FastExcel 1.3.0(cn.idev.excel,EasyExcel 的后续维护线)。业务代码只调 ExcelUtils,不要直接 FastExcelFactory。

组件位置 ​

名称说明仓库路径
Starter读写 Excel、字典转换、下拉列ruoyi-office/yudao-framework/yudao-spring-boot-starter-excel/
ExcelUtilswrite / read.../excel/core/util/ExcelUtils.java
@DictFormat声明字典类型.../excel/core/annotations/DictFormat.java
DictConvert值 ↔ 标签.../excel/core/convert/DictConvert.java
导出 / 导入接口/system/user/export-excel、/importyudao-module-system 的 UserController
前端下载Blob 另存为@vben/utils 的 downloadFileFromBlobPart
前端导入弹窗下模板、上传、覆盖开关web-antd/src/components/import-excel-modal/

Excel 导出 ​

复用分页查询,把 pageSize 设成 PageParam.PAGE_SIZE_NONE(-1),再写成文件。用户导出权限是 system:user:export。

导出接口 ​

UserController.exportUserList 先查全量,再把 AdminUserDO 拼成 UserRespVO(部门名等):

java
@GetMapping("/export-excel")
@PreAuthorize("@ss.hasPermission('system:user:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportUserList(@Validated UserPageReqVO exportReqVO,
                           HttpServletResponse response) throws IOException {
    exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
    List<AdminUserDO> list = userService.getUserPage(exportReqVO).getList();
    ExcelUtils.write(response, "用户数据.xls", "数据", UserRespVO.class, buildUserRespList(list));
}

岗位导出更短:BeanUtils.toBean(list, PostRespVO.class) 后直接 write。路径在同一模块的 PostController。

不要把 DO 交给 ExcelUtils

AdminUserDO 带密码哈希、审计字段。导出、导入都用单独的 VO,只暴露要出现在表格里的列。

导出 VO ​

UserRespVO 类上标 @ExcelIgnoreUnannotated:没写 @ExcelProperty 的字段(备注、头像、createTime、角色名)不会出列。

java
@ExcelIgnoreUnannotated
public class UserRespVO {

    @ExcelProperty("用户编号")
    private Long id;

    @ExcelProperty("用户名称")
    private String username;

    @ExcelProperty("部门名称")
    private String deptName;

    @ExcelProperty(value = "用户性别", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.USER_SEX)
    private Integer sex;

    @ExcelProperty(value = "帐号状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.COMMON_STATUS)
    private Integer status;
}

sex、status 在库里是数字,单元格里是「男 / 女」「开启 / 禁用」。@Trans 翻译字段要先 TranslateUtils.translate,再 write,见 VO 对象转换、数据翻译。

用户导出列表示意图

示意图:导出后的列来自 @ExcelProperty,字典列已经是标签。

ExcelUtils.write ​

类型化写入会先写响应头,再拿 OutputStream。否则 Servlet 一旦写出字节,后面的 Content-Type 会被丢掉。

java
response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
FastExcelFactory.write(response.getOutputStream(), head)
        .registerWriteHandler(new ColumnWidthMatchStyleStrategy())
        .registerWriteHandler(new SelectSheetWriteHandler(head))
        .registerConverter(new LongStringConverter())
        .sheet(sheetName).doWrite(data);
名称说明仓库路径
ColumnWidthMatchStyleStrategy按内容调列宽,上限 255同模块 handler/
SelectSheetWriteHandler@ExcelColumnSelect 生成下拉同上
LongStringConverter避免 Long 精度丢失FastExcel 自带

还有动态表头重载:write(response, filename, sheetName, List<List<String>> head, List<List<Object>> data)。表头不固定时用它。

前端下载 ​

exportUser 走 requestClient.download,拿到 Blob 后交给 downloadFileFromBlobPart。另存文件名由前端传入,可以和后端 filename 不一致。

ts
export function exportUser(params: any) {
  return requestClient.download('/system/user/export-excel', { params });
}
ts
async function handleExport() {
  const data = await exportUser(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '用户.xls', source: data });
}

两段分别在 web-antd/src/api/system/user/index.ts 和 views/system/user/index.vue。工具函数在 ruoyi-office-vben/packages/@core/base/shared/src/utils/download.ts。

筛选项会带进导出

handleExport 用的是表格搜索表单。前端没传的条件,后端也不会过滤。

Excel 导入 ​

用户导入权限是 system:user:import。先下模板,再 POST 上传。type 区分三种表:user(账号 + 组织)、role(用户角色)、post(用户岗位)。

导入接口 ​

java
@PostMapping("/import")
@PreAuthorize("@ss.hasPermission('system:user:import')")
public CommonResult<UserImportRespVO> importExcel(
        @RequestParam("file") MultipartFile file,
        @RequestParam(value = "updateSupport", defaultValue = "false") Boolean updateSupport,
        @RequestParam(value = "type", defaultValue = "user") String type) throws Exception {
    if ("role".equalsIgnoreCase(type)) {
        return success(userService.importUserRoleList(
                ExcelUtils.read(file, UserRoleImportExcelVO.class), updateSupport));
    }
    if ("post".equalsIgnoreCase(type)) {
        return success(userService.importUserPostList(
                ExcelUtils.read(file, UserPostImportExcelVO.class), updateSupport));
    }
    return success(userService.importUserList(
            ExcelUtils.read(file, UserImportExcelVO.class), updateSupport));
}

UserImportRespVO 分三块:createUsernames、updateUsernames、failureUsernames(用户名 → 失败原因)。updateSupport=false 时已存在账号跳过。

导入 VO 与模板 ​

导入列和导出列可以不同。UserImportExcelVO 用「登录名称 / 组织名称」,不用用户编号。deptId 标了 @ExcelIgnore,只兼容旧模板。

java
public class UserImportExcelVO {

    @ExcelProperty("登录名称")
    private String username;

    @ExcelProperty("组织名称")
    private String orgName;

    @ExcelProperty("用户邮箱")
    private String email;

    @ExcelProperty(value = "用户性别", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.USER_SEX)
    private Integer sex;
}

模板由 /get-import-template 写出两行示例。邮箱用业务域即可,文档里写成 alice@example.com。

用户导入模板示意图

示意图:组织填 总部/研发部 这种路径;性别、状态填字典标签。

单元格要写标签,不要写字典值

DictConvert 导入时按标签反查。写成 1 / 0 对不上「男」「开启」,字段会是 null,后台打 error 日志。

ExcelUtils.read ​

空文件返回空列表。普通导入用 read(file, head);怕一次灌太多,用带 maxRowCount 的重载,到上限就停。

java
public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
    if (file == null || file.isEmpty()) {
        return Collections.emptyList();
    }
    try (InputStream inputStream = file.getInputStream()) {
        return FastExcelFactory.read(inputStream, head, null)
                .autoCloseStream(false)
                .doReadAllSync();
    }
}

try 包住 InputStream,避免 Windows 下临时文件锁死。还有无 head 的 read(file),按列下标拿 Map<Integer, String>。

前端导入 ​

views/system/user/modules/import-form.vue 配三种 importTypes,弹窗组件负责下模板和上传:

ts
{
  value: 'user',
  label: '用户账号 + 组织',
  templateFileName: '用户导入模板.xls',
  downloadTemplate: () => importUserTemplate('user'),
  importRequest: (file, updateSupport) =>
    importUser(file, updateSupport, 'user'),
}
ts
export function importUserTemplate(type: UserImportType = 'user') {
  return requestClient.download('/system/user/get-import-template', { params: { type } });
}

CRM 客户导入模板还在字段上标 @ExcelColumnSelect,写出带下拉的模板。字典用 dictType,地区等动态数据用 functionName(实现 ExcelColumnSelectFunction)。

字段转换器 ​

FastExcel 的 Converter 两个方向:convertToJavaData 把单元格变成 Java 字段,convertToExcelData 反过来。DictConvert 读字段上的 @DictFormat,再调 DictFrameworkUtils。

java
String label = readCellData.getStringValue();
String value = DictFrameworkUtils.parseDictDataValue(type, label);
return Convert.convert(fieldClazz, value);
java
String label = DictFrameworkUtils.parseDictDataLabel(type, String.valueOf(object));
return new WriteCellData<>(label);
名称说明仓库路径
DictConvert单值字典.../convert/DictConvert.java
MultiDictConvert库里逗号、表格里顿号同目录
MoneyConvert分 → 元,导出用同目录
JsonConvert对象写成 JSON 文本同目录
StringListConvertList<String> 用 / 拼接同目录
AreaConvert地区编号 ↔ 名称同目录
LocalTimeStringConverterLocalTime 按字符串读写同目录

自己写转换器时,同样挂在 @ExcelProperty(converter = XxxConvert.class)。

常用注解 ​

名称说明仓库路径
@ExcelProperty列名;converter 指定转换器cn.idev.excel.annotation
@ExcelIgnoreUnannotated没注解的字段不进出 Excel同上
@ExcelIgnore单个字段忽略同上
@ColumnWidth列宽,单位字符,最大 255同上
@DictFormat字典类型,如 system_user_sexStarter annotations/
@ExcelColumnSelect模板下拉,dictType / functionName 二选一同上

标题行高、字体、合并单元格等样式注解看 FastExcel 文档。多数业务列表只需要 @ExcelProperty + @ExcelIgnoreUnannotated。

列名必须和模板一致

@ExcelProperty("登录名称") 对的是表头文字,不是 Java 字段名。改了列名却继续用旧模板,这一列读进来是 null。

配置与操作 ​

导出 VO 加 @ExcelProperty + @ExcelIgnoreUnannotated,用 ExcelUtils。列名必须和模板表头一致。依赖是 FastExcel。

开启见 框架层。

相关篇 ​

联系我们

获取报价、演示和二开方案

微信咨询二维码

微信咨询

17156169080

添加时备注「RuoYi Office」

在线体验商业版