zhizhijie
2026-09-11 a76a8cce476ceccbc061a3f51681b6090a7d3741
feat: 汇总大表排名页随报表期刷新 + 同比列统一百分数显示

用户反馈《道路运输量汇总表》两个问题,本轮修复:

1) 排名表数据没有更新
- 母版里「货运量排名 / 货运周转量排名 / 中口径排名」三页是上一报表期的静态值,
导出时用程序自己的独立排名表(生成_货运量排名/周转量排名/中口径排名)刷新这三页:
· 数值格搬值;源侧公式按目标块偏移整体平移后写公式(打开时按汇总表自身数据重算);
母版自带的 SUM/RANK/占比公式保留不动;
· 块标题随报表期同步(1-7月 → 1-8月、7月 → 8月)。
- 修两个坑:
· 母版标题是 inlineStr,POI 在 t="inlineStr" 单元格上 setCellValue 只写 <v>、
不更新 <is><t>,Excel/WPS 优先读 <is> 导致标题仍显示旧月份 → 写前先 setBlank();
· 不再直接搬 POI 对源表 RANK 的求值结果(源表同比留空时会 17 行全 1),改为写公式。

2) 同比显示成小数
- SummaryWorkbookFiller.ensureMonthYoyFormulas 改为无条件沿用上月同比列样式
(母版把新月份同比列做成了普通数值 0.00,回填公式后显示成小数)。

