zhizhijie
2026-09-18 8719902780eadfc55d4d159d5a111b433f065137
功能(中口径): 四维口径统一 + 修独立中口径明细扩列月份总行为空

- loadMidClassMap 补维度3(城际城乡巡游出租 = city_taxi 总量−城市内)、维度4
(城际城乡网约车 = WycSplitCalc 拆分),新增 midPeriodKey
- fillMidCell 去掉「出租/网约车写死 0」旁路;fillMidYoyCell / fillMidCumYoyColumn 同比改四维齐全
- exportPassengerMidDetail 改按列遍历(扩列出来的 7~12 月空格才填得上);扩列月份总行
改由库内四维合计直接写入;该函数重算改逐格容错的 recalcWorkbookFormulas
(evaluateAll 会在跨簿公式处整本中断,累计列拿不到新缓存值)
- 验证:8月全省总行 4003.5701 = 班线 1646.48 + 公交 1436.63 + 出租城乡 493.90 + 网约车城乡 426.56;
18 个地区总行 = Σ4 维、累计列 = Σ1-8 月;与汇总表「中口径明细」页逐格一致;
回归:汇总表 17 页 0 差异、排名件 0 差异、明细件仅 36 格变化
- 遗留:排名件累计少 2063.45(= 网约车 1-6 月无源)、母版城市级公式引错页、网约车 1-6 月订单缺口

