当前位置: 首页 > news >正文

Excel 批量导入实战:当 EasyExcel 遇上单元格嵌入附件

Excel 批量导入实战:当 EasyExcel 遇上单元格嵌入附件

适用场景:后端需要解析用户上传的 Excel,其中不仅包含常规文本数据,还包含直接嵌入单元格的附件(zip、docx、图片等),要求一次性完成数据入库、附件落盘、关联业务实体。


一、场景与痛点

在常见的 B 端后台中,运营/产品同学往往通过一份 Excel 模板批量录入业务数据。模板里除了常规字段,还经常出现“需求附件”列——用户把文件直接拖进 Excel 单元格里。这对后端提出了两个核心挑战:

  1. MultipartFile 的流只能读一次:EasyExcel 读完之后,如果再用 POI 读,流已经耗尽。
  2. Excel 里的附件不是超链接,而是二进制对象:需要精确解析 OLE、OOXML、图片三种嵌入形态,并按行号对应回业务数据。
  3. Excel中的附件类型,名称获取问题:excel对实体附件的数据和名称存储在不同的位置,需要根据特定的格式进行解析,且部分格式如:.xlsx和.docx格式的名称并无存储,本文暂时只找到对.zip的名称支持。

本文以“设计需求批量导入”模块为实例,给出一条可复用的工程化路径


二、技术栈选型

组件用途版本参考
EasyExcel快速读取文本数据、类型转换、注解映射2.x / 3.x
Apache POI (XSSF)解析底层 OOXML 结构,提取单元格嵌入对象5.x
Hutool字符串、集合、空值工具5.x
Spring Boot事务控制、Service 层编排3.x
自定义 OSS 工具附件上传、路径管理

为什么不只用 EasyExcel?
EasyExcel 专注于高性能的文本/数值读取,对单元格内嵌 OLE/图片对象只提供有限支持。对于“需要把附件捞出来并上传 OSS”的场景,必须下沉到 Apache POI 的XSSFWorkbook+XSSFDrawing层做原生解析。


三、整体架构流程

用户上传 Excel (.xlsx) │ ▼ ┌───────────────────┐ │ 1. 读取字节数组 │ ← 关键:先复制 bytes,解决流不可重复读 │ byte[] fileBytes │ └───────────────────┘ │ ┌───┴───┐ ▼ ▼ EasyExcel Apache POI (文本数据) (嵌入附件) │ │ ▼ ▼ List<VO> List<EmbeddingDesc> │ │ └───┬───┘ ▼ ┌───────────────────┐ │ 2. 按行号关联 │ ← 把附件挂到对应 VO 上 │ vo.setAttachments()│ └───────────────────┘ │ ▼ ┌───────────────────┐ │ 3. 逐行校验 │ ← 字典、用户、日期、多选字段、岗位-类型联动 │ validate/convert │ └───────────────────┘ │ ▼ ┌───────────────────┐ │ 4. 分批收集 │ ← 主表PO、用户绑定、Code映射、附件信息 │ 4 个临时集合 │ └───────────────────┘ │ ▼ ┌───────────────────┐ │ 5. 批量写入 │ ← 先插主表,再插关联表,最后上传附件 │ insert + OSS upload│ └───────────────────┘ │ ▼ ┌───────────────────┐ │ 6. 后置业务 │ ← 操作历史、审批流、待办、消息通知 └───────────────────┘

四、关键实现拆解

4.1 文件字节复制:解决流不可重复读

MultipartFile.getInputStream()只能消费一次。EasyExcel 和 POI 各自需要独立流,因此第一步就是把文件读成字节数组

byte[]fileBytes=file.getBytes();// 1. EasyExcel 读取List<DesignDemandImportVo>importVos=EasyExcel.read(newByteArrayInputStream(fileBytes)).head(DesignDemandImportVo.class).sheet("DesignDemand").doReadSync();// 2. POI 解析附件(同样基于 fileBytes)List<ExcelAttachmentParser.EmbeddingDesc>embeddings=ExcelAttachmentParser.resolve(fileBytes);

通用经验:凡是要用两套工具解析同一个上传文件,先读 bytes,再各自 new ByteArrayInputStream,不要直接传file.getInputStream()


4.2 双管道解析:文本 + 附件

