Files
lislgosms/docs/wps-cell-image-import-export-plan-20260904.md

23 KiB
Raw Permalink Blame History

WPS 单元格图片导入与双格式导出实现方案

日期:2026-09-04 状态:已按方案实施并通过本地验证 范围:报备资料导入、单条报备资料导出、批次通道文件下载和批次 ZIP 下载

1. 目标

在不改变现有报备资料、材料版本、审核、批次和通道报备状态逻辑的前提下:

  1. 导入自动识别并解析两类 .xlsx 图片:
    • ExcelJS 当前支持的标准 Drawing 图片;
    • WPS DISPIMG + xl/cellimages.xml 单元格图片。
  2. 导出时让用户选择:
    • Excel 通用格式:保持系统现有标准 Drawing 图片格式;
    • WPS 单元格图片格式:生成与样本相同机制的 DISPIMG 单元格图片文件。
  3. 保持公式安全检查严格有效。只允许与包内受控图片一一对应的 WPS DISPIMG,不得直接放宽为允许任意公式。
  4. 两种格式使用相同的真实 PostgreSQL 资料、批次快照和 MinIO 文件,格式选择不得改变业务数据、任务状态或历史批次内容。

本方案不涉及短信发送、Redis Stream、队列、计费、余额、通道连接参数或部署架构调整。

2. 样本核验结果

样本:C:\Users\hectorzhao\Downloads\行业报备.xlsx

项目 实测结果
文件大小 20,976,525 字节
SHA-256 D5B5029B044EF5B8A86A68D4FE33E4F80D3AAB3A9B547D228145158D4B1518C1
工作表 行业
使用范围 A1:M171 行表头、16 行数据
WPS 图片公式 43 个,位于 H2:K17 的非空图片单元格
cellimages.xml 图片项 43 个
图片关系 43 个,和公式 ID 一一对应
业务图片 xl/media/image2.pngimage44.png,均为 PNG
业务图片原始字节合计 20,672,756 字节,约占整个文件 98.55%
图片像素范围 宽 4611226,高 276840
标准 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 加载工作簿,然后:

  1. assertSafeWorkbook 拒绝任何公式或疑似公式文本;
  2. readEmbeddedImages 只读取 worksheet.getImages()
  3. 根据 Drawing 左上角锚点换算图片所在行列;
  4. 提交导入时将图片上传至真实 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

  1. 从真实 MinIO 下载 PNG/JPEG/GIF 原文件;
  2. workbook.addImage 注册媒体;
  3. worksheet.addImageoneCell Drawing 锚点覆盖到目标单元格;
  4. 按通道配置调整列宽、图片显示范围和行高;
  5. 图片单元格本身仍保留文件名文本。

该格式是标准 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 包,只做受限结构解析:

  1. 验证 ZIP 路径规范化,拒绝绝对路径、.. 路径穿越和重复关键部件;
  2. 拒绝宏、ActiveX、OLE、外部链接、外部关系和非预期可执行部件;
  3. 读取 workbook、worksheet、关系文件、cellimages.xml 和媒体清单;
  4. 从原始 worksheet XML 提取真实公式单元格,避免 ExcelJS 因合并单元格复制公式造成重复图片;
  5. 建立 sheetName + row + column -> image 映射;
  6. 输出检测格式、图片数、冲突和安全诊断,再交给现有 ExcelJS 文本/样式解析流程。

本次样本在原始 XML 中是 43 个公式;ExcelJS 模型中会因合并单元格显示为 49 个公式。因此 WPS 图片定位必须以原始 worksheet XML 为准。

5.3 WPS 单元格图片解析器

新增 WpsCellImageReader,按以下顺序解析:

  1. 从 workbook relationships 找到且仅找到一个 WPS cellImage 部件;
  2. 解析 cellimages.xml 中每个 xdr:cNvPr@namea:blip@r:embed
  3. 通过 cellimages.xml.rels 解析内部媒体路径;
  4. 只接受包内图片关系,不允许 TargetMode="External"
  5. 从每个原始工作表单元格提取严格格式的 DISPIMG ID
  6. 校验公式 ID、cellImage ID、relationship ID 和媒体对象一一可解析;
  7. 返回与现有 EmbeddedImage 相同的 {row, column, extension, buffer},让后续映射、MinIO 上传和审核流程继续复用。