测试报告:docs/功能测试报告/功能测试报告_中口径四维口径统一_2026-09-18.md
工作日结:docs/工作日结/工作日结_2026-09-18.md(第 3 步收口建议)
2个文件已修改
1个文件已添加
257 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_中口径四维口径统一_2026-09-18.md 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-18.md 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_Öпھ¶ËÄά¿Ú¾¶Í³Ò»_2026-09-18.md
New file
@@ -0,0 +1,77 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_中口径四维口径统一_2026-09-18
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
**目标**:让「中口径」在系统各处使用同一套口径与同一套源数据,消除「同一指标在不同页/不同导出件里对不上」的问题。
**中口径定义(4 ç»´ï¼Œ2026-09-18 èµ·ç»Ÿä¸€ï¼‰**
| ç»´åº¦ | å«ä¹‰ | æ•°æ®æ¥æº |
|---|---|---|
| 1 | å…¬è·¯ç­çº¿å®¢è¿ | `h2031_enterprise_monthly`(企业月报)+ `passenger_individual_monthly`(个体月报) |
| 2 | åŸŽé™…城乡公交 | `city_bus_monthly` çš„ `passenger_chengxiang / turnover_chengxiang` |
| 3 | åŸŽé™…城乡巡游出租 | `city_taxi_monthly`:总量 âˆ’ åŸŽå¸‚内(`passenger_volume - passenger_city`,周转量同法) |
| 4 | åŸŽé™…城乡网约车 | `WycSplitCalc`(订单 + å…¨çœ pin + å·¡æ¸¸å‡ºç§Ÿæ‹†åˆ†ï¼‰ï¼šæ€»é‡ âˆ’ åŸŽå¸‚内 |
- æ€»å®¢è¿é‡/总周转量 = ç»´åº¦ 1+2+3+4(`loadMidClassMap` çš„维度 0);
- **全省行一律 = Î£17 å¸‚å·ž**(不采用库内 `region_code='420000'` çš„全省行,该口径 1-8 æœˆæ¯” Î£å¸‚州多 3042.75,属业务待核项);
- åŒæ¯” = (本期 âˆ’ åŽ»å¹´åŒæœŸ) / åŽ»å¹´åŒæœŸï¼›æ— åŽ»å¹´æ•°æ®ç•™ç©ºã€‚
**本次改动文件**:`traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java`
- `loadMidClassMap()`:补维度 3(巡游出租城乡)、维度 4(网约车城乡),新增 `midPeriodKey()`;
- `fillMidCell()`:去掉「出租/网约车写死 0」旁路,四维统一取库内值;
- `fillMidYoyCell()` / `fillMidCumYoyColumn()`:同比由「只算前两维」改为四维齐全;
- `exportPassengerMidDetail()`:由「只处理公式格」改为「按列遍历」,扩列出来的 7~12 æœˆç©ºæ ¼æ‰å¡«å¾—上;扩列月份的总行值列由库内四维合计直接写入;重算改用逐格容错的 `recalcWorkbookFormulas()`(原 `evaluateAll()` ä¼šåœ¨è·¨ç°¿å…¬å¼å¤„整本中断,累计列拿不到新缓存值)。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ 8090(`start-dev.ps1` é‡å¯ï¼ŒPID 11772)、前端 8080(复用已在运行实例);数据库 `traffic_audit`(127.0.0.1:3308);报表期 2026-08,mode=month。
- å¯¼å‡ºæŽ¥å£ï¼š`/api/report/export/summaryWorkbook`、`/api/report/export/passengerMidDetail`、`/api/report/export/passengerMidRank`。
- è¯æ®æ–‡ä»¶ï¼š`_tmp/_qa_out/step3c_summary.xlsx`、`step3c_midDetail.xlsx`、`step3c_midRank.xlsx`、`chk_step3c2.txt`、`chk_step3c3.txt`、`cmp_step3bc.txt`(本轮);`step3b_*.xlsx`(改动前基线)。
## ä¸‰ã€ç”¨ä¾‹è¡¨
| # | ç±»åž‹ | ç”¨ä¾‹ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | æ­£å‘ | ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径明细》全省 8 æœˆæ€»è¡Œï¼ˆQ5) | = å››ç»´åˆè®¡ 4003.57(原为空) | 4003.5701 | é€šè¿‡ |
| 2 | æ­£å‘ | ç‹¬ç«‹ä»¶å…¨çœ 7 æœˆæ€»è¡Œï¼ˆO5) | = 4028.3213(原为空) | 4028.3213 | é€šè¿‡ |
| 3 | æ­£å‘ | 18 ä¸ªåœ°åŒºï¼ˆå…¨çœ+17 å¸‚州)× 7/8 æœˆæ€»è¡Œ = Î£è¯¥å— 4 ç»´è¡Œï¼ˆ8 æœˆå„维齐全时) | ç›¸ç­‰ | 8 æœˆ 18/18 ç›¸ç­‰ï¼›7 æœˆ 18/18 ç›¸ç­‰ | é€šè¿‡ |
| 4 | æ­£å‘ | ç‹¬ç«‹ä»¶ç´¯è®¡åˆ—(AA)缓存值 | = è¯¥è¡Œ 1-8 æœˆä¹‹å’Œ | 18/18 è¡Œç›¸ç­‰ï¼ˆå…¨çœ 31528.0607,武汉 2759.8855) | é€šè¿‡ |
| 5 | æ­£å‘ | ç‹¬ç«‹ä»¶ä¸Žæ±‡æ€»å¤§è¡¨ã€Œä¸­å£å¾„明细」页逐格对照(全省行 1-8 æœˆã€4 ä¸ªç»´åº¦åˆ†è§£ï¼‰ | å®Œå…¨ä¸€è‡´ | 1-8 æœˆ 4 ç»´ + æ€»é‡å…¨éƒ¨ä¸€è‡´ | é€šè¿‡ |
| 6 | æ­£å‘ | æ±‡æ€»å¤§è¡¨ã€Œä¸­å£å¾„排名」全省累计 K3 | = Î£17 å¸‚å·ž K4:K20 | 29464.6108 = Î£å¸‚州(独立排名件 B3 åŒå€¼ï¼‰ | é€šè¿‡ï¼ˆå£å¾„一致性另见问题 2) |
| 7 | å›žå½’ | æ±‡æ€»å¤§è¡¨ç”Ÿæˆä»¶æ”¹åŠ¨å‰åŽé€æ ¼å¯¹æ¯”ï¼ˆ17 é¡µï¼‰ | 0 å·®å¼‚ | 0 æ ¼å˜åŒ– | é€šè¿‡ |
| 8 | å›žå½’ | ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径排名》改动前后逐格对比 | 0 å·®å¼‚ | 0 æ ¼å˜åŒ– | é€šè¿‡ |
| 9 | å›žå½’ | ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径明细》改动前后逐格对比 | ä»…目标格变化 | ä»… 36 æ ¼å˜åŒ– = 18 åœ°åŒº Ã— 2 ä¸ªæœˆï¼ˆO/Q æ€»è¡Œï¼‰ | é€šè¿‡ |
| 10 | è´Ÿå‘/边界 | `wyc_order_monthly` 2026-01~06 è®¢å•列全为 NULL、模板《网约车订单及全省总量.xlsx》只有 2026-07 ä¸€è¡Œæœ‰æ•°æ® | ç»´åº¦ 4 è¯¥æœˆæ— æº â†’ è¡Œç•™ç©ºï¼ˆä¸ç¼–造) | ç‹¬ç«‹ä»¶ã€ŒåŸŽé™…城乡网约车」1-6 æœˆä¸ºç©ºï¼›æ±‡æ€»è¡¨è¯¥å£å¾„由母版定稿承载 | é€šè¿‡ï¼ˆæŒ‰é¢„期留空并记录) |
| 11 | è´Ÿå‘/边界 | åŽ»å¹´åŒæœŸæ— æ•°æ®ï¼ˆå¦‚ 2025-01 ç½‘约车订单缺失) | åŒæ¯”留空 | ç•™ç©ºï¼Œæœªå†™ 0 / #DIV/0! | é€šè¿‡ |
| 12 | è´Ÿå‘/边界 | ç¼ºæ¨¡æ¿æ–‡ä»¶ | æŠ›ã€Œæœªæ‰¾åˆ°æ¨¡æ¿æ–‡ä»¶ã€å¹¶å†™å…¥åŽç«¯æ—¥å¿— | æœªè§¦å‘(模板齐全) | ä¸é€‚用 |
**入库抽查**(报表期 2026-08,与源表逐项对齐)
| æŠ½æŸ¥é¡¹ | åº“内值 | ç”Ÿæˆä»¶å€¼ | ç»“论 |
|---|---|---|---|
| `city_bus_monthly` 8 æœˆ 17 å¸‚州合计 | 12315.37 | ä¸­å£å¾„维度 2 = 1436.63(城际城乡口径,非合计口径) | å£å¾„不同,非同一指标 |
| `city_taxi_monthly` 8 æœˆåŸŽé™…城乡差(荆州 100.94 / å­æ„Ÿ 73.90 / æ©æ–½ 53.54 / è¥„阳 49.84 / å®œæ˜Œ 46.81 / æ­¦æ±‰ 0) | Î£ = 493.90 | ç»´åº¦ 3 å…¨çœ = 493.90 | é€šè¿‡ |
| `wyc_order_monthly` 2026-08(`order_sum` 30508652、`order_wuhan` 11550551、`pin_k` 4997.74) | â€” | ç»´åº¦ 4 å…¨çœ = 426.56 | é€šè¿‡ |
## å››ã€å‘现的问题与处置
1. **(本次已修)独立《生成_中口径明细》扩列后的 7~12 æœˆæ€»è¡Œä¸ºç©ºã€ç´¯è®¡åœåœ¨ 6 æœˆ**
   - æ ¹å› ï¼šæ€»è¡Œå€¼åˆ—在扩列出来的月份里既不是公式也没有缓存值,旧分支 `keepCached` ç­‰äºŽæŠŠæ ¼å­æ¸…空;`evaluateAll()` åˆåœ¨è·¨ç°¿å…¬å¼å¤„整本中断,累计列得不到新值。
   - å¤„置:扩列月份总行改由库内四维合计直接写入;重算改逐格容错。验证见用例 1-4、9。
