# 汇总大表 M0「月度版式重建器」规格(2026-10-06) > 状态:**【待验收】本轮只定规格与工作量,不含代码改动**;规格经用户审定后再开工。 > 依据:`docs/汇总大表数据驱动改造方案_2026-09-07.md`(含 2026-09-08 附录)、`HANDOFF.md` 的 2026-09-28 / 09-29 两节、2026-09-29 用户手工产出的 9 月母版。 ## 一、背景与目标 - **现状**:汇总大表的数据驱动回填(方案 M1–M3)已落地——`exportSummaryWorkbook` 已按「读库计算 → 回填母版 → 整本重算」运行(`ReportExportService.java:3634` 起,计算层 `reportexport/calc`)。 - **唯一卡点**:**月度列版式**。程序只按 `{year}年{month}月道路运输量汇总表.xlsx` 精确匹配母版(`ReportExportService.resolveSummaryMother`,同文件 4576 行),目标月没有对应母版就直接抛错,不会自动扩列。9 月母版是用户 09-29 在 Excel 里手工扩出来的(本机 `docs/生成汇总大表/2026年9月道路运输量汇总表.xlsx`、`_试做_20260929.xlsx`,均未入库)。 - **目标(M0)**:给定「一本可用母版 + 目标年月」,程序自动把列版式重建到目标月,产出可直接回填的母版版式,替代每月手工扩列。 - **出口验收**:① 重建后生成 2026-07,数值与已修正 7 月母版逐格 diff=0(仅列位变化);② 生成 2026-09,与用户手工 9 月母版逐格 diff=0。 ## 二、为什么是增量改造而不是重写 | 层次 | 现状 | 结论 | |---|---|---| | 计算层 | `reportexport/calc/{PaxCalc,FreightCalc,MidCalc,WycSplitCalc}`,约 900 行 | 直接复用 | | 回填层 | `SummaryWorkbookFiller`(899 行):`fillMonthlyLedger` / `ensureMonthFormulas` / `ensureMonthYoyFormulas` / `repairCumulativeYoyFormulas` / `yearMonthCols` / `usableMonthCols` / `cumulativeHelperCols` | 直接复用 | | 扩列原语 | `ReportExportService.java:1039-1200`:`moveColumnPair` / `cloneColumnPair` / `cloneCellForMonth` / `cloneMergedRegion` / `copyColumnWidth` / `fixCumulativeFormulas`;且已跑通 `extendMonthlyColumns`(独立小表 1–6 月 → 1–12 月) | 直接复用 | | **缺口** | 把上述原语**编排**到汇总大表 19 个页签里的 10 个长表页,并逐页配置规则 + 回归自检 | **本次新增** | 已有投入(不可丢弃):38 个提交、930 格差异核对、A1–E3/Q1–Q3 一轮口径拍板。重写不划算。 ## 三、列版式规则(0 基列号) | 项 | 规则 | |---|---| | 当月值列 | 1 月 = 2(C 列);m 月 = `2 + 2 × (m − 1)` | | 当月同比列 | 当月值列 + 1 | | 累计列 / 累计同比列 | 2026 组末尾,当前 14/15(O/P);扩到 12 月后为 26/27 | | 2025 及更早参照组 | 每扩一个月整体右移 2 列 | | 已确认口径(方案 D1) | 当月值列+同比列插在累计之前、累计滚动、2025+ 参照组右移——等价于本文件的「月度版式重建器」 | `SummaryWorkbookFiller.FIRST_MONTH_COL=2`(同文件 47 行)与该公式一致,但属硬编码锚点,见第七节第 4 条。 ## 四、需要扩列的 10 个长表页(数字取自方案 v1 附录实测) | 页签 | maxCol | 合并区 | 需平移的公式数 | |---|---|---|---| | ` 货运`(注意前导空格) | 145 | 140 | 6638 | | 班线包车 | 285 | 204 | 5688 | | 中口径明细 | 87 | 157 | 6654 | | 公交 | 86 | 76 | 2004 | | 出租 | 86 | 76 | 1872 | | 网约车 | 86 | 70 | 1664 | | 公路总客运 | 274 | 194 | 未单独统计 | | 城市客运 | 72 | 55 | 未单独统计 | | 轨道、轮渡 | 72 | 110 | 未单独统计 | | 中口径客运量 | 68 | 111 | 未单独统计 | 已知 6 页公式平移合计约 **24,520 处**(公交 2004 / 出租 1872 / 网约 1664 / 中口径明细 6654 / 货运 6638 / 班线 5688)。 ## 五、不需要扩列的页 - **6 个成品页**(当月块与累计块是行结构、列位固定,直接按映射填值/公式):货运量排名、货运周转量排名、中口径排名、城市汇总、城市分市州、道路运输周转量。 - **3 个静态页**(不动):中口径分析、2025年城市客运量、中口径计算过程。 ## 六、改动文件清单 - 新增 `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/layout/SummaryLayoutRebuilder.java`:编排层,按目标月重建 10 个长表页的列布局。 - 新增 `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/layout/SummarySheetRule.java`:逐页规则配置(表头行、数据起止行、月份组起点、累计列位、是否派生页、合并区策略)。 - 新增 `traffic-audit-server/src/test/java/com/trafficaudit/reportexport/layout/SummaryLayoutRebuilderTest.java`:扩列后 7/9 月数值回归自检。 - 改造 `ReportExportService.exportSummaryWorkbook`(同文件 3634 行起):母版缺失时不再抛错,改为「取最近可用母版 → 重建到目标月 → 继续回填」。 - 改造 `ReportExportService.resolveSummaryMother`(同文件 4576 行):放开"仅精确匹配"限制,保留"找不到任何蓝本才报错"。 - 可选(建议做):把 `SummaryWorkbookFiller.FIRST_MONTH_COL` 硬编码改为按表头 `yearMonthCols` 语义定位,避免母版挪列后静默写错列。 - 前端:无需改动(入口与参数不变)。 - 文档:本规格 + `HANDOFF.md` 结论;`_试做_20260929.xlsx` 等过程产物不入库。 ## 七、关键算法 1. **选蓝本**:目标月母版存在则直接用;不存在则取最近可用母版(默认 9 月手工版,见第十节待确认项 1)。 2. **定位**:复用 `yearMonthCols` 的表头识别(支持文本 `2026年8月` 与数值日期序列表头,只认今年/去年),在前 5 行内找出各月份组的实际列位——先认位置再动列,不盲信固定列号。 3. **重建**(对每个长表页):移动累计列对到 2026 组末尾 → 按月克隆月份列对(表头文本/日期修正、样式、合并区、列宽、跨页公式列引用同步)→ 修正累计与累计同比公式。 4. **重算与自检**:`setFullCalcOnLoad(true)` + 整本重算;输出前跑「#REF! 计数 = 0」「累计列覆盖到当期」自检,不通过则中止。 ## 八、验证方案(M0 出口) | 编号 | 用例 | 预期 | |---|---|---| | 1 | 重建版式后生成 2026-07 | 与已修正 7 月母版被填区域逐格 diff=0,仅列位变化 | | 2 | 生成 2026-09 | 与用户手工 9 月母版逐格 diff=0 | | 3 | 公式自检 | 全簿 `#REF!` = 0;累计/累计同比覆盖到当期 | | 4 | 版式自检 | 合并区、列宽、隐藏标记、打印区与手工 9 月母版一致;Excel/WPS 打开无修复告警 | | 5 | 负向:目标月源数据缺失 | 给出缺数清单,不静默输出空表 | | 6 | 负向:母版被改导致月份表头识别不到 | 明确报错,不猜测列位 | ## 九、工作量预估 | 阶段 | 预估 | |---|---| | 规格表定稿(本文件 + 用户审定) | 0.5 天 | | Rebuilder + 逐页规则 + 单测 | 1.5–2.5 天 | | 7/8 月回归 + 9 月对照核对 | 1 天 | | 风险缓冲 | 0.5–1 天 | | **合计** | **约 3.5–5 天**(含测试与验收) | 说明:`HANDOFF.md` 09-29 节曾估「0.5~1 天」,那只是扩列原语的复用估计;按附录实测(约 2.45 万处公式平移 + 2 千余处合并区)重估为 3.5–5 天。 ## 十、风险与待确认 1. **待确认:以哪本作扩列蓝本。** 建议以用户手工的 9 月母版为准(版式已由用户固化)。 2. **待确认:是否一次建成 12 月满版式。** 方案 v1 建议满版式、后续月份只做「隐藏/置灰未到月份 + 累计重算 + 填当月」,可显著降低后续风险;若坚持逐月插入,复杂度与回归成本更高。 3. **母版历史坑要一并覆盖**:母版由 openpyxl 写出,存在高压缩(已用 `ZipSecureFile.setMinInflateRatio` 绕过)与残留 `` 非法格(`normalizeStaleInlineStrings` 清理)两类问题,逐列克隆时需保持这两处处理。 4. **城市客运页参照组是压缩结构**(同比 + 1-X 月累计),与其它长表页不同,需单独规则。 5. **跨年开新本(2027)不在 M0 范围**,需另定"新年块"规则。 6. **未覆盖页**:中口径分析等 3 个静态页仍沿用母版静态内容,本阶段不改。 ## 十一、参照同事「专项统计」的设计映射与落地路线(2026-10-08) ### 11.1 对齐表:他为什么能一个母版,我们对应怎么做 | 同事专项统计的做法 | 他为什么能成立 | 汇总大表的对应做法 | |---|---|---| | 一份母版(`2026年十一数据.xlsx`),**天为行、年为列** | 增长维度是「年」,一年加一组列;天数固定 8 | 冻结《道路运输量汇总表_标准母版.xlsx》:**12 个月满版式一次到位**,只读 | | 母版同时是数据容器(历史年份的值就在并排年列里) | 历史值随模板一起走 | **反过来做**:历史参照值入库,母版只留版式,参照组由 DB 回填 | | 表头/行标签语义定位(`findYearColumn` / `parseDayIndex`) | 列序变了也能导 | 新增页规则表 `SummarySheetRule`:按表头认月份组、按行标签认市州/指标,不用硬编码列锚点 | | 易漂移口径外置成表(`holiday_calendar` / `passenger_coefficient` / `vehicle_adjustment`) | 规则不散在代码与单元格里 | 参照年保留组数、排名口径(全省不排名)、周转量系数 B=G+L/2+Q/10、缺数置空规则 外置 | | `DECIMAL(30,15)` + `BigDecimal` + 按 Excel 显示精度取值 | 精度口径统一,无浮点尾差 | 承接长期待办「16 张表 228 个 double 列改 DECIMAL + BigDecimal」 | | 导入两阶段确认 + 全程只读核对 | 历史数据不被误覆盖 | 生成前先出缺数/差异清单,人工确认后再出表(现有 B4 基数体检补齐) | | 输出走模板文件(docx / PNG) | 版式来源单一 | 保留现有做法(快报改 `word/document.xml`、JFreeChart 出 PNG) | ### 11.2 落地路线(四阶段) - **阶段 1(= M0,3.5–5 天)**:标准母版冻结 + 满版式 + 跨年重建 + 页规则表。 新增 `SummaryLayoutRebuilder` / `SummarySheetRule` / 单测。 出口:7 月回归 diff=0;9 月对照手工版 diff=0;10 月起可直接出表。 - **阶段 2(+1–2 天)**:把现有三份文件依赖合并为一份。 现状依赖:月度母版(`resolveSummaryMother`)+ `_备份_` donor(`resolveSummaryDonor` / `restoreSummaryMonthlyBlocks`)+ 版式参照件(`resolveSummaryLayoutRef`)。 目标:只留《道路运输量汇总表_标准母版.xlsx》,参照值从库回填,停用 donor 与还原机制。 出口:`docs/生成汇总大表/` 下只剩 1 个模板文件 + 输出目录(`_bak_*` 等本地留存、不入库)。 - **阶段 3(可选,+2–3 天)**:母版内用命名区域/占位符标注需填格,程序按名填写;母版被挪列/插行时明确报错。 - **阶段 4(可选,+5–10 天)**:把 10 个长表页的版式参数化(列组规则 + 合并区 + 列宽 + 数字格式 + 打印区),程序从版式定义渲染,母版文件降级为一次性样式提取源; 3 个隐藏公式页(中口径计算过程 / 2025年城市客运量 / 中口径分析)保留文件形态。 ### 11.3 两条不照搬 1. **不把 Excel 当数据源**:同事那边业务每天填同一形状的 Excel 文件;汇总大表数据必须走 DB 导入 + 两阶段确认,不能退回"手工填母版"。 2. **必须有「新年组」逻辑**:他的天数是常数(8 天),跨年只是加一组年列;我们跨年要新增下一年度列组并把当年组降级为参照组,这一步他没有对应实现。 ### 11.4 底线与风险 - 3 个隐藏公式页依赖母版现成公式,阶段 1–2 必须原样保留,阶段 4 才谈重建。 - 打印区、页眉页脚、条件格式、列宽必须逐页核对,是"肉眼版式"最易漏的点。 - 业务改指标行/页签/边框属于需求变更,任何方案都要一次适配;本方案把频率从「每月一次」降到「偶尔一次」。 - 母版改为只读,可顺带规避 WPS/openpyxl 重存带来的高压缩与残留 `` 非法格两类历史坑。