不把 cellimages.xml 中的 a:xfrm 坐标当作单元格地址。样本的业务单元格位置来自公式本身,ID 才是图片关联键。

5.4 公式安全策略

保留现有“默认拒绝所有公式”原则,仅为已验证的 WPS 图片单元格建立精确白名单。允许条件必须全部满足:

  1. 原始公式完整匹配:

    ^_xlfn\.DISPIMG\("ID_[A-F0-9]{32}",1\)$
    
  2. 公式所在工作表和单元格已由 OOXML 检查器登记;

  3. ID 在 cellimages.xml 中唯一存在;

  4. 图片关系是包内图片且目标文件通过类型、大小和签名校验;

  5. 一个公式只引用一张图片,一个 ID 不允许被不受控地复用到多个业务单元格;

  6. 除该单元格外,工作簿不存在其他公式、共享公式、数组公式、外部链接或疑似公式文本。

不要修改为“公式名包含 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 后处理:

  1. 先按现有代码生成标准 XLSX
  2. 根据标准 Drawing 锚点确定每张报备图片的目标单元格;
  3. 为每张图片生成唯一 ID_ 加 32 位大写十六进制标识;
  4. 创建 xl/cellimages.xml
  5. 创建 xl/_rels/cellimages.xml.rels
  6. 在 workbook relationships 增加 WPS cellImage 关系;
  7. [Content_Types].xml 增加 WPS cellImage 类型;
  8. 将目标单元格改写为严格的 _xlfn.DISPIMG("ID",1) 公式及缓存显示值;
  9. 移除已经转换的 Drawing 图片锚点和失去引用的媒体关系;
  10. 保留表头、文本、列宽、行高、冻结窗格、批次快照内容和未转换对象;
  11. 重新打包并再次执行包结构、关系完整性和公式安全自检。

这样可以复用现有成熟导出逻辑,避免为 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.tsimport-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

  • 导入格式、图片数量和诊断可写入现有导入批次 preview JSON
  • 导入后的图片仍是现有 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,不把包含真实企业和个人信息的样本提交到仓库:

  1. 2 张标准 Drawing 图片;
  2. 2 张有效 WPS 单元格图片;
  3. 标准/WPS 混合但不冲突;
  4. 同单元格冲突;
  5. 公式 ID 不存在;
  6. cellImage ID 重复;
  7. relationship 缺失、外链、路径穿越;
  8. 图片扩展名与文件签名不一致;
  9. 普通公式、共享公式、数组公式和伪造 DISPIMG
  10. 合并单元格下不重复计数;
  11. ZIP 项目数、单图、总解包大小超限;
  12. 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

  1. 分析样本但不审核应用,确认原业务签名不变化;
  2. 使用专用测试企业/应用提交少量合成行,确认 FileObject、导入批次和待审核项一致;
  3. 审核通过后确认图片字段落入真实材料、材料版本只按既有规则变化一次;
  4. 失败行不清空已有补资料字段;
  5. 删除测试数据前单独确认清理边界,不触碰真实客户资料。

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.mddocs/testing-progress.md

构建、ExcelJS 回读或合成 fixture 通过都不能代替真实 WPS 打开和真实 API/PostgreSQL/MinIO 验收。

11. 实施顺序

  1. 先做无业务依赖的最小 WPS OOXML 读写原型,使用 2 行、PNG/JPEG/GIF 小图在目标 WPS 和 Excel 365 实际打开。
  2. 固化包安全限制和 DISPIMG 精确白名单。
  3. 实现 WorkbookPackageInspectorWpsCellImageReader,接入分析和提交两阶段。
  4. 调整上传上限并加入解包、图片和内存保护。
  5. 实现标准 XLSX 到 WPS 单元格图片的纯转换器。
  6. 接入单条导出、批次单文件和批次 ZIP 三个后端入口。
  7. 实现共用格式选择 UI 和导入识别提示。
  8. 完成合成 fixture、当前样本、真实 MinIO、真实数据库、浏览器及 WPS/Excel 客户端验收。
  9. 更新测试用例和测试进度;是否提交、推送、部署分别等待明确授权。

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模式与提交的图片字节模式必须一致;清空损坏单元格后,其余图片定位及字节不变。补回缺失图片仍需用户提供原图,平台不能推断或借用相邻图片。