2. **(未修,需口径决策)《生成_中口径排名》的全省累计与同比与《生成_中口径明细》不自洽**
   - çŽ°è±¡ï¼šæŽ’åä»¶å…¨çœç´¯è®¡ = 29464.6108、累计同比 = 0.638;中口径明细全省累计 = 31528.0607、累计同比 = 0.0581,差值 2063.45 = 2026 å¹´ 1-6 æœˆç»´åº¦ 4(333.39+294.49+347.23+335.39+369.27+383.68)。
   - æ ¹å› ï¼šæŽ’名件由 Java ç”¨ `loadMidClassMap()` çŽ°ç®—ï¼ˆ1-6 æœˆç½‘约车无源 â†’ å°‘一块,且 2025 å¹´åŸºçº¿ä¹Ÿåªæœ‰åº“内口径);中口径明细页走的是母版公式链 + å®šç¨¿ç¼“存值。
   - å»ºè®®ï¼šè§ã€Šæ±‡æ€»è¡¨ä¸‰æ¡ä¸»çº¿ä¿®å¤æ–¹æ¡ˆ_2026-09-18》第 3 æ­¥â€”—统一由「母版公式链 + å°è´¦é¡µã€å‡ºæ•°ï¼ŒJava ä¸å†å¦ç®—一套(详见本轮答复)。
3. **(属母版缺陷)中口径明细城市级「城际城乡巡游出租/网约车」行公式引错页**
   - çŽ°è±¡ï¼šæ­¦æ±‰ R13/R14 å…¬å¼ä¸º `=公交!C13-公交!C15`(应分别指向 `出租!`、`网约车!`),城市级恒为 0;而全省行 R8/R9 é æ¯ç‰ˆç¼“存值给出真实数(535.83 / 333.39),二者不自洽。
   - å¤„置:需修母版这两行的公式引用(一次性),之后由重算自动出值。本次未改母版。
4. **(数据缺口)2026 å¹´ 1-6 æœˆç½‘约车订单未入库**
   - `wyc_order_monthly` 2026-01~06 ä»…有 `pin_*`,`order_*` å…¨ NULL;模板文件也只有 2026-07 ä¸€è¡Œæœ‰æ•°æ® â†’ ç»´åº¦ 4 çš„ 1-6 æœˆæ— æºã€‚