测试:《功能测试报告_汇总表排名页刷新与同比百分数_2026-09-11.md》
- 2026-08 导出 949,956 字节 / 23.8s,刷新排名页数值格数=711,缺数提示=[];
- 货运量排名与独立排名表逐格比对 454 格 0 差异;三页标题已到 1-8月/8月;
- 8 月同比列(10 个台账页 + 3 个排名页)全部 0.00%;
- 2026-07 回归:仅被刷新的排名页有预期差异,其余 16 页 0 差异。
3个文件已修改
1个文件已添加
316 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_汇总表排名页刷新与同比百分数_2026-09-11.md 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-11.md 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×ܱíÅÅÃûҳˢÐÂÓëͬ±È°Ù·ÖÊý_2026-09-11.md
New file
@@ -0,0 +1,80 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总表排名页刷新与同比百分数_2026-09-11
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
用户反馈《道路运输量汇总表》两个问题:**1. æ‰€æœ‰æŽ’名表里面的数据没有更新;2. åŒæ¯”应该是百分数而不是小数**。本轮定位并修复:
### 1. æŽ’名页数据刷新(问题 1)
- çŽ°è±¡ï¼šæ±‡æ€»å¤§è¡¨æ¯ç‰ˆ `2026å¹´8月道路运输量汇总表.xlsx` é‡Œçš„ `货运量排名` / `货运周转量排名` / `中口径排名` ä¸‰é¡µæ˜¯**上一报表期的静态值**(标题写「2026å¹´1-7月 / 2026å¹´7月」,数值也是 7 æœˆï¼‰ï¼Œå¯¼å‡º 8 æœˆæ—¶æ•´é¡µä¸éšæ•°æ®å˜åŒ–。
- å¤„置:导出链路新增 `refreshSummaryRankSheets`,用程序自己的独立排名表(`生成_货运量排名.xlsx` / `生成_周转量排名.xlsx` / `生成_中口径排名.xlsx`,与用户日常核对口径一致)刷新这三页:
  - æ•°å€¼æ ¼æ¬æ•°å€¼ï¼›æºä¾§æ˜¯å…¬å¼çš„(RANK/占比等)按目标块偏移整体平移后写成公式(打开时按汇总表自身数据重算);目标格本身已是公式的(母版 SUM/RANK/占比)保留不动;
  - å—标题随报表期同步(`2026å¹´1-7月…` â†’ `2026å¹´1-8月…`、`2026å¹´7月…` â†’ `2026å¹´8月…`)。
### 2. åŒæ¯”列显示成小数(问题 2)
- çŽ°è±¡ï¼šæ¯ç‰ˆæŠŠæ–°æœˆä»½ï¼ˆ8 æœˆï¼‰çš„同比列做成了「普通数值 `0.00`」格式,而 7 æœˆåŠä»¥å‰çš„同比列是 `0.00%`;回填公式后 8 æœˆåŒæ¯”显示成 `-0.0294` è¿™æ ·çš„小数。
- å¤„置:`SummaryWorkbookFiller.ensureMonthYoyFormulas` åŽŸæ¥åªåœ¨ç›®æ ‡æ ¼ä¸ºç©ºæ—¶å¥—ç”¨ä¸ŠæœˆåŒæ¯”åˆ—æ ·å¼ï¼Œæ”¹ä¸º**无条件沿用上月同比列样式**(`0.00%`)。
### æ”¹åŠ¨æ–‡ä»¶
- `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java`:新增 `refreshSummaryRankSheets` / `copyRankValues` / `rankCell` / `shiftFormula`,在 `ensureMonthFormulas` ä¹‹åŽã€`applySummaryReferenceLayout` ä¹‹å‰è°ƒç”¨ã€‚
- `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java`:`ensureMonthYoyFormulas` æ ·å¼æ²¿ç”¨ã€‚
## äºŒã€æµ‹è¯•环境
- åŽç«¯ `http://localhost:8090`(`start-dev.ps1` å¯åŠ¨ï¼‰ï¼Œå¯¼å‡ºæŽ¥å£ `GET /api/report/export/summaryWorkbook?period=YYYY-MM`。
- æ¯ç‰ˆï¼š`docs/生成汇总大表/2026å¹´8月道路运输量汇总表.xlsx`;版式参照:`docs/生成汇总大表/生成_道路运输量汇总表.xlsx`。
- ç‹¬ç«‹æŽ’名表底稿:`docs/货运/生成_货运量排名.xlsx`、`docs/货运/生成_周转量排名.xlsx`、`docs/公路旅客/生成_中口径排名.xlsx`。
- æµ‹è¯•时间:2026-09-11;测试产物:`_gen2_2026-08.xlsx`、`_gen2_2026-07.xlsx`、`_v2_freight_rank.xlsx`、`_v2_turnover_rank.xlsx`、`_v2_mid_rank.xlsx`。
## ä¸‰ã€ç”¨ä¾‹è¡¨
### 3.1 æ­£å‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-1 | å¯¼å‡º `period=2026-08` | HTTP 200,正常产出文件 | 949,956 å­—节,23.8 ç§’返回 | é€šè¿‡ |
| TC-2 | åŽç«¯å›žå¡«æ—¥å¿— | æœ‰æŽ’名页刷新统计、无缺数提示 | å†™å…¥å½“月值格数=1659,补写当月同比公式格数=600,补写当月合计公式格数=43,**刷新排名页数值格数=711**,对齐列宽列数=422,缺数提示=[] | é€šè¿‡ |
| TC-3 | è´§è¿é‡æŽ’名块标题 | éšæŠ¥è¡¨æœŸæ”¹ä¸º 1-8 æœˆ/8 æœˆ | `A1=2026å¹´1-8月全省分市州累计完成公路货运量情况`;`A22=2026å¹´8月全省分市州完成公路货运量情况` | é€šè¿‡ |
| TC-4 | è´§è¿å‘¨è½¬é‡æŽ’名块标题 | åŒä¸Š | `A2=…2026å¹´1-8月…`;`A24=…2026å¹´8月…` | é€šè¿‡ |
| TC-5 | ä¸­å£å¾„排名块标题 | åŒä¸Š | `J1=2026å¹´1-8月全省分市州累计完成道路客运生产情况`;`J22=2026å¹´8月…` | é€šè¿‡ |
| TC-6 | æ ‡é¢˜è½ç›˜æ–¹å¼ï¼ˆåŽŸå§‹ XML æ ¡éªŒï¼‰ | `<is><t>` ä¸Žæ˜¾ç¤ºä¸€è‡´ï¼ˆä¸èƒ½å†å‡ºçް `<v>` æ–°å€¼ + `<is>` æ—§å€¼ï¼‰ | `xl/worksheets/sheet2.xml`:`<c r="A1" s="776" t="s"><v>0</v></c>`,共享串 0 = `2026å¹´1-8月…`(旧文件是 inlineStr,`<v>`=8月、`<is><t>`=7月) | é€šè¿‡ |
| TC-7 | è´§è¿é‡æŽ’名逐格入库抽查(与独立排名表比对) | å…¨å—一致 | ç´¯è®¡å— + å½“月块共 **454 æ ¼é€ä¸ªæ¯”对,0 å¤„不一致** | é€šè¿‡ |
| TC-8 | è´§è¿å‘¨è½¬é‡æŽ’名当月块抽查 | ä¸Žç‹¬ç«‹è¡¨ä¸€è‡´ | æ­¦æ±‰ B28=192320.5226(源 B27)、G28=504369.61288609065(源 G27)、E28=0.3334642531154999(源 E27)、J28=-0.021161120884320206(源 J27) | é€šè¿‡ |
| TC-9 | è´§è¿å‘¨è½¬é‡æŽ’名母版公式保留 | ä¸å†™æ­»å€¼ï¼Œä¿ç•™ SUM/RANK/占比 | å…¨çœ B27=`=SUM(B28:B44)`;武汉 C28=`=RANK(B28,B$28:B$44)`、D28=`=B28/B$27` | é€šè¿‡ |
| TC-10 | ä¸­å£å¾„排名数值抽查 | ä¸Žç‹¬ç«‹è¡¨ä¸€è‡´ | K4=2759.8855(源 B4)、O4=152330.9312(源 F4)、K25=349.536(源 L4)、O25=18917.1637(源 P4) | é€šè¿‡ |
| TC-11 | ä¸­å£å¾„排名「增速排名」列 | ä¸å†æ˜¯é™æ€æ€ªå€¼ï¼Œä¸”与页内同比自洽 | ç”±é™æ€å€¼ï¼ˆå¤šè¡ŒåŒä¸º 1.0)改为 `=RANK(M4,M$4:M$20)`、`=RANK(M25,M$25:M$41)` ç­‰è‡ªæ´½å…¬å¼ï¼›ç¼“存值与母版原值一致(L25=5、N25=10、P25=2、R25=5) | é€šè¿‡ |
| TC-12 | 8 æœˆåŒæ¯”列格式(问题 2) | ç™¾åˆ†æ•° `0.00%` | è´§è¿ R5=`=Q5/AJ5-1` / `0.00%`;班线包车、城市客运、公交、出租、网约车、轨道轮渡、中口径明细 8 æœˆåŒæ¯”列(R åˆ—)**全部 `0.00%`** | é€šè¿‡ |
| TC-13 | æŽ’名页同比列格式 | ç™¾åˆ†æ•° `0.00%` | è´§è¿é‡æŽ’名 E4=-0.0763 / `0.00%`、D5=0.3207… / `0.00%`;货运周转量排名、中口径排名同比列同为 `0.00%` | é€šè¿‡ |
| TC-14 | æ‰“开即重算 | ä»å¸¦å…¨é‡é‡ç®—标记 | `<calcPr â€¦ fullCalcOnLoad="true"/>` ä¿ç•™ | é€šè¿‡ |
### 3.2 è´Ÿå‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-N1 | å…¨è¡¨æ‰«æã€Œè¡¨å¤´å«åŒæ¯” + æ•°å€¼/公式 + æ ¼å¼éžç™¾åˆ†æ•°ã€çš„æ ¼ | ä¸åº”有新引入的小数同比 | 8 æœˆä»¶å‘½ä¸­ 3 æ ¼ï¼Œå…¨éƒ¨åœ¨éšè—é¡µï¼ˆ`2025年城市客运量!E13`、`中口径计算过程!G85/G86`),母版同位置同样如此 | é€šè¿‡ï¼ˆéžæœ¬è½®å¼•入) |
| TC-N2 | ç‹¬ç«‹æŽ’名表源侧同比为空时(库中无 2025 å¹´ä¸­å£å¾„数据) | ä¸åº”把 POI å¯¹ç©ºå€¼çš„æ±‚值结果(全为 1)搬进汇总表 | æ”¹ä¸ºå†™å…¬å¼åŽï¼Œè¯¥åˆ—为 `=RANK(…)` å…¬å¼ï¼ŒæŒ‰é¡µå†…同比取值,不再是 17 è¡Œå…¨ 1 | é€šè¿‡ |
| TC-N3 | è°ƒè¯•日志清理 | å¯¼å‡ºæ—¥å¿—不应残留 `RANKDBG` | `backend.log` ä¸­ `RANKDBG` è®¡æ•° = 0 | é€šè¿‡ |
### 3.3 åŽ†å²æœˆå›žå½’ï¼ˆ2026-07)
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-R1 | å¯¼å‡º `period=2026-07` | æ­£å¸¸äº§å‡ºã€å½“月同比仍指向 2025 å¹´ 7 æœˆ | 941,903 å­—节;货运 P5=`=O5/AF5-1` / `0.00%` | é€šè¿‡ |
| TC-R2 | ä¸Žæ”¹åŠ¨å‰ 7 æœˆäº§ç‰©ï¼ˆ`_gen_2026-07.xlsx`)逐格 diff | å·®å¼‚仅限被刷新的排名页 | ä»… `货运量排名` 51 æ ¼ã€`中口径排名` 133 æ ¼æœ‰å·®å¼‚(排名页由静态值改为按数据刷新,属预期);其余 16 é¡µ 0 å·®å¼‚ | é€šè¿‡ |
| TC-R3 | 7 æœˆæŽ’名页标题 | ä»ä¸º 1-7 æœˆ/7 æœˆï¼ˆæŒ‰æŠ¥è¡¨æœŸï¼‰ | `A1=2026å¹´1-7月…`、`A22=2026å¹´7月…`;中口径 `J22=2026å¹´7月…` | é€šè¿‡ |
| TC-R4 | 7 æœˆåŒæ¯”格式 | æ— æ–°å¢žå°æ•°åŒæ¯” | å…¨è¡¨å‘½ä¸­ 3 æ ¼ï¼Œå‡ä¸ºéšè—é¡µæ¯ç‰ˆåŽŸæœ‰æ ¼ | é€šè¿‡ |
## å››ã€å‘现的问题与处置
1. **排名页标题改了但 Excel ä»æ˜¾ç¤ºæ—§æœˆä»½**:母版标题单元格是 `inlineStr`,POI åœ¨ `t="inlineStr"` å•元格上 `setCellValue(String)` åªå†™ `<v>`、不动 `<is><t>`,而 Excel/WPS ä¼˜å…ˆè¯» `<is><t>`。处置:写值前先 `setBlank()` æ¸…掉原单元格再写(落盘为共享串,`<is>` ä¸å†æ®‹ç•™æ—§æ–‡æ¡ˆï¼‰ã€‚
2. **POI æ±‚值口径偏差**:源独立排名表里手工留空的同比会让 `RANK` å‡½æ•°æ±‚值一律返回 1,直接搬值会把 17 è¡Œã€Œå¢žé€ŸæŽ’名」写成全 1。处置:源侧公式不再搬求值结果,而是按目标块偏移平移成公式写入,再用汇总表自身数据求值缓存。
3. **中口径排名同比列仍为母版旧值**:库中只有 2026-01~08 çš„客运数据(`h2031_enterprise_monthly` / `city_bus_monthly`),没有 2025 å®šç¨¿æ•°æ®ï¼Œç‹¬ç«‹æŽ’名表算不出中口径同比 â†’ è¯¥åˆ—按设计保留母版值;等 2025 æ•°æ®å…¥åº“后随导出自动刷新。
4. **静态页待办**:`城市汇总`、`城市分市州`、`中口径分析`、`道路运输周转量` å››é¡µåœ¨æ¯ç‰ˆé‡Œæ˜¯æ•´é¡µé™æ€ï¼ˆæœ¬è½®å¯¼å‡ºä¸Žæ¯ç‰ˆé€æ ¼ 0 å·®å¼‚),页标题仍写「1-7 æœˆ / 7 æœˆã€ã€‚因整页数据未数据驱动,本轮**未**只改标题(避免标题 8 æœˆã€æ•°æ® 7 æœˆï¼‰ï¼Œç•™å¾…后续做成数据驱动时一并处理。
## äº”、结论
- ç”¨æˆ·åé¦ˆçš„两个问题均已修复并验证:三张排名页的标题与数值随报表期刷新(货运量排名 454 æ ¼é€æ ¼æ¯”对 0 å·®å¼‚),8 æœˆåŒæ¯”列统一为百分数显示。
- 2026-08 / 2026-07 åŒæœŸå¯¼å‡ºé€šè¿‡ï¼Œ7 æœˆåŽ†å²æœˆå›žå½’å·®å¼‚ä»…é™è¢«åˆ·æ–°çš„æŽ’åé¡µï¼Œæ— å‰¯ä½œç”¨ã€‚
- é—留:中口径同比待 2025 å¹´æ•°æ®å…¥åº“;四张静态成品页待数据驱动改造。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-11.md
@@ -16,12 +16,22 @@
- ç­çº¿åŒ…车、轨道轮渡「全省」行补齐合计公式(母版原只对 1 æœˆå†™æ­»å…¬å¼ï¼‰ã€‚
- ç‰ˆå¼æŒ‰ç”¨æˆ·æŒ‡å®šå‚ç…§ä»¶ `docs/生成汇总大表/生成_道路运输量汇总表.xlsx` é€åˆ—对齐列宽/隐藏;**8 æœˆæ‰©åˆ— Q/R çš„宽度也取自参照件**。
### 3. æ±‡æ€»è¡¨æŽ’名页刷新 + åŒæ¯”百分数(用户新反馈 2 é¡¹ï¼‰
- ç”¨æˆ·åé¦ˆã€Œè´§è¿çš„æ±‡æ€»è¡¨é‡Œ 1.所有排名表数据没有更新;2.同比应该是百分数而不是小数」。
- æŽ’名页:母版里 `货运量排名`/`货运周转量排名`/`中口径排名` ä¸‰é¡µæ˜¯ä¸Šä¸€æœŸçš„静态值。导出时改用程序自己的独立排名表(生成_货运量排名/周转量排名/中口径排名)刷新这三页:数值搬值、源侧公式按偏移平移后写公式(打开重算)、母版自带公式保留、块标题随报表期同步(1-7月→1-8月)。
- åŒæ¯”格式:`ensureMonthYoyFormulas` æ”¹ä¸ºæ— æ¡ä»¶æ²¿ç”¨ä¸ŠæœˆåŒæ¯”列样式(母版把新月份同比列做成普通数值 `0.00`,显示成小数)。
- ä¿®æŽ‰ä¸¤ä¸ªå‘:① æ¯ç‰ˆæ ‡é¢˜æ˜¯ inlineStr,POI `setCellValue` åªå†™ `<v>` ä¸æ›´æ–° `<is>`,Excel ç»§ç»­æ˜¾ç¤º 7 æœˆ â†’ å…ˆ `setBlank()` å†å†™ï¼›â‘¡ ç›´æŽ¥æ¬ POI å¯¹æºè¡¨ RANK çš„æ±‚值结果会把「增速排名」写成 17 è¡Œå…¨ 1 â†’ æ”¹ä¸ºå¹³ç§»å†™å…¬å¼ã€‚
### 3. æ–‡æ¡£å½’æ¡£
- æ˜¨å¤©å·²å®Œæˆã€Šæ¹–北省道路货物运输量分市州统计方法(2023)编制说明》学习笔记归档(`docs/货运/学习笔记_分市州道路货物运输量统计方法_2026-09-10.md`),今天未再改动。
## äºŒã€æµ‹è¯•结果摘要(详见《功能测试报告》)
- `docs/功能测试报告/功能测试报告_网约车订单报告导入_2026-09-11.md`:TC-1~TC-7 å…¨éƒ¨é€šè¿‡ï¼›7 æœˆæŠ¥å‘Šè§£æž 17 å¸‚州、合计 29,730,791,与库中逐项一致;两类负向用例提示明确。
- `docs/功能测试报告/功能测试报告_汇总大表8月数据回填与版式对齐_2026-09-11.md`:TC-1~TC-16 å…¨éƒ¨é€šè¿‡ã€‚
- `docs/功能测试报告/功能测试报告_汇总表排名页刷新与同比百分数_2026-09-11.md`:正向 14 ä¾‹ / è´Ÿå‘ 3 ä¾‹ / å›žå½’ 4 ä¾‹å…¨éƒ¨é€šè¿‡ã€‚
  - 2026-08 å¯¼å‡ºæˆåŠŸï¼ˆ23.8s / 949,956 å­—节);日志「刷新排名页数值格数=711,缺数提示=[]」。
  - ä¸‰å¼ æŽ’名页标题已到 1-8 æœˆ/8 æœˆï¼›è´§è¿é‡æŽ’名与独立排名表逐格比对 454 æ ¼ 0 å·®å¼‚ï¼›8 æœˆåŒæ¯”列全部 `0.00%`。
  - 7 æœˆåŽ†å²æœˆå›žå½’ï¼šä»…è¢«åˆ·æ–°çš„æŽ’åé¡µæœ‰é¢„æœŸå·®å¼‚ï¼ˆè´§è¿é‡æŽ’å 51 æ ¼ã€ä¸­å£å¾„排名 133 æ ¼ï¼‰ï¼Œå…¶ä½™ 16 é¡µ 0 å·®å¼‚。
  - 2026-08 å¯¼å‡ºæˆåŠŸï¼ˆ4.6s / 948,735 å­—节);写入当月值 1659 æ ¼ã€å½“月同比公式 600 æ ¼ã€å½“月合计公式 43 æ ¼ã€ç¼ºæ•°æç¤ºä¸ºç©ºã€‚
  - è´§è¿ Î£17 å¸‚å·ž = å…¨çœï¼ˆ16888.7615);网约车/公交/出租/班线包车/轨道轮渡全省行与市州行均正确。
  - åˆ—宽与参照件**真实差异 0 åˆ—**。
