feat(corporate-bank-account-payment-order): 基于原模板生成建行制单

这个提交包含在:
2026-08-03 01:18:10 +08:00
父节点 91564d6774
当前提交 23c3768c46
共修改 19 个文件,包含 713 行新增和 152 行删除
@@ -133,14 +133,15 @@ POST 制单接口接收 `{ reason }`,仅允许明确 finance/finance_mgr 业
禁止部分成功:任意一项失败时不生成文件、不更新制单状态、不写成功导出审计。
### 6.3 BIFF8 与下载
### 6.3 BIFF8 模板填充与下载
- 使用 SheetJS 官方精确 `0.20.3` tarball并锁定 package-lock 完整性。
- 运行时代码只调用生成 API,不解析上传文件或用户提供的工作簿。
- 显式构建七个字符串字段的 `{ t: "s", v: value }` 单元格;金额使用数值类型和两位小数格式。
- 工作簿按顺序创建 `Sheet1`、`Sheet2`、`Sheet3`,Sheet1 固定 15 列表头。
- 内存生成 `bookType: "biff8"` buffer,先完成审计和制单计数事务,再设置 `application/vnd.ms-excel`、安全附件名、`nosniff`、`private, no-store` 等响应头并发送。
- 不写对象存储或服务器持久目录。网络中断后的已记录导出按保守口径视为已生成,用户可填写新原因重导。
- 仓库保存一份经脱敏和元数据检查的固定空白 BIFF8 模板资产;它保留三个工作表、15 列表头和三行预格式化空数据行,不含真实业务数据。运行时校验固定 SHA-256,不解析上传文件,也不接受请求控制模板路径。
- Node 继续负责解密、资格校验、聚合 blockers、排序、流水号、金额和审计;固定 Python 填充器只接收已经校验的行 DTO,不承担权限或状态机判断。
- Node 通过子进程 stdin 发送 JSON,Python 从 stdout 返回 BIFF8 二进制。完整账号不得进入 argv、env、日志或 stderr;子进程使用固定解释器路径、超时和 `maxBuffer`,并校验退出码、输出大小与 OLE 魔数。
- Python 使用固定版本 `xlrd==2.0.2`、`xlwt==1.3.0`、`xlutils==2.0.0`,以 `XLWTWriter + process(XLRDReader(...))` 复制原工作簿,再按模板首个数据行样式写值。生产镜像与 CI 从同一份 `--require-hashes` requirements 安装到专用 venv。
- 模板已有三行数据样式;超过三笔时复制第一个数据行的样式并设置相同行高。金额列复制原样式后只把 `num_format_str` 改为 `0.00`,七个标识字段以字符串写入。
- 生成器不再依赖 SheetJS;`xlsx` 依赖从服务端移除。先完成审计和制单计数事务,再设置 `application/vnd.ms-excel`、安全附件名、`nosniff`、`private, no-store` 等响应头并发送。
- 不写对象存储、临时制单文件或服务器持久目录。网络中断后的已记录导出按保守口径视为已生成,用户可填写新原因重导。
## 7. 页面动作与进度
@@ -164,10 +165,11 @@ POST 制单接口接收 `{ reason }`,仅允许明确 finance/finance_mgr 业
## 9. 验证与依赖门禁
- Node 单元/路由测试覆盖权限矩阵、单一启用账户并发约束、通知、审计、批次快照、历史拒绝、CCB-only、确定性排序、重导和隐私负向断言。
- 使用合成数据生成 `.xls`,由 CI 明确安装的 `xlrd==2.0.2` 独立读回三个工作表、15 列、七个字符串字段、金额、中文和重复导出一致性。
- 使用合成数据生成 `.xls`,由 CI 的固定 Python 环境独立读回三个工作表、15 列、七个字符串字段、金额、中文和重复导出一致性,并比较列宽、行高、字体、加粗、底色、边框、对齐等模板样式签名。
- 覆盖超过三笔的扩展行、模板 hash 漂移、解释器/依赖缺失、子进程失败、输出越界和 stderr 隐私负向断言。
- 用合成非建行项证明整批失败且没有文件、制单计数或成功审计。
- Excel/WPS 人工打开验证中文、长字符串和金额;企业网银只有存在明确“仅校验”步骤时才使用虚构建行数据做最终确认。
- 安装依赖后运行 npm audit/许可证与 lockfile 审查;如官方精确 tarball不可重复获取或引入不可接受告警,暂停而非换用 registry 旧版。
- 固定 Python requirements 必须带完整哈希并在 CI/镜像使用 `--require-hashes`;任何包或模板哈希不符时暂停,不允许联网浮动安装或自动降级到重建工作簿。
## 10. 迁移、上线与回滚
@@ -81,3 +81,25 @@
- 治理:Trellis task 校验、governance、process contract、敏感内容扫描和 `git diff --check` 通过;文档注册审计仅保留仓库既有 22 个未登记文件警告。
- 依赖:`[email protected]` 来自官方精确 tarball且锁定;生产依赖审计仍有 14 项仓库既有告警,未执行破坏性 `npm audit fix --force`。
- 尚未完成:Excel/WPS 人工打开、前端 E2E/截图、生产数据库迁移、真实资料配置、部署、公网检查和企业网银校验。
## 11. 原模板保真修订(2026-08-03)
- [x] Excel 人工打开否决 SheetJS 重建输出:字段合同正确,但原模板列宽、行高、字体、底色、边框和对齐丢失。
- [x] 用户批准改为“固定脱敏原模板 + Python BIFF8 保真填充”,不再接受重建工作簿方案。
- [x] 提交经敏感检查的空白模板资产并固定 SHA-256;不提交 `input/` 原件或任何生成制单文件。
- [x] 用固定 Python requirements 实现 stdin JSON / stdout BIFF8 填充器,stderr 仅固定错误码。
- [x] Node 改为子进程桥接,保留现有资格、冻结快照、blockers、排序、流水号、审计与导出统计合同。
- [x] 生产镜像和 CI 使用同一带哈希 requirements 和绝对 venv 解释器;移除服务端 SheetJS 生成依赖。
- [x] 独立读回增加模板视觉签名、超过三行扩展、模板/运行时失败和隐私负向测试。
- [x] 重新运行聚焦/全量后端、前端、治理和只读复核;用合成数据交付 Excel 人工复核,不部署。
### 修订验证记录
- 受控模板仅有 15 个表头和三行预格式化空数据行;模板 SHA-256 与运行时常量一致,字符串扫描未发现账号、URL、凭据或业务数据。
- 固定 Python 环境的独立读回已确认三个工作表、15 列、账号/`01` 文本、数值金额 `0.00`,并逐格比较列宽、行高、字体/加粗、底色、边框、对齐;第四笔扩展行同样通过。
- 聚焦测试在资格门禁修订后为 70 项通过;新增覆盖 admin 非财务拒绝、四种非 reviewed 状态、历史 item 非银行 override、模板 hash、依赖缺失、子进程超时/输出越界和 stderr 隐私。
- 后端全量最终为 176 个文件、1199 项全部通过;此前 Dockerfile 缓存层文本合同失败已通过恢复独立 `RUN cd server && npm ci` 修复并纳入最终全量。
- 前端 typecheck/build、Prisma validate、流程合同、Trellis governance、敏感扫描和 diff 检查通过;文档审计仅保留仓库既有 22 个未登记文件警告。
- 本地 Docker 构建因 Alpine 包下载长时间无进展被主动取消,未形成镜像通过证据;生产镜像、部署、公网与网银验证仍待单独批准。
- 当前实现已生成并独立读回一份 gitignored 合成 Excel 验收文件;原型文件此前经用户 Excel 人工打开认可,当前实现文件仍可用于最终人工外观确认。
- 付款/隐私只读复核在资格门禁修订后确认无 Blocker、High 或 Medium;唯一 Low(失败路径缺显式“无成功审计”断言)已补测试并复验。
@@ -59,14 +59,16 @@
### R4 银行制单导出
- 从符合状态与权限要求的付款批次生成与空白模板一致的 BIFF8 `.xls`。
- 从符合状态与权限要求的付款批次生成与受控空白原模板视觉和结构一致的 BIFF8 `.xls`;不得重建一个仅字段相同但丢失列宽、行高、字体、底色、边框或对齐的工作簿。
- 银行制单是推荐的效率工具而非付款完成硬门禁;旧快照、格式异常或人工付款场景仍可通过线下转账、上传回单和登记完成闭环,银行回单继续是银行渠道付款完成的证据门禁。
- 新批次已复核、资料完整且尚未制单时,顶部主动作显示“下载银行制单”;首次制单后主动作切换为“上传银行回单”,重新下载进入“更多”。已经人工付款的用户可直接在回单区域上传,不被制单状态阻断。
- 批次详情显示是否已生成制单;处理进度在未制单时显示“等待财务线下付款并上传回单”,制单后显示“已生成银行制单,等待付款完成并上传回单”。
- 仅 `reviewed`(待财务付款)批次允许制单;`draft`、`failed`、`cancelled` 与 `executed` 均禁止制单。
- `reviewed` 状态允许因文件丢失或本地操作失败重复导出,每次仍须填写原因并独立审计;重新下载前必须明确提示“重新下载不会作废之前的文件。如果上一份已在网银导入,请勿重复提交。”重复导出的业务字段必须来自同一冻结快照,不允许临时覆盖账号、金额或用途。
- `executed` 后不得重新生成可导入网银的指令文件;付款归档依赖银行回单与付款台账,避免把归档需要和可重复付款指令混为一体。
- 原样保留三个工作表、15 列顺序与表头;不得把格式伪装成 `.xls` 扩展名。
- 原样保留三个工作表、15 列顺序与表头,以及原模板的列宽、表头/数据行行高、字体、加粗、底色、边框、对齐和换行;不得把格式伪装成 `.xls` 扩展名。
- 仓库可提交一份经脱敏检查、仅含空白表头和预格式化空行的受控模板资产;运行时只读取该固定资产,不接受用户上传模板或请求指定路径。模板哈希漂移时制单失败关闭。
- 模板预置数据行不足时,新行必须复制首个数据行的样式和行高;金额列仅覆盖为数值类型与 `0.00` 格式,不改变其他视觉属性。
- 用途固定为“报销款”;客户方流水号由付款批次编号与三位行序号生成;备注写对应报销单编号;开户行名称取冻结快照;付款凭证号不进入制单。
- 批次项必须使用显式、稳定且带唯一兜底键的排序,禁止依赖数据库自然顺序;同一批次重复导出时每项的行序号和客户流水号必须不变。
- 客户方流水号由单一生成函数负责,并在写文件前校验长度、字符集和批次内唯一性;不允许静默截断或分散内联拼接。首轮验证若证明银行只接受纯数字,应只调整该生成函数与对应合同。
@@ -74,20 +76,21 @@
- 金额首版按数值写入并设置两位小数格式,必须无科学计数法和精度损失;若银行安全导入验证明确拒绝,再改为两位小数字符串。
- 收款人名称、用途与备注中文不得乱码。
- 收款方联行号首版固定留空;首次安全验证使用字符串 `01`、建行虚构账号和空联行号,覆盖真实生产主场景。跨行 `02` 与联行号留待支持非建行员工时另立增量任务。
- 导出文件通过响应直接下载,不持久化到服务器存储,不进入 Git,不记录原始制单内容。
- 导出文件通过响应直接下载,不持久化到服务器存储,不记录原始制单内容;只有脱敏空白模板资产可以进入 Git。
- 完整账号只可通过子进程标准输入传给固定填充器,不得进入命令行参数、环境变量、日志或错误输出;生成的二进制只从标准输出返回,并设置明确的大小上限。
- 导出使用独立权限、必填原因和逐次审计;审计只记录操作人、批次、笔数、合计金额及原因,不记录完整账号。
- 旧快照批次、非银行渠道、缺少付款账户、缺少员工银行标识或账号、公司或员工银行不是建行的批次必须在导出前一次性列出不可制单原因。
### R5 本地与人工验证
- 使用合成数据生成 `.xls`,再用独立于生成器的解析器读回验证工作表、表头、字段类型、中文与金额。
- 使用合成数据生成 `.xls`,再用独立解析验证工作表、表头、字段类型、中文、金额和模板视觉签名;至少覆盖列宽、行高、字体、加粗、底色、边框和对齐。
- 独立解析器及版本必须进入可复现的测试环境,不能依赖某台开发机偶然安装的 `xlrd`。
- 至少使用 Excel 或 WPS 人工打开一次,确认中文和账号显示。
- 只有企业网银存在明确的“仅校验、不提交”环节时,才允许用完全虚构数据做最终格式确认;否则不进行生产网银试验。
### R6 依赖安全
- BIFF8 生成依赖必须在设计阶段确定受支持版本、来源、锁定和完整性校验方式。
- BIFF8 生成依赖必须在设计阶段确定受支持版本、来源、锁定和完整性校验方式;CI 与生产镜像必须使用同一份带哈希的固定 Python requirements。
- 不得静默接受已知高危依赖告警;若只能使用存在已知解析漏洞的版本,必须证明运行路径只生成、不解析不可信输入,并单独获得风险例外批准。
## Test Plan
@@ -116,6 +119,7 @@
- [ ] 未制单批次、已制单批次和人工付款路径均可按既定主动作/次动作继续,制单状态不是上传回单或登记付款完成的硬门禁。
- [ ] 批次详情和处理进度能区分“未生成制单”与“已生成制单待回单”,便于观察实际使用覆盖率。
- [ ] 授权财务人员填写原因后可下载符合模板的 BIFF8 `.xls`,下载行为留下脱敏审计。
- [ ] Excel 打开生成文件时,原模板列宽、行高、字体、加粗、底色、边框、对齐和换行保持不变;超过三笔时新增行仍复制相同数据行样式。
- [ ] 重新下载前展示重复付款风险原文;每次重导独立记录原因,但不会宣称旧文件已失效。
- [ ] 只有 `reviewed` 批次可制单;同一 `reviewed` 批次可带新原因重导,其他四种状态均被明确拒绝。
- [ ] 批次进入 `executed` 后无法重新生成银行制单,银行回单与付款台账仍可用于归档核验。
@@ -126,6 +130,7 @@
- [ ] 使用字符串 `01`、建行合成账号和空联行号完成本地验证;合成非建行项证明整批失败且不会生成部分文件。
- [ ] 缺失资料或状态不合格时,页面一次性说明全部阻塞项及处理方式。
- [ ] 自动化、本地人工打开和可选网银校验均使用合成数据,不触发真实付款。
- [ ] 模板哈希漂移、Python 或固定依赖缺失、填充器失败时返回固定安全错误,stderr、日志和测试证据均不含完整账号或原始 payload,人工付款路径不受影响。
## Out of Scope
@@ -134,4 +139,4 @@
- 首版支持多个同时启用的公司付款账户。
- 非建行、跨行 `02`、联行号维护和混合银行批次制单。
- 为历史付款批次补写或回填完整账号快照。
- 将真实账号、模板业务数据或生成的制单文件提交到仓库。
- 将真实账号、含业务数据的模板或生成的制单文件提交到仓库;经检查的脱敏空白模板资产除外。
@@ -6,7 +6,8 @@
- 工作簿固定包含 `Sheet1`、`Sheet2`、`Sheet3`;只有 `Sheet1` 有 15 列表头,另两个工作表为空。
- 必填列为序号、付款方账号/名称、收款方行别代码、收款方账号/名称、金额、用途。
- 数据行多数使用文本格式;账号、行别代码、联行号、流水号、手机号和短信标志必须在写入值时显式使用字符串类型,不能依赖显示格式补救。
- 模板文件只作为本地分析输入,不进入 Git、Trellis 或运行时服务器。
- 2026-08-03 Excel 人工打开证明 SheetJS 重建工作簿虽满足字段合同,但无法保留原模板列宽、行高、宋体/加粗、底色、边框和对齐,因此该方案被否决。
- 原始 `input/` 文件仍只作为本地分析输入;实施允许从中制作一份经脱敏、元数据和样式签名检查的受控空白模板资产进入 Git 与运行时镜像。含业务数据的模板和生成文件仍禁止提交。
## 当前数据与权限
@@ -26,15 +27,15 @@
## BIFF8 依赖结论
- npm registry 的官方 `xlsx` 版本停在 `0.18.5`,不能作为无例外默认依赖。
- SheetJS 官方 CDN 可取得 `0.20.3` tarball;实施时应使用精确版本 URL并让 `package-lock.json` 固定完整性,不使用浮动 URL。
- 运行时代码只调用工作簿/工作表构建和 `write(..., { bookType: "biff8" })`,不得开放 `read`/`readFile` 解析用户输入。
- 也存在 npm 上的第三方发布镜像,但官方精确 tarball的来源更直接;若正式安装前完整性或许可证门禁不通过,停止并重新审查,不静默换包。
- SheetJS CE 即使读取模板并启用样式选项后再写 BIFF8,仍无法保真复制字体、底色、边框、对齐和行高,因此从运行时生成路径移除。
- 隔离验证确认 `xlrd==2.0.2`、`xlwt==1.3.0`、`xlutils==2.0.0` 使用 `XLWTWriter + process(XLRDReader(...))` 可保留原模板视觉合同;金额列可在复制样式后单独覆盖 `0.00`。
- 固定 Python 填充器只读取仓库内受控模板,只从 stdin 接受 Node 已校验的合成行 DTO,并把二进制写 stdout;不接受上传模板或外部路径。
- Python 包必须以带哈希 requirements 固定,并由 CI 与生产镜像同源安装。解释器、依赖、模板或哈希异常均 fail closed,不回退到无样式生成。
## 独立读回验证
- 当前开发机可用 `xlrd 2.0.2`,但仓库没有 Python requirements/pyproject,不能把本机安装当成 CI 合同。
- 实施应增加一个最小、固定版本的独立验证环境:生成器由 Node/SheetJS 执行,读回器由 Python `xlrd==2.0.2` 执行;CI 明确安装该版本后运行合成文件验证。
- 当前开发机可用 `xlrd 2.0.2`,但本轮需新增最小、带哈希的固定 requirements,使生成和读回环境在 CI 与镜像可复现。
- 生成由固定 Python 模板填充器完成;合同脚本再以独立打开步骤读回并比较原模板样式签名,而不是依赖生成器返回的内部对象。
- 验证文件必须写入临时目录并在结束后清理;失败输出只报告字段名和断言,不打印合成账号之外的任何内容。
## 关键设计约束