23 KiB
WPS 单元格图片导入与双格式导出实现方案
日期:2026-09-04 状态:已按方案实施并通过本地验证 范围:报备资料导入、单条报备资料导出、批次通道文件下载和批次 ZIP 下载
1. 目标
在不改变现有报备资料、材料版本、审核、批次和通道报备状态逻辑的前提下:
- 导入自动识别并解析两类
.xlsx图片:- ExcelJS 当前支持的标准 Drawing 图片;
- WPS
DISPIMG + xl/cellimages.xml单元格图片。
- 导出时让用户选择:
- Excel 通用格式:保持系统现有标准 Drawing 图片格式;
- WPS 单元格图片格式:生成与样本相同机制的
DISPIMG单元格图片文件。
- 保持公式安全检查严格有效。只允许与包内受控图片一一对应的 WPS
DISPIMG,不得直接放宽为允许任意公式。 - 两种格式使用相同的真实 PostgreSQL 资料、批次快照和 MinIO 文件,格式选择不得改变业务数据、任务状态或历史批次内容。
本方案不涉及短信发送、Redis Stream、队列、计费、余额、通道连接参数或部署架构调整。
2. 样本核验结果
样本:C:\Users\hectorzhao\Downloads\行业报备.xlsx
| 项目 | 实测结果 |
|---|---|
| 文件大小 | 20,976,525 字节 |
| SHA-256 | D5B5029B044EF5B8A86A68D4FE33E4F80D3AAB3A9B547D228145158D4B1518C1 |
| 工作表 | 行业 |
| 使用范围 | A1:M17,1 行表头、16 行数据 |
| WPS 图片公式 | 43 个,位于 H2:K17 的非空图片单元格 |
cellimages.xml 图片项 |
43 个 |
| 图片关系 | 43 个,和公式 ID 一一对应 |
| 业务图片 | xl/media/image2.png 至 image44.png,均为 PNG |
| 业务图片原始字节合计 | 20,672,756 字节,约占整个文件 98.55% |
| 图片像素范围 | 宽 461~1226,高 276~840 |
| 标准 Drawing | 3 个,均指向 A1 的同一张 1×1、84 字节 PNG 占位图 |
样本中每个图片单元格保存的不是标准 Drawing 锚点,而是以下公式:
_xlfn.DISPIMG("ID_7C706627234641A7BCEA9177C8A3EACB",1)
其引用链为:
sheet1.xml 中的单元格公式
-> 公式内 ID_xxx
-> xl/cellimages.xml 中 xdr:cNvPr@name
-> a:blip@r:embed
-> xl/_rels/cellimages.xml.rels
-> xl/media/imageN.png
xl/_rels/workbook.xml.rels 还包含 WPS 专用关系:
Type="http://www.wps.cn/officeDocument/2020/cellImage"
Target="cellimages.xml"
[Content_Types].xml 包含:
ContentType="application/vnd.wps-officedocument.cellimage+xml"
PartName="/xl/cellimages.xml"
样本中的图片显示尺寸较小,但 PNG 原始像素和原始字节仍完整保存在 XLSX 中。调整单元格显示尺寸不等于压缩图片,WPS 格式选择也不应被描述为压缩功能。
3. 与系统现有图片处理方式的差异
3.1 当前导入
当前实现使用 ExcelJS 加载工作簿,然后:
assertSafeWorkbook拒绝任何公式或疑似公式文本;readEmbeddedImages只读取worksheet.getImages();- 根据 Drawing 左上角锚点换算图片所在行列;
- 提交导入时将图片上传至真实 MinIO,并把
fileObjectId/fileName/contentType写入待审核资料。
对本次样本的实际结果是:
- ExcelJS 将 43 个
DISPIMG识别为公式,因此现有安全检查会直接拒绝文件; - ExcelJS
worksheet.getImages()只返回 3 个 A1 的 1×1 占位 Drawing; - 43 个真实业务图片虽然进入 ExcelJS 的媒体集合,但没有单元格锚点,当前代码无法关联到 H2:K17;
- 即使简单放宽公式检查,图片列仍会被识别为无图片,单元格文本还可能变成
=DISPIMG(...),不能得到真实文件对象; - 当前上传上限为 10 MiB,而样本约 20.0 MiB,会在工作簿解析前被上传中间件拒绝。
3.2 当前导出
当前导出使用 ExcelJS:
- 从真实 MinIO 下载 PNG/JPEG/GIF 原文件;
workbook.addImage注册媒体;worksheet.addImage以oneCellDrawing 锚点覆盖到目标单元格;- 按通道配置调整列宽、图片显示范围和行高;
- 图片单元格本身仍保留文件名文本。
该格式是标准 DrawingML 图片:Excel 和 WPS 通常都能打开,兼容范围更广;但图片本质是浮动绘图对象,不是 WPS 的单元格图片值。
3.3 差异结论
| 对比项 | 系统现有 Excel 格式 | 样本 WPS 格式 |
|---|---|---|
| 单元格内容 | 文件名文本 | DISPIMG 公式 |
| 图片定位 | 工作表 Drawing 锚点 | 公式 ID 关联 cellimages.xml |
| 图片关系文件 | xl/drawings/*.xml(.rels) |
xl/cellimages.xml 和专用 rels |
| ExcelJS 直接读取 | 支持 | 不支持单元格映射 |
| Microsoft Excel 兼容性 | 较好 | 取决于 Excel 版本,可能显示 _xlfn.DISPIMG 或不显示图片 |
| WPS 行/列语义 | 浮动对象 | WPS 原生单元格图片 |
| 图片原始大小 | 默认保留原图 | 样本同样保留原图 |
因此不能用“允许 DISPIMG 公式”代替 WPS 图片支持,也不能把两种格式合并为同一解析路径。
4. 产品交互方案
4.1 导入
导入不要求用户预先选择格式。上传后由后端自动检测:
excel_drawing:仅存在标准 Drawing 图片;wps_cell_image:存在有效cellimages.xml和匹配的DISPIMG;mixed:两种图片同时存在;none:没有图片。
解析结果页显示只读提示,例如:
已识别:WPS 单元格图片格式,43 张图片
混合格式按单元格合并。若同一单元格同时存在 WPS 单元格图片和标准 Drawing,视为冲突并要求用户修正,不静默选择其中一个。
4.2 单条报备资料导出
“导出报备资料”点击后打开格式选择弹窗:
Excel 通用格式,默认选中;说明“图片为标准 Excel 图片,兼容 Excel 和 WPS”;WPS 单元格图片格式;说明“图片作为 WPS 单元格图片,Microsoft Excel 兼容性取决于版本”。
确认后才请求导出。关闭弹窗不发请求,生成中禁止重复提交,失败保留选择并展示后端真实错误。
4.3 报备批次导出
现有“报备文件导出”已经是弹窗,不再叠加第二层弹窗。在弹窗顶部增加同一格式选择:
- 下载单个通道文件时应用当前选择;
- “全部下载”时,ZIP 内所有 XLSX 使用同一种选择格式;
- TXT 简报不受影响。
Excel 通用格式继续使用现有文件名。WPS 文件增加 _WPS 后缀以免用户混淆,例如:
2026-09-04_通道名_RBxxxx_WPS.xlsx
默认值始终为 Excel 通用格式,不改变既有用户的下载结果和接口行为。
5. 后端实现设计
5.1 增加统一格式枚举
type ReportWorkbookFormat = 'excel_drawing' | 'wps_cell_image';
所有入口统一校验该枚举,缺省为 excel_drawing。不接受任意字符串、文件扩展名或由前端提交的 OOXML 片段。
建议接口调整:
POST /api/admin/report-materials/single-export
body.outputFormat = excel_drawing | wps_cell_image
GET /api/admin/report-materials/batches/:id/files/:fileId/download
?outputFormat=excel_drawing|wps_cell_image
GET /api/admin/report-materials/batches/:id/download
?outputFormat=excel_drawing|wps_cell_image
权限、租户、批次、通道和文件归属校验保持现有逻辑;格式参数不参与业务对象定位。
5.2 导入前置 OOXML 检查器
在 ExcelJS 之前增加 WorkbookPackageInspector,使用项目已有 jszip 读取原始 XLSX 包,只做受限结构解析:
- 验证 ZIP 路径规范化,拒绝绝对路径、
..路径穿越和重复关键部件; - 拒绝宏、ActiveX、OLE、外部链接、外部关系和非预期可执行部件;
- 读取 workbook、worksheet、关系文件、
cellimages.xml和媒体清单; - 从原始 worksheet XML 提取真实公式单元格,避免 ExcelJS 因合并单元格复制公式造成重复图片;
- 建立
sheetName + row + column -> image映射; - 输出检测格式、图片数、冲突和安全诊断,再交给现有 ExcelJS 文本/样式解析流程。
本次样本在原始 XML 中是 43 个公式;ExcelJS 模型中会因合并单元格显示为 49 个公式。因此 WPS 图片定位必须以原始 worksheet XML 为准。
5.3 WPS 单元格图片解析器
新增 WpsCellImageReader,按以下顺序解析:
- 从 workbook relationships 找到且仅找到一个 WPS cellImage 部件;
- 解析
cellimages.xml中每个xdr:cNvPr@name和a:blip@r:embed; - 通过
cellimages.xml.rels解析内部媒体路径; - 只接受包内图片关系,不允许
TargetMode="External"; - 从每个原始工作表单元格提取严格格式的
DISPIMGID; - 校验公式 ID、cellImage ID、relationship ID 和媒体对象一一可解析;
- 返回与现有
EmbeddedImage相同的{row, column, extension, buffer},让后续映射、MinIO 上传和审核流程继续复用。
不把 cellimages.xml 中的 a:xfrm 坐标当作单元格地址。样本的业务单元格位置来自公式本身,ID 才是图片关联键。
5.4 公式安全策略
保留现有“默认拒绝所有公式”原则,仅为已验证的 WPS 图片单元格建立精确白名单。允许条件必须全部满足:
-
原始公式完整匹配:
^_xlfn\.DISPIMG\("ID_[A-F0-9]{32}",1\)$ -
公式所在工作表和单元格已由 OOXML 检查器登记;
-
ID 在
cellimages.xml中唯一存在; -
图片关系是包内图片且目标文件通过类型、大小和签名校验;
-
一个公式只引用一张图片,一个 ID 不允许被不受控地复用到多个业务单元格;
-
除该单元格外,工作簿不存在其他公式、共享公式、数组公式、外部链接或疑似公式文本。
不要修改为“公式名包含 DISPIMG 即放行”,也不要只依赖 ExcelJS 解析后的 cell.value.result。
5.5 标准 Drawing 与 WPS 图片统一
readEmbeddedImages 调整为接收前置检查结果:
标准 Drawing 解析结果
+ WPS cell image 解析结果
-> 按 sheet/row/column 合并
-> 冲突检测
-> 统一 EmbeddedImage[]
样本中 A1 的 3 个 1×1 Drawing 是占位对象,不应当被当作 H2:K17 的图片,也不应以“图片数量大于零”证明 WPS 图片已解析。是否在 WPS 导出中生成类似占位 Drawing,应通过最小原型在目标 WPS 版本中验证后决定;第一版不盲目复制样本中的冗余占位对象。
5.6 上传大小与资源保护
样本超过当前 10 MiB 上限。按最终业务要求将报备 XLSX 单文件上限调整为 100 MiB,同时增加解包级限制,而不是只提高 Multer 上限:
- ZIP 压缩文件最大 100 MiB;
- 解压后总大小最大 500 MiB;
- ZIP 项目数最大 1,000;
- 单张图片最大 10 MiB;
- 单工作簿图片最大 500 张;
- 工作表最大 20 张、单表最大 20,000 行和 200 列;
- XML 单部件最大 10 MiB;
- 图片必须校验文件签名和实际 MIME,不能只信扩展名或 relationship;
- 分析和提交阶段均执行同一检查,不能只在预览阶段检查;
- 超限返回明确错误码和当前限制,不发生部分 MinIO 上传或部分数据库落库。
100 MiB 文件在 ExcelJS、JSZip 和图片 Buffer 并存时会产生数倍内存峰值。实施时需要记录 1、5、20、50、100 MiB 样本的解析耗时和 Node RSS,必要时限制并发分析数;不能仅根据压缩文件大小估算内存。
5.7 WPS 导出生成器
保留 ExcelJS 作为工作簿表格、样式、列宽、行高和标准格式的唯一生成入口,再增加 WpsCellImageWorkbookTransformer 对生成结果做受控 OOXML 后处理:
- 先按现有代码生成标准 XLSX;
- 根据标准 Drawing 锚点确定每张报备图片的目标单元格;
- 为每张图片生成唯一
ID_加 32 位大写十六进制标识; - 创建
xl/cellimages.xml; - 创建
xl/_rels/cellimages.xml.rels; - 在 workbook relationships 增加 WPS cellImage 关系;
- 在
[Content_Types].xml增加 WPS cellImage 类型; - 将目标单元格改写为严格的
_xlfn.DISPIMG("ID",1)公式及缓存显示值; - 移除已经转换的 Drawing 图片锚点和失去引用的媒体关系;
- 保留表头、文本、列宽、行高、冻结窗格、批次快照内容和未转换对象;
- 重新打包并再次执行包结构、关系完整性和公式安全自检。
这样可以复用现有成熟导出逻辑,避免为 WPS 另写一套字段取值、转换、图片下载和样式代码。
5.8 当前批次文件的处理
格式选择发生在下载时,不修改历史批次和 MinIO 原文件:
- Excel 通用格式:直接返回现有 MinIO XLSX,字节和哈希保持不变;
- WPS 单元格图片格式:读取该批次现有 XLSX,在内存或受控临时目录中转换后返回;
- 批次 ZIP:逐通道转换 XLSX,再和原 TXT 简报一起打包;任一文件转换失败则整个 ZIP 明确失败,不输出不完整包;
- 第一版不缓存 WPS 派生文件,不新增数据库记录,也不覆盖原
ReportExportFile;如真实性能证明需要缓存,再单独设计派生文件生命周期。
单条导出则先按现有逻辑生成标准工作簿,再根据选择决定是否转换为 WPS 格式。
6. 前端修改范围
预计涉及:
src/apps/admin/AdminReportTasksPage.tsx:单条导出格式弹窗;src/apps/admin/AdminChannelReportPage.tsx:单条导出格式弹窗;src/apps/admin/AdminReportBatchesPage.tsx:现有导出弹窗增加格式选择;src/api/admin/channels-reports.api.ts:传递outputFormat;- 报备导入分析弹窗:显示自动识别的图片格式和数量,展示不支持/冲突原因;
- 共用一个格式选择组件和类型,不在三个页面复制状态及文案。
桌面端和 390px 窄屏均需验证弹窗选项、说明、生成中、失败重试和下载行为。格式选择不写入 localStorage,重新打开默认回到 Excel 通用格式。
7. 后端修改范围
预计涉及:
api/src/report-materials/report-materials.controller.ts:上传上限和导出格式参数;api/src/report-materials/report-materials.contracts.ts:格式枚举及导入诊断类型;api/src/report-materials/report-materials.helpers.ts:保留通用 helper,公式检查改为接收精确白名单;api/src/report-materials/import-parser.service.ts、import-review.service.ts:分析及提交阶段统一解析;api/src/report-materials/channel-export.service.ts:单条导出选择;api/src/report-materials/batch-download.service.ts:批次单文件和 ZIP 选择;- 新增独立 OOXML 包检查、WPS 图片读取和 WPS 输出转换服务;
- 继续复用
FilesService、MinIO、操作日志和现有字段映射。
不要把 ZIP/XML 细节继续堆入已经较大的 report-materials service。解析器和转换器应是无数据库副作用的纯服务,方便使用合成工作簿做完整安全测试。
8. 数据库、MinIO 和兼容性判断
8.1 数据库
第一版不需要 Prisma migration:
- 导入格式、图片数量和诊断可写入现有导入批次
previewJSON; - 导入后的图片仍是现有
FileObject; - 导出格式是一次下载请求参数,不改变材料版本和批次记录。
若以后要求“记住每个通道默认格式”或缓存 WPS 派生文件,才需要单独设计配置或文件记录,不在本次顺带增加。
8.2 MinIO
- 导入后继续逐张保存真实图片对象;
- 原始导入 XLSX 继续按现有流程保存;
- WPS 导出第一版不永久保存,不覆盖现有批次文件;
- 任何解析失败都不得留下无法关联的部分文件。若提交阶段已上传部分行图片后某行失败,应沿用现有逐行结果并增加可追踪清理策略测试。
8.3 格式兼容
excel_drawing是跨 Excel/WPS 的默认格式;wps_cell_image明确标注为 WPS 优先格式;- 两者扩展名均为
.xlsx,但内部结构不同; - 不承诺旧版 Microsoft Excel 原生显示 WPS
DISPIMG; - WPS 目标版本、Windows Excel 365 和 LibreOffice 至少各做一次打开结果记录,不能只用 ExcelJS 回读判定可交付。
9. 错误处理
建议增加稳定错误码:
| 错误码 | 含义 |
|---|---|
WORKBOOK_TOO_LARGE |
压缩文件超过上限 |
WORKBOOK_EXPANDED_TOO_LARGE |
解包总量超过上限 |
WORKBOOK_UNSAFE_PART |
宏、外链、OLE 或危险部件 |
WORKBOOK_FORMULA_NOT_ALLOWED |
存在非白名单公式 |
WPS_CELL_IMAGE_RELATION_INVALID |
ID、关系或媒体缺失/重复 |
WPS_CELL_IMAGE_CONFLICT |
同一单元格存在两种图片 |
WORKBOOK_IMAGE_TYPE_UNSUPPORTED |
图片真实类型不支持 |
WORKBOOK_IMAGE_LIMIT_EXCEEDED |
图片数量或单图大小超限 |
WPS_EXPORT_CONVERSION_FAILED |
标准文件转 WPS 失败 |
错误信息要指出工作表、单元格或部件,但不得回显原始图片内容、服务器路径或敏感文件对象信息。
10. 测试方案
10.1 解析器单元测试
使用代码生成的小型 OOXML fixture,不把包含真实企业和个人信息的样本提交到仓库:
- 2 张标准 Drawing 图片;
- 2 张有效 WPS 单元格图片;
- 标准/WPS 混合但不冲突;
- 同单元格冲突;
- 公式 ID 不存在;
- cellImage ID 重复;
- relationship 缺失、外链、路径穿越;
- 图片扩展名与文件签名不一致;
- 普通公式、共享公式、数组公式和伪造
DISPIMG; - 合并单元格下不重复计数;
- ZIP 项目数、单图、总解包大小超限;
- PNG、JPEG、GIF 的支持结果。
10.2 当前样本回归
只在本地受控测试中使用当前样本,验收:
- 自动识别
wps_cell_image; - 识别 1 个工作表、16 条数据、43 张业务图片;
- 43 个公式 ID、cellImage 和媒体关系全部匹配;
- 不把 A1 的 3 个 1×1 占位 Drawing 计入业务字段;
- 图片列、行号和预览映射正确;
- 提交前后图片哈希一致;
- 非图片字段和手机号等文本/数字读取不被 WPS 解析器改变。
10.3 真实后端集成测试
在明确授权的测试环境使用真实 API、PostgreSQL 和 MinIO:
- 分析样本但不审核应用,确认原业务签名不变化;
- 使用专用测试企业/应用提交少量合成行,确认 FileObject、导入批次和待审核项一致;
- 审核通过后确认图片字段落入真实材料、材料版本只按既有规则变化一次;
- 失败行不清空已有补资料字段;
- 删除测试数据前单独确认清理边界,不触碰真实客户资料。
10.4 双格式导出测试
同一批次快照分别导出两种格式:
- 文本、字段顺序、默认值、转换、行数和图片内容哈希一致;
- Excel 格式仍使用标准 Drawing,既有文件字节不被修改;
- WPS 格式中每个图片单元格的公式、ID、关系和媒体一一对应;
- WPS 中图片按单元格显示,排序、筛选、调整行高后行为符合目标版本;
- Excel 365 打开两种格式并记录 WPS 格式的实际兼容结果;
- 单通道下载和全部 ZIP 下载均验证文件名、数量、内容和失败原子性;
- 没有图片的工作簿两种格式内容一致,WPS 格式不生成空的 cellImage 部件。
10.5 性能和资源
记录 1、5、20、50、100 MiB 文件的:
- 上传和分析总耗时;
- JSZip 检查耗时;
- ExcelJS 加载耗时;
- 峰值 RSS;
- 43、100、500 张图片时的处理时间;
- 批次 ZIP 多通道转换的总耗时和临时空间。
达到上限时应快速、明确失败,不使 API 进程因并发大文件出现长时间无响应。
10.6 前端和门禁
- 导入自动识别提示、映射预览、空数据、失败、权限和超限状态;
- 三个导出入口的弹窗、默认选项、取消、生成中、失败重试和成功下载;
- 1600px 桌面和 390px 窄屏;
- 浏览器 Network 参数、响应文件名和 Console;
- API 定向与全量测试、前后端 TypeScript、Vite 构建、Prisma validate、依赖安全、部署契约和
git diff --check; - 同步
docs/system-functional-test-cases.md和docs/testing-progress.md。
构建、ExcelJS 回读或合成 fixture 通过都不能代替真实 WPS 打开和真实 API/PostgreSQL/MinIO 验收。
11. 实施顺序
- 先做无业务依赖的最小 WPS OOXML 读写原型,使用 2 行、PNG/JPEG/GIF 小图在目标 WPS 和 Excel 365 实际打开。
- 固化包安全限制和
DISPIMG精确白名单。 - 实现
WorkbookPackageInspector与WpsCellImageReader,接入分析和提交两阶段。 - 调整上传上限并加入解包、图片和内存保护。
- 实现标准 XLSX 到 WPS 单元格图片的纯转换器。
- 接入单条导出、批次单文件和批次 ZIP 三个后端入口。
- 实现共用格式选择 UI 和导入识别提示。
- 完成合成 fixture、当前样本、真实 MinIO、真实数据库、浏览器及 WPS/Excel 客户端验收。
- 更新测试用例和测试进度;是否提交、推送、部署分别等待明确授权。
12. 影响与风险结论
- 前端:中等,涉及三个导出入口和导入提示,但不改变报备业务表格主流程。
- 后端:较高,涉及不受 ExcelJS 支持的 WPS 私有 OOXML 扩展、ZIP 安全、内存峰值和批量转换。
- 数据库:预计无 migration。
- MinIO:复用现有对象;WPS 派生文件第一版不持久化。
- 短信链路:无影响,不应触发发送、补发、重投或重新入队。
- 最大风险不是 UI,而是错误放宽公式安全检查、ZIP 资源消耗、WPS 与 Excel 客户端兼容差异,以及把占位 Drawing 误认成业务图片。
最小充分范围是“自动读取 WPS 单元格图片 + 保留现有 Excel 导出 + 按需生成 WPS 变体”。不在本次加入图片压缩、通道默认格式、派生文件缓存、历史数据回写或更多办公格式。
13. 2026-09-07 图片关系解析边界修复
本节细化第5.3、9和10节,不放宽公式白名单、不改变审核/导入流程。真实故障文件中,一个没有blip的图片节点仍被身份证单元格引用;跨节点正则错误地将其后的正常图片关系配给该损坏ID,反而首先报告正常营业执照图片缺失。
- 按单个cellImage节点提取图片ID与blip,不得越过该节点的结束边界。自闭合空图片节点、空单元格也有独立边界,不能吞入下一节点或单元格。
- 未被任何单元格引用的残留节点不阻断正常图片解析;仍被DISPIMG引用而ID、内部图片关系或媒体文件不可解析时必须拒绝,不能按空值导入。
- 同一图片ID重复时不得后写覆盖;被引用的重复ID明确报错。重复relationship ID不得任取目标,外部图片关系不作为包内图片接受。
- 错误明确给出工作表名、单元格地址及重新插图/清空建议;缺失引用、重复图片ID、缺失媒体分别说明。此层尚无字段映射上下文,不猜测字段名;例如“工作表【生活缴费】N7的WPS图片引用缺失或不唯一,请重新插入图片或清空该单元格”。
- 分析的metadata模式与提交的图片字节模式必须一致;清空损坏单元格后,其余图片定位及字节不变。补回缺失图片仍需用户提供原图,平台不能推断或借用相邻图片。