# 汇总大表 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 重存带来的高压缩与残留 `` 非法格两类历史坑。 ## 十二、存储层/展示层约定:参照同事「二维表」模式(2026-10-09) ### 12.1 三层职责 - **存储层(DB)**:一张张二维表,键与值明确;存三类内容——原始值、配置值、人工调整值。 - **计算层**:导出时按口径计算(累计、同比、环比、占比、排名)。 - **展示层(Excel)**:只把值和公式按版式摆出来,不承担存储职责。 ### 12.2 「一表一粒度」约定(示例取自同事的专项统计) | 表 | 主键(粒度) | 值 | 类别 | |---|---|---|---| | holiday_passenger_flow | 年 + 日期 + 运输方式 | 客运量 | 原始值 | | holiday_vehicle_flow | 年 + 日期 + 道路类别 + 车型 | 车流量、环比基期 | 原始值 | | holiday_calendar | 年 + 节假日类型 + 第几日 | 日期 | 配置值 | | holiday_passenger_coefficient | 年 + 日期类别 | 客运系数 | 配置值 | | holiday_vehicle_adjustment | 年 + 日期 | 调整后值 + 原因 | 人工调整值 | 我们现有导入表同样满足该约定:`city_bus_monthly`(报表期 + 地区 + 企业)、`city_taxi_monthly`(报表期 + 地区)、`freight_turnover_import`(报表期 + 地区)、`scale_split_transport`(报表期 + 地区 + 月/累计)、`h2031_enterprise_monthly`(报表期 + 企业)、`h2032_enterprise_monthly`、`wyc_order_monthly` / `wyc_total_monthly`、`passenger_individual_monthly`。存储层形态是对的。 ### 12.3 人工调整值一律单独建表(覆盖层) - 模式:**原始记录不动**,另表存「覆盖值 + 原因 + 时间」,唯一键与所覆盖对象一致(参照同事的 `holiday_vehicle_adjustment`)。 - 我们的适用对象:2025 参照值修正、异常站剔除、业务临时改数等。 ### 12.4 现状核验(2026-10-09 实测,修正早期判断) 实测库 `127.0.0.1:3308 / traffic_audit`(MySQL 8.0.29): | 表 | 2025 月份数 | 结论 | |---|---|---| | city_bus_monthly | 12/12 | 齐全 | | city_taxi_monthly | 12/12 | 齐全 | | wyc_order_monthly / wyc_total_monthly | 12/12 | 齐全 | | h2031_enterprise_monthly / h2032_enterprise_monthly | 12/12 | 齐全 | | passenger_individual_monthly | 12/12 | 齐全 | | freight_turnover_import | 1/12(仅 2025-12) | **不构成缺口**:表带 `last_freight_m01..m12` / `last_turnover_m01..m12`,2026 各期文件自带去年同月值 | | scale_split_transport | 0/12 | **不构成缺口**:表带 `above/below/total_yoy`,同比已随 2026 各期入库 | - 早期"库里没有 2025 年城市客运数据"的注释与提示文案(`ReportExportService.java:499 / 558 / 561`)**已过期**,触发条件不再成立;同比取数走的是库内去年同期(`aggOf(data, year-1, m, "湖北省")`,约 540 行)。 - **真正遗留的是口径问题,不是缺数据**:中口径排名/明细的 2025 列仍以**母版静态定稿值**为准(`readMidDetailLastYearCumulative`,`ReportExportService.java:2745` 起),代码注释(2686-2689 行)说明原因——"排名页原先查库现算,两者在 **2025-03/04 网约车拆分**上不同"。 - 处置:先做「库内 2025 vs 母版 2025」逐项 diff(中口径 4 组件 × 17 市州 × 12 月 + 累计),交业务确认以哪边为准,再决定是否切换读库。 ### 12.5 结果层(比同事更进一步,可选) - 同事库里**不存计算结果**:累计、同比、占比、环比均导出时算,Excel 内表现为公式。 - 我们可选增加一张结果表,粒度 = 期间 + 页 + 指标 + 市州 + 当月/累计;Calc 写入、Filler 读取。 - 收益:小表与跨页合成页共用一套结果;逐格 diff 时能直接区分「算错」还是「填错」。 - 约定:按期间**幂等重算**(先删该期间再插);显示取整不下沉到库,只在展示层舍入。 ### 12.6 自检纪律(红线) > **母版里不允许存在「只有它才有的数据」。** 判定方法:把母版所有值清空后,程序能否重新生成一张完全一致的表?能 → 数据都在库;不能 → 还有数据藏在 Excel 里,继续往库里搬。 ### 12.7 与 M0 的关系(依赖顺序) - 12.4 的口径核对不阻塞 M0 阶段 1(版式重建)。 - M0 阶段 2「停用 `_备份_` donor 还原」的数据前提**已基本满足**(2025 已在库);但仍需先关闭 12.4 的差异,否则切换取数来源会让数字变动、无法通过 diff=0 回归。