@@ -32,9 +42,12 @@
2. 2026-08 å¯¼å‡ºä¼šæ‰“印「未找到同期备份母版(_备份_2026å¹´8月…)」告警,属无害提示(8 æœˆæ¯ç‰ˆæœ¬èº«å®Œæ•´ï¼‰ï¼ŒåŽç»­å¯è€ƒè™‘按需求决定是否消除。
3. æ ¹ç›®å½•堆积较多历史临时文件(`_t_*.py`、`_*.sql`、`_gen_*.xlsx`、`_bak_*.java`、`_token.txt` ç­‰ï¼‰ï¼Œå¾…用户同意后集中清理。
4. ç‰ˆå¼å‚ç…§ä»¶ `docs/生成汇总大表/生成_道路运输量汇总表.xlsx` ç›®å‰ä¸ºæœªå…¥åº“文件,是否纳入版本管理待确认。
5. ä¸­å£å¾„排名的「同比增速」列仍是母版旧值:库中只有 2026 å¹´æ•°æ®ï¼Œæ²¡æœ‰ 2025 å¹´ä¸­å£å¾„定稿数据,算不出 8 æœˆåŒæ¯”;等 2025 æ•°æ®å…¥åº“后自动刷新。
6. `城市汇总`/`城市分市州`/`中口径分析`/`道路运输周转量` å››é¡µåœ¨æ¯ç‰ˆé‡Œæ˜¯æ•´é¡µé™æ€ï¼ˆæœ¬è½®å¯¼å‡ºä¸Žæ¯ç‰ˆ 0 å·®å¼‚),标题仍写 7 æœˆï¼Œéœ€åŽç»­åšæˆæ•°æ®é©±åŠ¨åŽä¸€å¹¶æ›´æ–°ã€‚
## å››ã€æ˜Žå¤©è®¡åˆ’
1. ç”¨æˆ·é‡ä¼ çœŸå®ž 8 æœˆç½‘约车报告后,重新导出 2026-08 å¹¶é€é¡¹å¤æ ¸ç½‘约车页。
2. ç»§ç»­æŒ‰ç”¨æˆ·åé¦ˆæ ¸å¯¹ 8 æœˆæ±‡æ€»å¤§è¡¨æˆå“ï¼ˆé‡ç‚¹ï¼šä¸­å£å¾„各页、货运排名页)。
3. æ¸…理临时文件(需用户确认)。
4. è§†ç”¨æˆ·åé¦ˆå†³å®šæ˜¯å¦æŠŠå››å¼ é™æ€æˆå“é¡µï¼ˆåŸŽå¸‚汇总/城市分市州/中口径分析/道路运输周转量)做成数据驱动。
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
@@ -530,10 +530,9 @@
                String f = prev.getCellFormula();
                if (f == null || f.isEmpty()) continue;
                String shifted = shiftFormulaColumns(f, 2);
                if (target == null) {
                    target = row.createCell(yoyCol);
                    if (prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle());
                }
                if (target == null) target = row.createCell(yoyCol);
                // æ¯ç‰ˆæŠŠæ–°æœˆä»½çš„同比列做成了普通数值格式(显示成小数),统一沿用上月同比列的样式(百分数)
                if (prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle());
                try {
                    target.setCellFormula(shifted);
                    written++;
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -53,6 +53,7 @@
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -3351,10 +3352,11 @@
            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems);
            int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
            int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
            int widthApplied = applySummaryReferenceLayout(wb);
            log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={},"
                    + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, widthApplied, dbProblems);
                    + "刷新排名页数值格数={},按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, widthApplied, dbProblems);
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
@@ -3441,6 +3443,216 @@
        return max;
    }
    // ==================== æ±‡æ€»å·¥ä½œç°¿å†…的排名页刷新 ====================
    /**
     * æ±‡æ€»å·¥ä½œç°¿é‡Œçš„「货运量排名 / è´§è¿å‘¨è½¬é‡æŽ’名 / ä¸­å£å¾„排名」在母版里是上一报表期的静态值,
     * å¯¼å‡ºæ—¶ç”¨ç‹¬ç«‹æŽ’名表(《生成_货运量排名》《生成_周转量排名》《生成_中口径排名》,与用户日常核对的口径一致)
     * çš„结果刷新数值,避免汇总表里的排名页停留在旧月份。
     * æ•°å€¼æ ¼æ¬å€¼ã€å…¬å¼æ ¼æŒ‰åç§»å¹³ç§»å…¬å¼åŽå†™å…¥ï¼›ç›®æ ‡æ ¼æœ¬èº«å·²æ˜¯å…¬å¼çš„(SUM/RANK/占比/补数)保留不动,打开时按新数据重算。
     */
    private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode) throws Exception {
        int n = 0;
        n += copyRankValues(wb, "货运量排名", exportFreightRank(period, mode), new int[][]{{0, 0}, {22, 0}});
        n += copyRankValues(wb, "货运周转量排名", exportTurnoverRank(period, mode), new int[][]{{0, 0}, {22, 0}});
        n += copyRankValues(wb, "中口径排名", exportPassengerMidRank(period, mode), new int[][]{{0, 0}, {0, 10}});
        return n;
    }
    /**
     * æŠŠç‹¬ç«‹æŽ’名表的各个块按标题对齐搬进汇总大表对应页。
     * srcBlocks[i] = æºå— i çš„èµ·å§‹ {行, åˆ—}(0 åŸºï¼‰ï¼›ç›®æ ‡å—按标题顺序(累计块在前、当月块在后)对应。
     * æ•°å€¼æ ¼ç›´æŽ¥æ¬å€¼ï¼›æºä¾§æ˜¯å…¬å¼çš„(排名/占比等)按目标块偏移整体平移后写入公式,打开时按汇总表自身数据重算。
     */
    private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks) throws Exception {
        XSSFSheet dst = dstWb.getSheet(dstSheetName);
        if (dst == null) return 0;
        List<XSSFCell> dstTitles = rankTitleCells(dst);
        if (dstTitles.isEmpty()) return 0;
        int copied = 0;
        FormulaEvaluator dstEv = dstWb.getCreationHelper().createFormulaEvaluator();
        try (InputStream sin = new ByteArrayInputStream(srcBytes);
             XSSFWorkbook srcWb = new XSSFWorkbook(sin)) {
            XSSFSheet src = srcWb.getSheetAt(0);
            FormulaEvaluator ev = srcWb.getCreationHelper().createFormulaEvaluator();
            int blocks = Math.min(srcBlocks.length, dstTitles.size());
            for (int b = 0; b < blocks; b++) {
                int sr0 = srcBlocks[b][0];
                int sc0 = srcBlocks[b][1];
                XSSFCell dstTitle = dstTitles.get(b);
                int rowOff = dstTitle.getRowIndex() - sr0;
                int colOff = dstTitle.getColumnIndex() - sc0;
                int h = rankBlockHeight(src, sr0, sc0);
                int w = rankBlockWidth(src, sr0, sc0);
                for (int r = 0; r < h; r++) {
                    Row srcRow = src.getRow(sr0 + r);
                    if (srcRow == null) continue;
                    Row dstRow = dst.getRow(sr0 + r + rowOff);
                    if (dstRow == null) dstRow = dst.createRow(sr0 + r + rowOff);
                    for (int c = 0; c < w; c++) {
                        Cell sc = srcRow.getCell(sc0 + c);
                        if (sc == null) continue;
                        int dc = sc0 + c + colOff;
                        if (r == 0) {
                            // æ ‡é¢˜è¡Œï¼šåŒæ­¥å—标题文案。母版标题是 inlineStr,POI çš„ setCellValue åªå†™ <v>、
                            // ä¸æ›´æ–° <is>,Excel ä¼šç»§ç»­æ˜¾ç¤ºæ—§æœˆä»½ï¼Œå¿…须先 setBlank æ¸…掉再写。
                            Cell dt = dstRow.getCell(dc);
                            if (dt == null) dt = rankCell(dstRow, dc);
                            if (sc.getCellType() == CellType.STRING && dt.getCellType() != CellType.FORMULA) {
                                String text = sc.getStringCellValue();
                                String old = dt.getCellType() == CellType.STRING ? dt.getStringCellValue() : null;
                                if (text != null && !text.equals(old)) {
                                    dt.setBlank();
                                    dt.setCellValue(text);
                                }
                            }
                            continue;
                        }
                        Cell dcCell = dstRow.getCell(dc);
                        if (dcCell != null && dcCell.getCellType() == CellType.FORMULA) continue; // ä¿ç•™æ¯ç‰ˆè‡ªå·±çš„公式
                        if (sc.getCellType() == CellType.FORMULA) {
                            // æºä¾§å…¬å¼ï¼ˆæŽ’名/占比):按目标块偏移平移后原样写入,再按汇总表自身数据求值缓存。
                            // ä¸ç›´æŽ¥æ¬ POI å¯¹æºè¡¨çš„æ±‚值结果(如 RANK å¯¹æ‰‹å·¥ç•™ç©ºçš„同比一律返回 1)。
                            String f = sc.getCellFormula();
                            if (f == null || f.isEmpty()) continue;
                            String shifted = shiftFormula(f, colOff, rowOff);
                            if (dcCell == null) dcCell = rankCell(dstRow, dc);
                            dcCell.setCellFormula(shifted);
                            try {
                                dstEv.evaluateFormulaCell(dcCell);
                            } catch (Exception ignore) {
                                // æ±‚值失败不阻塞导出:打开时由 Excel/WPS é‡ç®—
                            }
                            copied++;
                            continue;
                        }
                        Double v = rankNumeric(sc, ev);
                        if (v == null) continue;
                        if (dcCell == null) dcCell = rankCell(dstRow, dc);
                        dcCell.setCellValue(v);
                        copied++;
                    }
                }
            }
        }
        return copied;
    }
    /** ç›®æ ‡å—缺格时新建,并沿用同行左邻格样式(防新格丢边框/百分数格式) */
    private Cell rankCell(Row row, int col) {
        Cell c = row.createCell(col);
        Cell left = col > 0 ? row.getCell(col - 1) : null;
        if (left != null) c.setCellStyle(left.getCellStyle());
        return c;
    }
    /** å…¬å¼å†…所有 A1 å¼•用整体平移:列 +dCol、行 +dRow(SUM/RANK ç­‰å‡½æ•°ååŽæŽ¥æ‹¬å·ä¸ä¼šè¢«åŒ¹é…ï¼‰ */
    private String shiftFormula(String formula, int dCol, int dRow) {
        if (formula == null || (dCol == 0 && dRow == 0)) return formula;
        java.util.regex.Matcher m = java.util.regex.Pattern
                .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)")
                .matcher(formula);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            String col = m.group(2);
            int idx = 0;
            for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1);
            int nidx = Math.max(1, idx + dCol);
            StringBuilder nc = new StringBuilder();
            while (nidx > 0) {
                int rem = (nidx - 1) % 26;
                nc.insert(0, (char) ('A' + rem));
                nidx = (nidx - 1) / 26;
            }
            int nrow = Math.max(1, Integer.parseInt(m.group(4)) + dRow);
            m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + nrow));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    /** æŽ’名页里的“块标题”单元格(含“全省分市州”的说明文字),按行、列顺序返回 */
    private List<XSSFCell> rankTitleCells(Sheet sh) {
        List<XSSFCell> out = new ArrayList<>();
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != CellType.STRING) continue;
                String t = cell.getStringCellValue();
                if (t != null && t.contains("全省分市州")) out.add((XSSFCell) cell);
            }
        }
        return out;
    }
    /** å—高度:从起始行向下直到整行为空(在块列范围内) */
    private int rankBlockHeight(Sheet sh, int r0, int c0) {
        int h = 0;
        for (int r = r0; r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            boolean any = false;
            if (row != null) {
                int last = row.getLastCellNum();
                for (int c = c0; c < last; c++) {
                    if (rankHasContent(row.getCell(c))) { any = true; break; }
                }
            }
            if (!any) break;
            h++;
        }
        return h;
    }
    /** å—宽度:从起始列向右直到整列为空(在块行范围内,扫描上限 40 åˆ—) */
    private int rankBlockWidth(Sheet sh, int r0, int c0) {
        int w = 0;
        for (int c = c0; c < c0 + 40; c++) {
            boolean any = false;
            for (int r = r0; r < r0 + 30; r++) {
                Row row = sh.getRow(r);
                if (row != null && rankHasContent(row.getCell(c))) { any = true; break; }
            }
            if (!any) break;
            w++;
        }
        return w;
    }
    private boolean rankHasContent(Cell c) {
        if (c == null) return false;
        CellType t = c.getCellType();
        if (t == CellType.BLANK) return false;
        if (t == CellType.STRING) {
            String v = c.getStringCellValue();
            return v != null && !v.trim().isEmpty();
        }
        return true;
    }
    /** å–单元格数值(公式取求值结果,纯数字文本按数值解析),取不到返回 null */
    private Double rankNumeric(Cell c, FormulaEvaluator ev) {
        try {
            CellType t = c.getCellType();
            if (t == CellType.NUMERIC) return c.getNumericCellValue();
            if (t == CellType.FORMULA) {
                org.apache.poi.ss.usermodel.CellValue cv = ev.evaluate(c);
                return cv != null && cv.getCellType() == CellType.NUMERIC ? cv.getNumberValue() : null;
            }
            if (t == CellType.STRING) {
                String v = c.getStringCellValue();
                if (v == null) return null;
                String x = v.replace(",", "").trim();
                if (x.isEmpty()) return null;
                try { return Double.parseDouble(x); } catch (NumberFormatException ignore) { return null; }
            }
        } catch (Exception ignore) {
            return null;
        }
        return null;
    }
    /** å®šä½ç‰ˆå¼å‚ç…§ä»¶ docs/生成汇总大表/生成_道路运输量汇总表.xlsx(优先级同母版:配置目录 â†’ user.dir ç›¸å¯¹ â†’ ä¸Šçº§ï¼‰ */
    private File resolveSummaryLayoutRef() {
        String rel = summaryTemplateDir;