## äº”、结论
本次改动把「中口径」从 2 ç»´ï¼ˆç­çº¿+公交)统一为 4 ç»´ï¼Œå¹¶ä¿®æŽ‰äº†ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径明细》扩列月份总行为空、累计停在上一期的问题;已验证 12 æ¡ç”¨ä¾‹ï¼ˆ6 æ­£å‘ / 3 å›žå½’ / 3 è´Ÿå‘边界)全部通过,汇总大表 17 é¡µä¸Žã€Šç”Ÿæˆ_中口径排名》零回归。
仍待处理:排名件与明细件的口径归一(问题 2,需业务/方案确认)、母版中口径明细城市级公式引错页(问题 3)、网约车 1-6 æœˆè®¢å•数据缺口(问题 4)。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-18.md
@@ -21,6 +21,23 @@
- å†™äº†ã€Šæ±‡æ€»è¡¨ä¸‰æ¡ä¸»çº¿ä¿®å¤æ–¹æ¡ˆ_2026-09-18》(`docs/问题汇总/`),把根因、证据、4 æ­¥ä¿®æ³•、投资"按项目名称更新/新增插行"现状一并记录。
- æŠ•资侧核实:导入确实按「项目名称 + ç±»åˆ«ã€upsert(库内 å®¢è¿ç«™åœº 49/49、物流园区 135/135 æ— é‡å¤è¡Œï¼‰ï¼›ç¼ºå£æ˜¯"模板里没有的新项目不会插行"、"资金渠道分组行导入时被跳过"、"同名不同项目不弹窗"。
### 3. ç¬¬ 3 æ­¥ï¼šä¸­å£å¾„四维口径统一(下午)
**口径定死**(写进《功能测试报告_中口径四维口径统一_2026-09-18》):1 å…¬è·¯ç­çº¿ï¼ˆh2031+个体)= 2 åŸŽé™…城乡公交(city_bus)= 3 åŸŽé™…城乡巡游出租(city_taxi:总量−城市内)= 4 åŸŽé™…城乡网约车(WycSplitCalc æ‹†åˆ†ï¼‰ï¼›æ€»é‡ = å››ç»´ä¹‹å’Œï¼›**全省行一律 Î£17 å¸‚å·ž**。
**改动**(`ReportExportService.java`):
- `loadMidClassMap()` è¡¥ç»´åº¦ 3、维度 4(新增 `midPeriodKey()`);
- `fillMidCell()` åŽ»æŽ‰ã€Œå‡ºç§Ÿ/网约车写死 0」旁路;`fillMidYoyCell()`/`fillMidCumYoyColumn()` åŒæ¯”改四维齐全;
- `exportPassengerMidDetail()` ç”±ã€ŒåªéåŽ†å…¬å¼æ ¼ã€æ”¹ä¸ºã€ŒæŒ‰åˆ—éåŽ†ã€ï¼ˆæ‰©åˆ—å‡ºçš„ 7~12 æœˆç©ºæ ¼æ‰å¡«å¾—上);
- æ‰©åˆ—月份的总行值列改由库内四维合计直接写入(模板本无此列,克隆后既无公式也无缓存值);
- è¯¥å‡½æ•°é‡ç®—改用逐格容错的 `recalcWorkbookFormulas()`——`evaluateAll()` ä¼šåœ¨è·¨ç°¿å…¬å¼ï¼ˆSheet2 çš„ `[1]xx!A1`)处整本中断,累计列就永远拿不到新缓存值。
**验证**:独立《生成_中口径明细》8 æœˆå…¨çœæ€»è¡Œ 4003.5701、7 æœˆ 4028.3213(原为空),18 ä¸ªåœ°åŒºæ€»è¡Œ = Î£4 ç»´ã€ç´¯è®¡åˆ— = Î£1-8 æœˆï¼›ä¸Žæ±‡æ€»å¤§è¡¨ã€Œä¸­å£å¾„明细」页逐格一致;回归 = æ±‡æ€»è¡¨ 17 é¡µ 0 å·®å¼‚、排名件 0 å·®å¼‚、明细件仅 36 æ ¼ï¼ˆ18 åœ°åŒº Ã— 2 ä¸ªæœˆæ€»è¡Œï¼‰å˜åŒ–。
**顺带查清两个老问题**:
- ã€Šç”Ÿæˆ_中口径排名》全省累计 29464.6108 / åŒæ¯” 0.638,比中口径明细少 2063.45,差值正好是 2026 å¹´ 1-6 æœˆç»´åº¦ 4(网约车)→ æŽ’名件是 Java çŽ°ç®—ã€æ˜Žç»†é¡µèµ°æ¯ç‰ˆå…¬å¼é“¾ï¼Œä¸¤å¥—å£å¾„æ‰“æž¶ï¼ˆç¬¬ 3 æ­¥çš„真正收口点)。
- æ¯ç‰ˆã€Œä¸­å£å¾„明细」城市级「城际城乡巡游出租/网约车」行公式引错页(`=公交!C13-公交!C15`,应指向 `出租!`/`网约车!`),城市级恒 0、全省行靠缓存值 â†’ éœ€ä¿®æ¯ç‰ˆå…¬å¼ä¸€æ¬¡ã€‚
## äºŒã€æµ‹è¯•结果摘要
见 `docs/功能测试报告/功能测试报告_汇总表公式扩页与重算_2026-09-18.md`:8 æ¡æ­£å‘ + 4 æ¡è´Ÿå‘/边界 + 4 é¡¹å…¥åº“抽查,全部通过。
@@ -30,16 +47,38 @@
- ä¸­å£å¾„排名 K3 ç¼“å­˜ 24481.7407 = æ‰‹å·¥ Î£17 å¸‚州。
- å›žå½’:改动前后逐格对比,仅 216 æ ¼å˜åŒ–,全部落在目标两页,其余 17 é¡µ 0 å·®å¼‚。
### ç¬¬ 3 æ­¥ï¼ˆä¸‹åˆï¼‰æµ‹è¯•结果
见 `docs/功能测试报告/功能测试报告_中口径四维口径统一_2026-09-18.md`:6 æ¡æ­£å‘ + 3 æ¡å›žå½’ + 3 æ¡è´Ÿå‘边界,全部通过。
- ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径明细》全省 8 æœˆæ€»å®¢è¿é‡ 4003.5701 = ç­çº¿ 1646.4801 + å…¬äº¤ 1436.63 + å‡ºç§ŸåŸŽä¹¡ 493.90 + ç½‘约车城乡 426.56;
- 18 ä¸ªåœ°åŒº Ã—(7、8 æœˆï¼‰æ€»è¡Œ = Î£ è¯¥å— 4 ç»´è¡Œï¼›ç´¯è®¡åˆ—缓存值 = Î£1-8 æœˆï¼ˆå…¨çœ 31528.0607);
- å›žå½’:汇总大表 17 é¡µ 0 æ ¼å˜åŒ–、《生成_中口径排名》0 æ ¼å˜åŒ–、《生成_中口径明细》仅 36 æ ¼å˜åŒ–(全部是目标格)。
## ä¸‰ã€é—留问题与待办
1. **中口径排名同比仍是写死值**(M3=0.7424 / Q3=0.2317,库内全省口径),需与 Î£17 å¸‚州口径统一 â€”— ç¬¬ 3 æ­¥ã€‚
2. **`loadMidClassMap()` ç¼ºå·¡æ¸¸å‡ºç§Ÿ + ç½‘约车两维**,导致独立《生成_公路旅客中口径明细》少 535.83+333.39(1 æœˆï¼‰â€”— ç¬¬ 3 æ­¥ã€‚
3. **库内 h2031「全省」行 â‰  Î£17 å¸‚å·ž**(1-8 æœˆ 27524.49 vs 24481.74,差 3042.75):业务口径问题,建议业务侧核对。
4. æŠ•资:模板里没有的新项目不会自动插行;明细表资金渠道分组行未入库(需加 `fund_channel`);同名不同项目未弹窗。
5. æŠ•资两张明细表(`X月客运站投资明细表` / `X月物流站场投资明细`)生成器未做。
1. ~~中口径排名同比仍是写死值~~ â†’ **已按新口径取值**(M3=0.638 / Q3=0.2247),但仍与中口径明细的 0.0581 ä¸è‡ªæ´½ï¼Œå·®å€¼ 2063.45 = ç½‘约车 1-6 æœˆæ— æºï¼›**收口办法见下方"第 3 æ­¥æ”¶å£å»ºè®®"**。
2. ~~`loadMidClassMap()` ç¼ºå·¡æ¸¸å‡ºç§Ÿ + ç½‘约车两维~~ â†’ **已补**(维度 3、4),独立中口径明细四维齐全。
3. **母版缺陷待修**:中口径明细城市级「城际城乡巡游出租/网约车」行公式引错页(指向 `公交!`),城市级恒 0、全省行靠缓存值。
4. **数据缺口**:2026-01~06 ç½‘约车订单未入库(`order_*` å…¨ NULL),模板《网约车订单及全省总量.xlsx》只有 2026-07 ä¸€è¡Œ â†’ ç»´åº¦ 4 è¯¥ 6 ä¸ªæœˆæ— æºï¼›éœ€ä¸šåŠ¡è¡¥æ•°æˆ–ç¡®è®¤ä¸é€‚ç”¨ã€‚
5. åº“内 h2031「全省」行 â‰  Î£17 å¸‚州(1-8 æœˆ 27524.49 vs 24481.74,差 3042.75):业务口径问题,建议业务侧核对。
6. æŠ•资:模板里没有的新项目不会自动插行;明细表资金渠道分组行未入库(需加 `fund_channel`);同名不同项目未弹窗。
7. æŠ•资两张明细表(`X月客运站投资明细表` / `X月物流站场投资明细`)生成器未做。
### ç¬¬ 3 æ­¥æ”¶å£å»ºè®®ï¼ˆå¾…用户拍板)
不再让 Java å„算一套,改为**一条链路出数**:母版公式链(全省行 = Î£17 å¸‚州、累计 = Î£å„月、派生页跨页引用)+ å°è´¦é¡µå†™æ•° + ç”Ÿæˆæ”¶å°¾é‡ç®—。
1. å°è´¦é¡µï¼ˆè´§è¿ / ç­çº¿åŒ…车 / å…¬äº¤ / å‡ºç§Ÿ / ç½‘约车 / è½¨é“轮渡)只写当月值;
2. æ´¾ç”Ÿé¡µï¼ˆåŸŽå¸‚客运 / ä¸­å£å¾„明细 / å…¬è·¯æ€»å®¢è¿ / ä¸­å£å¾„客运量 / æŽ’名)**只补公式 + é‡ç®—,不单独写数**;
3. åªæœ‰æ¯ç‰ˆé‡Œæœ¬å°±æ˜¯ç¡¬ç¼–码数值的行(中口径明细的城市级"城际城乡出租/网约车"、全省行)按四维口径回填;
4. ç‹¬ç«‹å¯¼å‡ºä»¶ï¼ˆç”Ÿæˆ_中口径明细 / æŽ’名 / å…¬è·¯æ—…客分市州)改为直接取汇总表对应页的值,或共用同一口径函数;
5. æ¯ç‰ˆé‡Œå¼•错页的城市级公式修一次(第 3 æ¡ï¼‰ï¼›
6. ç”ŸæˆåŽè·‘"生成件 vs æ¯ç‰ˆé‡ç®—"逐格回归比对,差异清单落日志,作为验收基线。
## å››ã€æ˜Žå¤©è®¡åˆ’
1. ç¬¬ 3 æ­¥ï¼šç»Ÿä¸€ä¸­å£å¾„口径(rank å…¨çœåŒæ¯”改为同源 + `loadMidClassMap` è¡¥ä¸¤ç»´ï¼‰ï¼Œæ”¹å®Œå¯¹ç‹¬ç«‹ä¸­å£å¾„明细与汇总表逐项比对。
2. æŠ•资"新增项目插行 + åˆ†ç»„行入库"方案落地(等用户对"模块"口径的最后确认)。
3. å»ºç”Ÿæˆä»¶ vs æ¯ç‰ˆé‡ç®—的回归比对脚本,固化为每次改动的验收基线。
1. ç¬¬ 3 æ­¥æ”¶å£ï¼šæŒ‰ä¸Šé¢ 6 æ¡è½åœ°ï¼ˆå…ˆä¿®æ¯ç‰ˆå¼•错页的城市级公式 â†’ æŽ’名件改取同源 â†’ å»ºå›žå½’比对脚本)。
2. ç½‘约车 2026-01~06 è®¢å•数据缺口:向用户确认是补数还是留空。
3. æŠ•资"新增项目插行 + åˆ†ç»„行入库"方案落地(等用户对"模块"口径的最后确认)。
4. æŠ•资两张明细表生成器。
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -13,6 +13,7 @@
import com.trafficaudit.dataimport.entity.PassengerEnterpriseMonthly;
import com.trafficaudit.dataimport.entity.PassengerIndividualMonthly;
import com.trafficaudit.dataimport.entity.ScaleSplitTransport;
import com.trafficaudit.dataimport.entity.WycOrderMonthly;
import com.trafficaudit.auditengine.entity.AuditResult;
import com.trafficaudit.auditengine.entity.AuditRun;
import com.trafficaudit.auditengine.mapper.AuditResultMapper;
@@ -29,9 +30,11 @@
import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper;
import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper;
import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper;
import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper;
import com.trafficaudit.reportexport.calc.MidCalc;
import com.trafficaudit.reportexport.calc.PaxCalc;
import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller;
import com.trafficaudit.reportexport.calc.WycSplitCalc;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
@@ -98,6 +101,10 @@
    private CityTaxiMonthlyMapper cityTaxiMapper;
    @Resource
    private SummaryWorkbookFiller summaryWorkbookFiller;
    @Resource
    private WycSplitCalc wycSplitCalc;
    @Resource
    private WycOrderMonthlyMapper wycOrderMapper;
    @Resource
    private AuditResultMapper auditResultMapper;
    @Resource
@@ -1179,17 +1186,16 @@
        return lv == 0.0 ? null : (v - lv) / lv;
    }
    /** ä¸­å£å¾„月同比:k=-1 è¡¨ç¤ºæ€»è¡Œï¼ˆç­çº¿+公交之和,出租/网约车无源数据按 0);无去年数据留空 */
    /** ä¸­å£å¾„月同比:k=-1 è¡¨ç¤ºæ€»è¡Œï¼ˆç­çº¿+公交+出租城乡+网约车城乡 å››é¡¹ä¹‹å’Œï¼‰ï¼›æ— åŽ»å¹´æ•°æ®ç•™ç©º */
    private void fillMidYoyCell(Cell c, Map<Integer, Map<String, double[][]>> mid, int year, int month,
                                String area, int k, boolean volume) {
        double v = 0.0, lv = 0.0;
        if (k == -1) {
            for (int kk = 1; kk <= 4; kk++) {
                if (kk >= 3) continue;
                v += midClassVal(mid, year, month, area, kk, volume);
                lv += midClassVal(mid, year - 1, month, area, kk, volume);
            }
        } else if (k <= 2) {
        } else {
            v = midClassVal(mid, year, month, area, k, volume);
            lv = midClassVal(mid, year - 1, month, area, k, volume);
        }
@@ -1197,7 +1203,7 @@
        if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4));
    }
    /** ä¸­å£å¾„累计同比列(1..months æœˆç´¯è®¡ï¼›k=-1 æ€»è¡Œ = ç­çº¿+公交) */
    /** ä¸­å£å¾„累计同比列(1..months æœˆç´¯è®¡ï¼›å››ç»´é½å…¨ï¼‰ */
    private void fillMidCumYoyColumn(Sheet sheet, Map<Integer, Map<String, double[][]>> mid, int year,
                                     int months, int colIdx) {
        List<String> areas = midTemplateAreas();
@@ -1217,7 +1223,7 @@
            String area = areas.get(blockOffset / 5);
            double v = 0.0, lv = 0.0;
            for (int m = 1; m <= months; m++) {
                if (k <= 2) {
                {
                    v += midClassVal(mid, year, m, area, k, volume);
                    lv += midClassVal(mid, year - 1, m, area, k, volume);
                }
@@ -2181,20 +2187,15 @@
            boolean extended = fillMonths > 6;
            if (extended) extendMonthlyColumns(sheet, 2, currentYear, 4, sheet.getLastRowNum());
            List<String> areas = midTemplateAreas();
            // 2026-09-18:改为「按单元格类型无关」遍历——扩列(7~12 æœˆï¼‰å‡ºæ¥çš„值列在模板里是空格,
            // æ—§å®žçŽ°åªå¤„ç† CellType.FORMULA çš„æ ¼ï¼Œå¯¼è‡´ 7 æœˆåŠä»¥åŽæœˆä»½æ°¸è¿œå¡«ä¸ä¸Šï¼ˆ8 æœˆæ•´åˆ— 0、累计只到 6 æœˆï¼‰ã€‚
            for (Row row : sheet) {
                if (row == null) continue;
                for (Cell c : row) {
                    if (c.getCellType() != CellType.FORMULA) continue;
                    String f = c.getCellFormula();
                    if (f == null) continue;
                    boolean crossBook = f.contains("[");
                    if (!crossBook && !isYoyMonthCol(c.getColumnIndex() + 1)) continue; // å†…部公式(合计/累计)保留重算;同比列 #REF! è¦†å†™ä¸ºæ•°å€¼
                    if (crossBook && !is2026MonthCol(c.getColumnIndex() + 1)) {
                        keepCached(c); // 2025 å¹´åˆ—保留模板缓存值
                        continue;
                    }
                for (int c0 = 0; c0 < Math.max(row.getLastCellNum(), 0); c0++) {
                    Cell c = row.getCell(c0);
                    if (c == null) continue;
                    int r = c.getRowIndex() + 1;
                    int col = c.getColumnIndex() + 1;
                    int col = c0 + 1;
                    int blockOffset = -1;
                    boolean volume = false;
                    if (r >= 5 && r <= 94) {
@@ -2205,31 +2206,48 @@
                        volume = false;
                    }
                    if (blockOffset < 0) {
                        keepCached(c); // r1/r2 å¤‡æ³¨ç­‰
                        if (c.getCellType() == CellType.FORMULA) keepCached(c); // r1/r2 å¤‡æ³¨ã€é¡µå¤–跨簿引用等
                        continue;
                    }
                    if (blockOffset % 5 == 0) {
                        if (isYoyMonthCol(col)) {
                            fillMidYoyCell(c, mid, currentYear, monthOfCol(col), areas.get(blockOffset / 5), -1, volume);
                        } else {
                            keepCached(c); // æ€»è¡Œå†…部公式(防御)
                    boolean isVal = is2026MonthCol(col);
                    boolean isYoy = isYoyMonthCol(col);
                    if (!isVal && !isYoy) {
                        // 2025 å¹´åˆ—等:跨簿公式取缓存值断链;内部公式(合计/累计)保留,稍后 recalc é‡ç®—
                        if (c.getCellType() == CellType.FORMULA) {
                            String f = c.getCellFormula();
                            if (f != null && f.contains("[")) keepCached(c);
                        }
                        continue;
                    }
                    int k = blockOffset % 5; // 1=班线 2=公交 3=出租 4=网约车
                    String area = areas.get(blockOffset / 5);
                    if (is2026MonthCol(col)) {
                    if (blockOffset % 5 == 0) {
                        if (isYoy) {
                            fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, -1, volume);
                        } else if (monthOfCol(col) > 6) {
                            // æ‰©åˆ—出来的 7~12 æœˆæ€»è¡Œå€¼åˆ—:模板里本无此列,克隆后既没有公式也没有缓存值,
                            // ç›´æŽ¥å†™åº“内四维合计(维度0)。1~6 æœˆä¿ç•™æ¨¡æ¿ç¼“存值,避免丢掉模板里的定稿数。
                            double total = midClassVal(mid, currentYear, monthOfCol(col), area, 0, volume);
                            c.setBlank();
                            if (total != 0.0) c.setCellValue(round(total, 4));
                        } else if (c.getCellType() == CellType.FORMULA) {
                            keepCached(c); // 1~6 æœˆæ€»è¡Œå€¼åˆ— = æ¨¡æ¿ç¼“存值,保留
                        }
                        continue;
                    }
                    int k = blockOffset % 5; // 1=班线 2=公交 3=出租城乡 4=网约车城乡
                    if (isVal) {
                        fillMidCell(c, mid, currentYear, monthOfCol(col), area, k, volume);
                    } else if (isYoyMonthCol(col)) {
                        fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, k, volume);
                    } else {
                        keepCached(c); // 2025 å¹´åˆ—保留模板缓存值
                        fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, k, volume);
                    }
                }
            }
            // ç´¯è®¡åŒæ¯”列(模板 #REF! â†’ åº“内去年 1..N æœˆç´¯è®¡åŒæ¯”数值)
            fillMidCumYoyColumn(sheet, mid, currentYear, fillMonths, extended ? 27 : 15);
            recalc(wb);
            // evaluateAll ä¼šåœ¨è·¨ç°¿å…¬å¼ï¼ˆSheet2 çš„ [1]xx!A1)处整本中断,累计列 AA å°±æ°¸è¿œæ‹¿ä¸åˆ°æ–°ç¼“存值,
            // æ”¹ç”¨é€æ ¼å®¹é”™çš„ recalcWorkbookFormulas:坏格跳过、好格照算。
            int[] recalc = recalcWorkbookFormulas(wb);
            log.info("中口径明细公式重算完成:成功 {} æ ¼ï¼Œå¤±è´¥ {} æ ¼", recalc[0], recalc[1]);
            clearFormulaErrorsAll(wb); // åŽ»å¹´åŒæœŸæ˜Žç»†é¡µæ®‹ç•™ #REF! åŒæ¯”公式 â†’ æ¸…空
            return toBytes(wb);
        }
@@ -2256,12 +2274,8 @@
        return (col - 1) / 2;
    }
    /** å¡«ä¸­å£å¾„数据单元格:出租/网约车(3/4)显式 0,班线/公交填库内值(无值清空) */
    /** å¡«ä¸­å£å¾„数据单元格:班线/公交/出租城乡/网约车城乡 å››ç»´ç»Ÿä¸€å–库内值(无值清空) */
    private void fillMidCell(Cell c, Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) {
        if (k == 3 || k == 4) {
            writeExplicitZero(c); // å‡ºç§Ÿ/网约车暂无数据源,显式 0
            return;
        }
        double v = midClassVal(mid, year, month, area, k, volume);
        // POI setCellValue(double) å¯¹å…¬å¼å•元格只更新缓存不移除公式,必须先 setBlank å†å†™å€¼
        c.setBlank();
@@ -2369,9 +2383,54 @@
            addMidClass(mid, key, city, 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang()));
            addMidClass(mid, key, "湖北省", 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang()));
        }
        // ç»´åº¦3:城际城乡巡游出租 = å°è´¦é¡µã€Œå‡ºç§Ÿ!总量 âˆ’ å‡ºç§Ÿ!城市内」(源 city_taxi_monthly)
        for (CityTaxiMonthly t : cityTaxiMapper.selectList(null)) {
            if (t.getReportPeriod() == null || t.getCity() == null) continue;
            int key = midPeriodKey(t.getReportPeriod());
            if (key <= 0) continue;
            String city = RegionUtil.normalizeCityName(t.getCity());
            if (city == null) continue;
            double pass = nz(t.getPassengerVolume()) - nz(t.getPassengerCity());
            double turn = nz(t.getTurnover()) - nz(t.getTurnoverCity());
            addMidClass(mid, key, city, 3, pass, turn);
            addMidClass(mid, key, "湖北省", 3, pass, turn);
        }
        // ç»´åº¦4:城际城乡网约车 = ç½‘约车拆分结果(与台账「网约车」页同源同法)
        for (WycOrderMonthly o : wycOrderMapper.selectList(null)) {
            String per = o.getReportPeriod();
            if (per == null) continue;
            int key = midPeriodKey(per);
            if (key <= 0) continue;
            WycSplitCalc.WycResult wr;
            try {
                wr = wycSplitCalc.calc(per);
            } catch (Exception e) {
                continue; // è¯¥æœŸç¼ºè®¢å•/pin æˆ–出租源数据,跳过(不影响其它期)
            }
            for (Map.Entry<String, WycSplitCalc.WycMetrics> en : wr.getByCity().entrySet()) {
                WycSplitCalc.WycMetrics wm = en.getValue();
                addMidClass(mid, key, en.getKey(), 4, wm.getSuburbanPax(), wm.getSuburbanTurnover());
                addMidClass(mid, key, "湖北省", 4, wm.getSuburbanPax(), wm.getSuburbanTurnover());
            }
        }
        return mid;
    }
    /** "yyyy-MM" -> yyyy*100+MM;非法返回 0(与 loadMidClassMap çš„ key å£å¾„一致) */
    private int midPeriodKey(String period) {
        if (period == null) return 0;
        String[] parts = period.trim().split("-");
        if (parts.length != 2) return 0;
        try {
            int y = Integer.parseInt(parts[0]);
            int m = Integer.parseInt(parts[1]);
            if (y <= 0 || m < 1 || m > 12) return 0;
            return y * 100 + m;
        } catch (NumberFormatException e) {
            return 0;
        }
    }
    /** å†™å…¥åˆ†ç±»å€¼å¹¶ç´¯åŠ æ€»é‡ï¼ˆç»´åº¦0) */
    private void addMidClass(Map<Integer, Map<String, double[][]>> mid, int key, String city, int k, double pass, double turn) {
        double[][] arr = mid.computeIfAbsent(key, kk -> new HashMap<>()).computeIfAbsent(city, kk -> new double[5][2]);