Excel 导入导出
这篇解决什么
列表要下载成 .xls,或按模板把账号批量灌进系统。读完能改 ExcelUtils、导出 / 导入 VO 上的 @ExcelProperty,以及前端的 downloadFileFromBlobPart。
默认端口 48080,管理端前缀 /admin-api。范文走系统管理 → 用户管理。
示意图:页面只负责下发请求和落盘;读写文件在 ExcelUtils,字典标签在 DictConvert。
Starter 依赖 FastExcel 1.3.0(cn.idev.excel,EasyExcel 的后续维护线)。业务代码只调 ExcelUtils,不要直接 FastExcelFactory。
组件位置
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| Starter | 读写 Excel、字典转换、下拉列 | ruoyi-office/yudao-framework/yudao-spring-boot-starter-excel/ |
ExcelUtils | write / read | .../excel/core/util/ExcelUtils.java |
@DictFormat | 声明字典类型 | .../excel/core/annotations/DictFormat.java |
DictConvert | 值 ↔ 标签 | .../excel/core/convert/DictConvert.java |
| 导出 / 导入接口 | /system/user/export-excel、/import | yudao-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(部门名等):
@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、角色名)不会出列。
@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 会被丢掉。
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 不一致。
export function exportUser(params: any) {
return requestClient.download('/system/user/export-excel', { params });
}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(用户岗位)。
导入接口
@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,只兼容旧模板。
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 的重载,到上限就停。
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,弹窗组件负责下模板和上传:
{
value: 'user',
label: '用户账号 + 组织',
templateFileName: '用户导入模板.xls',
downloadTemplate: () => importUserTemplate('user'),
importRequest: (file, updateSupport) =>
importUser(file, updateSupport, 'user'),
}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。
String label = readCellData.getStringValue();
String value = DictFrameworkUtils.parseDictDataValue(type, label);
return Convert.convert(fieldClazz, value);String label = DictFrameworkUtils.parseDictDataLabel(type, String.valueOf(object));
return new WriteCellData<>(label);| 名称 | 说明 | 仓库路径 |
|---|---|---|
DictConvert | 单值字典 | .../convert/DictConvert.java |
MultiDictConvert | 库里逗号、表格里顿号 | 同目录 |
MoneyConvert | 分 → 元,导出用 | 同目录 |
JsonConvert | 对象写成 JSON 文本 | 同目录 |
StringListConvert | List<String> 用 / 拼接 | 同目录 |
AreaConvert | 地区编号 ↔ 名称 | 同目录 |
LocalTimeStringConverter | LocalTime 按字符串读写 | 同目录 |
自己写转换器时,同样挂在 @ExcelProperty(converter = XxxConvert.class)。
常用注解
| 名称 | 说明 | 仓库路径 |
|---|---|---|
@ExcelProperty | 列名;converter 指定转换器 | cn.idev.excel.annotation |
@ExcelIgnoreUnannotated | 没注解的字段不进出 Excel | 同上 |
@ExcelIgnore | 单个字段忽略 | 同上 |
@ColumnWidth | 列宽,单位字符,最大 255 | 同上 |
@DictFormat | 字典类型,如 system_user_sex | Starter annotations/ |
@ExcelColumnSelect | 模板下拉,dictType / functionName 二选一 | 同上 |
标题行高、字体、合并单元格等样式注解看 FastExcel 文档。多数业务列表只需要 @ExcelProperty + @ExcelIgnoreUnannotated。
列名必须和模板一致
@ExcelProperty("登录名称") 对的是表头文字,不是 Java 字段名。改了列名却继续用旧模板,这一列读进来是 null。
配置与操作
导出 VO 加 @ExcelProperty + @ExcelIgnoreUnannotated,用 ExcelUtils。列名必须和模板表头一致。依赖是 FastExcel。
开启见 框架层。
