feat: support WPS report material workbooks

This commit is contained in:
hectorzhao
2026-09-04 17:10:04 +08:00
parent 48d0363920
commit fb39c8b606
29 changed files with 2737 additions and 734 deletions
+1 -1
View File
@@ -146,7 +146,7 @@ PROD_ADMIN_PASSWORD='change-me'
`API_ENABLE_SEND_WORKER=true` 是生产发送链路必填项。后续发布脚本会在构建和迁移前校验该开关以及正整数 `API_SEND_WORKER_CONCURRENCY`;缺失时直接终止发布,防止 API/Gateway 健康但 BullMQ 短信队列无人消费。
HTTP容量边界固定为:NestJS普通JSON/URL-encoded请求体`2 MiB`,仅`/api/client/send/imports/*`使用`25 MiB` JSON解析上限,原始CSV/TSV正文继续由业务层限制为`20 MiB`Gateway读取NestJS API响应最多`4 MiB`且超限必须明确报错。客户文件导入走`sms.lisglo.com`私有API,因此该虚拟主机的`client_max_body_size`必须不低于`30m`,标准bootstrap配置为`50m``api.lisglo.com`只承载单条公网HTTP API、Swagger和健康检查,不承载客户文件导入;不要为导入需求开放私有路由或把NestJS所有JSON接口统一放宽到25MiB。发布前使用`nginx -T`确认最终生效值,不能只检查仓库模板。
HTTP容量边界固定为:NestJS普通JSON/URL-encoded请求体`2 MiB`,仅`/api/client/send/imports/*`使用`25 MiB` JSON解析上限,原始CSV/TSV正文继续由业务层限制为`20 MiB`Gateway读取NestJS API响应最多`4 MiB`且超限必须明确报错。客户文件导入和运营端报备资料导入`sms.lisglo.com`私有API;报备XLSX允许最大100MiB并另设500MiB解压后总量限制,因此该虚拟主机的`client_max_body_size`必须不低于`110m`(包含multipart开销),标准bootstrap配置为`110m``api.lisglo.com`只承载单条公网HTTP API、Swagger和健康检查,不承载文件导入;不要为导入需求开放私有路由或把NestJS所有JSON接口统一放宽。发布前使用`nginx -T`确认最终生效值,不能只检查仓库模板。
Gateway 的最终 TPS 防线依赖与 API 相同的 Redis。通道连接时会写入 `rate:gateway:channel:config:<channelId>` 权威上限,实际预约使用 `rate:gateway:channel:<channelId>`;这些 key 不应在正常发布时清理。多 Gateway 实例必须指向同一 Redis,才能共享单通道额度。超速的 `gateway.submit.commands` 消息会保持在 consumer group pending 中等待,不应通过手工 `XACK` 或删除 Stream 处理积压;先检查通道配置、Redis key、consumer group 和 Gateway 日志。V2起Submit Worker使用持续补位有界池并逐条ACK,`GATEWAY_SUBMIT_WORKER_CONCURRENCY`缺省64、最大1024;调整前必须同时核对供应商连接数、窗口、TPS限制、Gateway RSS和`cmpp_gateway_submit_worker_slots`,不能用放大并发绕过通道限速。
+8
View File
@@ -5062,3 +5062,11 @@ npm run verify:phase8
| TC-REPORT-FIELD-EDIT-002 | 编辑已被通道或通用字段引用的字段 | 页面锁定代码和类型,允许修改名称和说明;绕过前端直接修改代码或类型时API返回400,既有通道映射和历史资料不受影响 |
| TC-REPORT-FIELD-ORDER-001 | 分别在签名、引流通用字段中点击上移/下移并刷新 | 仅在当前资料类型内整体重排,真实`sortOrder`按新顺序持久化;并发导致集合变化时明确失败,不产生部分更新 |
| TC-REPORT-WORKBENCH-025 | 在通道报备明细点击单条导出,并分别模拟空请求体和缺失必填资料 | 正常请求以JSON Content-Type提交并下载真实XLSX;空请求体返回可读400而不是500;资料错误明确返回且不静默生成空文件,不改变报备状态或触发短信链路 |
| TC-REPORT-WPS-001 | 上传`行业报备.xlsx`等使用`_xlfn.DISPIMG``xl/cellimages.xml`及关系文件的WPS XLSX | 仅精确白名单内且图片关系完整的DISPIMG公式可解析;图片按真实单元格映射进入预览与审核,透明Drawing占位图不计入;普通公式、宏、外部对象、缺失关系和非法图片格式均明确拒绝 |
| TC-REPORT-WPS-002 | 分别上传10MB以上且不超过100MB、超过100MB、解压后超过500MB的报备XLSX | 第一类可进入解析;后两类分别在上传层或解包层明确拒绝;Nginx私有站点允许100MB业务文件及multipart开销,不扩大公网单条API边界 |
| TC-REPORT-WPS-003 | 在批次和单条报备导出选择“系统 Excel 文件”或“WPS 单元格图片文件” | 默认保持ExcelJS Drawing格式;WPS选项生成DISPIMG与cellimages关系且图片按对应单元格可回读;两种格式均来自同一真实资料快照,不改变报备状态或短信链路 |
| TC-REPORT-IMPORT-APPLICATION-001 | 未选择企业应用、选择其他企业的应用、选择当前企业应用后解析导入 | 前两种前后端均阻止导入;合法应用可解析并把applicationId写入导入批次,后续补资料只作用于该企业应用范围 |
| TC-REPORT-FIELD-MODAL-001 | 打开签名或引流字段配置弹窗,添加字段并检查通用字段与移除按钮 | 左侧字段卡片高度不因添加改变;右侧默认包含同资料类型的通用字段;移除按钮为通用宽度、无红色填充,保存仍调用真实通道字段接口 |
| TC-REPORT-MATERIAL-DETAIL-001 | 查看包含当前图片字段、未删除历史字段及旧图片引用的报备资料 | 当前字段展示名称、代码、导出名及图片预览;未删除历史字段继续展示且同时显示字段名称和代码;图片可内联查看并保留下载入口,缺失内容显示明确占位 |
| TC-REPORT-CHANNEL-IDENTITY-001 | 打开短信通道管理的报备详情 | 页面同时明确展示“通道名称”和“通道编号”,列表、筛选、状态修改和窄屏布局不受影响 |
| TC-DASHBOARD-METRIC-ORDER-001 | 打开运营看板并按从左到右、从上到下读取指标 | 顺序为发送总量、消息分片数、总体成功率、到达率、活跃签名、消费金额、返还金额、计收金额、利润、利润率;所有数值继续来自真实API口径 |
+12
View File
@@ -4438,3 +4438,15 @@ git diff --check
- Browser插件本轮不可用,按前端调试流程使用工作区Playwright Chromium验证本地生产构建。1600×1000运营看板和质量矩阵、390×844运营看板页面身份、非空、错误层、控制台及横向溢出检查通过;菜单项上下内边距实测7px,矩阵完整保留通道提交、成功率、平均到达和提交失败信息。截图数据只用于布局验证,不冒充真实业务数据。
- 本地真实PostgreSQL启动后,新看板聚合代码直接执行成功,返回当日零短信下分片数、到达率、计收、利润和利润率均为0,确认SQL语法、表关联和零分母处理可运行;本地API健康HTTP 200。Redis未启动时API持续输出连接拒绝,故未将该不完整本地栈作为页面功能验收,验证后已关闭本地API、预览和PostgreSQL。
- 本轮只做本地提交,不推送、不部署,不访问或修改测试/预生产业务数据;不发送、补发、重投或重新入队短信,不修改余额、通道或客户配置。本节与源码、测试用例一并纳入本轮本地提交。
## 2026-09-04 WPS报备资料兼容与工作台修复(本地验证完成,待测试环境发布)
- 以本地`main``48d0363`为基线实施,本地相对`origin/main`领先2个提交且未落后;没有pull、切分支或回退。既有未跟踪`docs/report-material-pool-remediation-plan-20260903.md`保持原样,本轮方案文档单独纳入精确提交范围。
- WPS导入新增原始OOXML解析:精确识别`_xlfn.DISPIMG("ID_...",1)`,经`xl/cellimages.xml`及关系文件定位媒体,再与ExcelJS标准Drawing图片按单元格合并;普通公式、宏/外部对象、不完整关系和非白名单图片继续拒绝,没有整体放宽公式安全校验。
- 真实样本`C:\Users\hectorzhao\Downloads\行业报备.xlsx`为20,976,525字节,解析得到工作表“行业”、17行、13列、43张业务图片,图片总字节20,672,756A1的84字节透明Drawing占位图被排除。该只读样本未写回或覆盖。
- 报备导入前端和API均将企业应用改为必选,并由后端校验应用属于所选企业;XLSX压缩文件上限调整为100MiB,增加500MiB解压总量限制。Nginx私有站点模板同步调整为110m以容纳multipart开销;这会提高单请求资源峰值,因此仍保留文件数、部件数、压缩/解压体积和格式安全限制。
- 批次报备文件弹窗与两个单条导出入口均可选择“系统Excel Drawing”或“WPS单元格图片”;默认保持现有Excel格式,WPS格式由同一真实资料快照生成并可被新解析器回读,不改变数据库资料、报备状态或短信链路。
- 报备字段库“添加字段”移入字段定义区域;通道字段配置弹窗加载真实通用字段作为右侧默认项,固定左侧卡片最小高度,移除按钮改为非危险填充的通用宽度。通道详情同时展示名称和编号;资料弹窗展示图片、字段名称/代码/导出名,并为未删除历史字段回填字段库名称。
- 运营看板指标按业务阅读顺序调整为发送总量、分片数、总体成功率、到达率、活跃签名、消费、返还、计收、利润和利润率;只调整排列,不改变上一提交新增的真实聚合口径。
- 定向前端4文件12项、前端全量13文件63项、定向API3套20项、API全量53套608项通过;前后端TypeScript、Vite生产构建、依赖安全、部署契约、结构质量、增量ESLint/Prettier、入口包体积和`git diff --check`通过。ESLint仅保留3条既有Hook依赖warningVite仅保留既有Chart分块超过500kB提示,入口gzip 107.80KiB,低于250KiB预算。
- Browser插件及工作区Playwright依赖在当前会话不可用,尚未把组件测试或本地构建冒充真实页面验收;测试环境部署后的登录页、真实API/服务、控制台和登录后交互仍需继续核验。测试机当前网络可达,但已有非交互密钥认证返回`Permission denied (publickey,password)`,正在复用工作站既有安全认证方式,不在命令或日志中写入密码。
@@ -0,0 +1,463 @@
# 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:M17`1 行表头、16 行数据 |
| WPS 图片公式 | 43 个,位于 H2:K17 的非空图片单元格 |
| `cellimages.xml` 图片项 | 43 个 |
| 图片关系 | 43 个,和公式 ID 一一对应 |
| 业务图片 | `xl/media/image2.png``image44.png`,均为 PNG |
| 业务图片原始字节合计 | 20,672,756 字节,约占整个文件 98.55% |
| 图片像素范围 | 宽 4611226,高 276840 |
| 标准 Drawing | 3 个,均指向 A1 的同一张 1×1、84 字节 PNG 占位图 |
样本中每个图片单元格保存的不是标准 Drawing 锚点,而是以下公式:
```text
_xlfn.DISPIMG("ID_7C706627234641A7BCEA9177C8A3EACB",1)
```
其引用链为:
```text
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 专用关系:
```text
Type="http://www.wps.cn/officeDocument/2020/cellImage"
Target="cellimages.xml"
```
`[Content_Types].xml` 包含:
```text
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.addImage``oneCell` 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`:没有图片。
解析结果页显示只读提示,例如:
```text
已识别:WPS 单元格图片格式,43 张图片
```
混合格式按单元格合并。若同一单元格同时存在 WPS 单元格图片和标准 Drawing,视为冲突并要求用户修正,不静默选择其中一个。
### 4.2 单条报备资料导出
“导出报备资料”点击后打开格式选择弹窗:
- `Excel 通用格式`,默认选中;说明“图片为标准 Excel 图片,兼容 Excel 和 WPS”;
- `WPS 单元格图片格式`;说明“图片作为 WPS 单元格图片,Microsoft Excel 兼容性取决于版本”。
确认后才请求导出。关闭弹窗不发请求,生成中禁止重复提交,失败保留选择并展示后端真实错误。
### 4.3 报备批次导出
现有“报备文件导出”已经是弹窗,不再叠加第二层弹窗。在弹窗顶部增加同一格式选择:
- 下载单个通道文件时应用当前选择;
- “全部下载”时,ZIP 内所有 XLSX 使用同一种选择格式;
- TXT 简报不受影响。
Excel 通用格式继续使用现有文件名。WPS 文件增加 `_WPS` 后缀以免用户混淆,例如:
```text
2026-09-04_通道名_RBxxxx_WPS.xlsx
```
默认值始终为 Excel 通用格式,不改变既有用户的下载结果和接口行为。
## 5. 后端实现设计
### 5.1 增加统一格式枚举
```ts
type ReportWorkbookFormat = 'excel_drawing' | 'wps_cell_image';
```
所有入口统一校验该枚举,缺省为 `excel_drawing`。不接受任意字符串、文件扩展名或由前端提交的 OOXML 片段。
建议接口调整:
```text
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@name``a: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. 原始公式完整匹配:
```text
^_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` 调整为接收前置检查结果:
```text
标准 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.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
- 导入格式、图片数量和诊断可写入现有导入批次 `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.md` 和 `docs/testing-progress.md`。
构建、ExcelJS 回读或合成 fixture 通过都不能代替真实 WPS 打开和真实 API/PostgreSQL/MinIO 验收。
## 11. 实施顺序
1. 先做无业务依赖的最小 WPS OOXML 读写原型,使用 2 行、PNG/JPEG/GIF 小图在目标 WPS 和 Excel 365 实际打开。
2. 固化包安全限制和 `DISPIMG` 精确白名单。
3. 实现 `WorkbookPackageInspector` 与 `WpsCellImageReader`,接入分析和提交两阶段。
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 变体”。不在本次加入图片压缩、通道默认格式、派生文件缓存、历史数据回写或更多办公格式。