文本管道:EasyExcel
@DatapublicclassDesignDemandImportVo{@ExcelProperty("品类")privateStringcategoryType_dictText;@ExcelProperty("希望交付时间")privateStringhopeDeliveryDate;@ExcelProperty("需求附件")privateStringattachmentZip;// 纯占位,不需要填内容@ExcelIgnoreprivateList<CellAttachment>attachments=newArrayList<>();// 真正存附件数据}
  • @ExcelProperty做列名映射;@ExcelIgnore字段不映射列,由代码手动填充。
  • 日期在 Excel 中可能是文本格式,所以先按String接收,再手动解析为LocalDate,避免格式错乱。
附件管道:Apache POI 原生解析

ExcelAttachmentParser.resolve(byte[] xlsxBytes)是核心工具类。它遍历XSSFDrawing,识别三种嵌入形态:

  1. OLE 对象:zip、docx、xlsx 等,通常以\u0001Ole10Nativepackage条目存储在POIFSFileSystem中。
  2. OOXML 原生嵌入:无 OLE2 包装的直接嵌入,直接读PackagePart的流。
  3. 粘贴图片XSSFPicture,通过XSSFPictureData获取 PNG/JPEG 字节。
publicstaticList<EmbeddingDesc>resolve(byte[]xlsxBytes){try(XSSFWorkbookworkbook=newXSSFWorkbook(newByteArrayInputStream(xlsxBytes))){for(inti=0;i<workbook.getNumberOfSheets();i++){XSSFSheetsheet=workbook.getSheetAt(i);XSSFDrawingdrawing=sheet.getDrawingPatriarch();if(drawing==null)continue;for(XSSFShapeshape:drawing.getShapes()){XSSFClientAnchoranchor=(XSSFClientAnchor)shape.getAnchor();introw=anchor.getRow1();// 获取附件所在行号if(shapeinstanceofXSSFObjectData){// OLE / OOXML 嵌入对象...}elseif(shapeinstanceofXSSFPicture){// 图片...}}}}}

OLE 文件名乱码是重灾区。Windows 中文系统默认用 GBK 编码文件名,但 Office 2016+ 可能用 UTF-8。解析器做了多编码尝试 + 启发式乱码检测

  • 优先 GBK → GB18030 → UTF-8 → 兜底 ISO-8859-1
  • 通过looksLikeMojibake()检测典型乱码特征(Âÿ等),反向选择正确编码。

下面是ExcelAttachmentParser完整代码

importlombok.extern.slf4j.Slf4j;importorg.apache.commons.io.IOUtils;importorg.apache.poi.openxml4j.opc.PackagePart;importorg.apache.poi.poifs.filesystem.*;importorg.apache.poi.xssf.usermodel.*;importjava.io.ByteArrayInputStream;importjava.io.IOException;importjava.io.InputStream;importjava.nio.ByteBuffer;importjava.nio.ByteOrder;importjava.nio.charset.StandardCharsets;importjava.util.*;/** * Excel单元格嵌入附件解析器 */@Slf4jpublicclassExcelAttachmentParser{publicstaticList<EmbeddingDesc>resolve(byte[]xlsxBytes){List<EmbeddingDesc>result=newArrayList<>();try(XSSFWorkbookworkbook=newXSSFWorkbook(newByteArrayInputStream(xlsxBytes))){// 获取所有嵌入式文件for(inti=0;i<workbook.getNumberOfSheets();i++){XSSFSheetsheet=workbook.getSheetAt(i);XSSFDrawingdrawing=sheet.getDrawingPatriarch();if(drawing==null)continue
http://www.jsqmd.com/news/1069017/

相关文章:

  • 异步 FIFO 的时序约束
  • ABCJS完整教程:7天掌握网页乐谱渲染与音频播放技术
  • 少林小龙武校靠谱吗,值得去吗
  • 034、代码重构工程:大规模重命名、提取函数与模块拆分的精确策略
  • 自然语言驱动全栈开发:从想法到完整项目,AI 编程的能力边界在哪里
  • Python通达信数据接口终极指南:3步掌握A股金融数据分析
  • Go自动重载工具Air:从入门到精通
  • 民宿/网约房数字化治理实战:IoT智能锁实现人证核验与远程授权,彻底解决安全与成本痛点
  • 5个关键技术要点:mootdx高效读取通达信金融数据的Python实现方案
  • 邦芒贴士:职场新人不能有的六种行为
  • 音乐歌词下载终极指南:免费获取网易云QQ音乐LRC歌词的完整方案
  • 告别论文熬夜内耗!这款合规学术AI工具,适配本硕博全学段写作
  • Java程序员转Agent开发?收藏这份学习路线,轻松入门大模型时代!
  • 只验签为什么挡不住二次打包:御盾 r325 静态/动态测评里的 fail-closed 证据链
  • B端GEO推广实操复盘:如何让品牌在AI问答中被准确引用
  • Mermaid Live Editor:告别拖拽式图表,用代码思维重塑可视化创作
  • 从生物学现象到新靶点:SHOC2–MRAS–PP1C 如何打开 RAS/MAPK 药物发现的新入口
  • 2026年京东云 618 活动 Hermes Agent/OpenClaw配置Token Plan手把手教学
  • 重明链迹丨每周区块链安全要闻(0615-0621)
  • 2026几款AI写论文工具实测,哪款AI论文写作工具好呢
  • 本科大数据应届生一线、二三线城市真实薪资
  • 剩余六个月备考管综考试,需要一套适合自己的规划!
  • flink 新旧connector的区别
  • 3个步骤+5个技巧:用AntiMicroX让任何游戏都支持你的游戏手柄
  • ISO15189质控合规解读:第三方质控并非强制,科学选型才是核心
  • MCP Skill 的输入输出设计模式——如何设计易用、安全的 Skill 接口
  • Vortex模组管理器:5分钟快速入门,轻松管理250+游戏模组
  • 用企微自动化,把对话变成公司大模型的第一推荐资产
  • Java入门第30课:封装、private、getter/setter
  • CVE-2021-41773 Apache HTTP Server 路径穿越与远程命令执行漏洞