4个文件已修改
4个文件已添加
1455 ■■■■■ 已修改文件
.gitignore 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_汇总大表四页数据驱动_2026-09-13.md 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_部署包打包与独立启动_2026-09-13.md 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-13.md 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/打包部署说明.md 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-deploy.py 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 496 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/security/utils/JwtUtils.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -67,3 +67,10 @@
# ==== packaged static dir (copied from dist, not committed) ====
web/
traffic-audit-server/web/
# ==== éƒ¨ç½²äº§ç‰©ï¼ˆä¸å…¥åº“)====
deploy/
traffic-audit-deploy-*.zip
_t_wyc_aug.docx
_deploytest/
traffic_audit_full_*.sql
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü´ó±íËÄÒ³Êý¾ÝÇý¶¯_2026-09-13.md
New file
@@ -0,0 +1,146 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表四页数据驱动_2026-09-13
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
### 1. èƒŒæ™¯
用户反馈:导出 2026 å¹´ 8 æœˆã€Šé“路运输量汇总表》后,以下 4 å¼ **跨页合成页**仍是上一报表期的静态值(标题写「2026å¹´1-7月 / 2026å¹´7月」,数值也是 7 æœˆçš„,不随报表期变化):
| é¡µå | æ¯ç‰ˆåŽŸçŠ¶æ€ |
| --- | --- |
| `城市汇总` | æ•´é¡µé™æ€ï¼ˆ1-7 æœˆç´¯è®¡ + 7 æœˆå½“月) |
| `城市分市州` | 4 å—整页静态 |
| `中口径分析` | 2 å—整页静态 |
| `道路运输周转量` | 2 å—整页静态 |
用户选定**方案 A**:沿用货运排名页的做法——用母版台账页自带的 2025 å¹´åŒæœŸåˆ—缓存值当同比基数,把「当年累计值」刷成 1-8 æœˆï¼ˆåŒæ¯”列沿用母版口径),4 é¡µä¸€æ¬¡æ€§åšå‡ºæ¥ã€‚
### 2. å£å¾„(先验证、后编码)
编码前先用母版台账页逐格聚合反推四页口径,与四页成品逐格比对 **0 å·®å¼‚**后确认:
| æŒ‡æ ‡ | å£å¾„ |
| --- | --- |
| åŸŽå¸‚客运量 | å…¬äº¤åŸŽå¸‚内客运量 + å·¡æ¸¸å‡ºç§ŸåŸŽå¸‚内客运量 + ç½‘约车城市内客运量 + è½¨é“客运量 + è½®æ¸¡å®¢è¿é‡ |
| åŸŽå¸‚客运周转量 | åŒä¸Šï¼Œæ¢æˆå„自旅客周转量 |
| ä¸­å£å¾„客运量/周转量 | å…¬è·¯ç­çº¿åŒ…车 + åŸŽé™…城乡公交 + åŸŽé™…城乡巡游出租 + åŸŽé™…城乡网约车,其中「城际城乡 X = X æ€»é‡ âˆ’ X åŸŽå¸‚内」(仅公交/出租/网约车;班线包车无城市内概念,直接用总量) |
| é“路货运周转量 | è´§è¿é¡µå¸‚州「规上货物周转量 + è§„下货物周转量」(**不用**母版「规上+规下」公式行) |
| é“路运输周转量页「道路客运周转量」 | ç­çº¿åŒ…车周转量 + åŸŽé™…城乡公交/出租/网约车周转量 |
| åŒæ¯” | å½“期 Ã· æ¯ç‰ˆå°è´¦é¡µ 2025 å¹´åŒæœŸåˆ— âˆ’ 1 |
**关于 2025 åŒæœŸåŸºæ•°**:库中只有 2026 å¹´åŸŽå¸‚客运/网约车数据,没有 2025 å¹´æ•°æ®ï¼Œæ•… 2025 åŒæœŸä¸€å¾‹å–**母版台账页自带的 2025 å¹´é€æœˆåˆ—缓存值**(V=1月、X=2月 â€¦ AJ=8月)。这也正是方案 A çš„取巧点:母版台账页本身带全 2025 é€æœˆåˆ—,无需等 2025 æ•°æ®å…¥åº“。
### 3. å®žçް
改动文件:`traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java`(+494 âˆ’2)。
- æ–°å¢žå…¥å£ `refreshSummaryCumulativeSheets(XSSFWorkbook wb, int year, int month)`;接在导出链路的 `refreshSummaryRankSheets(...)` ä¹‹åŽã€`applySummaryReferenceLayout(wb)` ä¹‹å‰è°ƒç”¨ã€‚
- å°è´¦å–æ•° `smReadLedgerMetrics`:**按行标签识别、不写死行号**
  - `公交`/`出租`/`网约车`:每市州「客运量 / æ—…客周转量 / å…¶ä¸­åŸŽå¸‚内客运量 / å…¶ä¸­åŸŽå¸‚内旅客周转量」4 è¡Œï¼›
  - `轨道、轮渡`:轨道(全省/武汉/黄石)与轮渡(全省/武汉)分块;
  - `班线包车`:全省与 17 å¸‚州客运量/周转量(「其中个体」行不重复计入);
  - ` è´§è¿`(页名带前导空格):市州「规上货物周转量 + è§„下货物周转量」。
- æœˆä»½åˆ—:2026 å¹´é€æœˆåˆ— C=1月 â€¦ Q=8月(`SM_CUR_COL0=2`),2025 å¹´åŒæœŸåˆ— V=1月 â€¦ AJ=8月(`SM_LAST_COL0=21`),每月间隔 2 åˆ—(值列 + åŒæ¯”列)。
- 19 ç»´æŒ‡æ ‡ä½ï¼ˆ`SM_DIM=19`):公交/出租/网约车 å„ 4 ç»´ï¼ˆæ€»é‡ã€å‘¨è½¬é‡ã€åŸŽå¸‚内总量、城市内周转量)+ è½¨é“ 2 ç»´ + è½®æ¸¡ 2 ç»´ + ç­çº¿åŒ…车 2 ç»´ + è´§è¿å‘¨è½¬é‡ 1 ç»´ã€‚
- å†™å€¼è§„则 `smSetVal`:**目标格原本是公式 â†’ ä¿ç•™æ¯ç‰ˆå…¬å¼ä¸è¦†ç›–**(打开即重算);目标格为空且新值为 0 â†’ ä¸æ–°å¢žï¼›å…¶ä½™å…ˆ `setBlank()` å†å†™ï¼Œé¿å… inlineStr æ®‹ç•™ã€‚
- å››é¡µå®žé™…写入 **1056 æ ¼**。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ `http://localhost:8090`(`start-dev.ps1` å¯åŠ¨ï¼ŒPID 27840),前端 `http://localhost:8080`(PID 23416)均就绪。
- æ¯ç‰ˆï¼š`docs/生成汇总大表/2026å¹´8月道路运输量汇总表.xlsx`;版式参照:`docs/生成汇总大表/生成_道路运输量汇总表.xlsx`。
- å¯¼å‡ºæŽ¥å£ï¼š`GET /api/report/export/summaryWorkbook?period=YYYY-MM`(Bearer token)。
- æµ‹è¯•时间:2026-09-13。
- æµ‹è¯•产物:`_t3_2026-08.xlsx`(950,783 å­—节 / HTTP 200 / 36.8 ç§’)、`_t3_2026-07.xlsx`(942,691 å­—节 / HTTP 200 / 25.7 ç§’);归档件 `docs/生成汇总大表/输出/生成_道路运输量汇总表_2026-08_四页数据驱动_2026-09-13.xlsx`(950,783 å­—节)。
- ç‹¬ç«‹å¤æ ¸è„šæœ¬ï¼š`_v5_four.py`(输出 `_v5_four.txt`)、`_verify_four.py`(输出 `_verify_four.txt`)。
## ä¸‰ã€ç”¨ä¾‹è¡¨
### 3.1 æ­£å‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-1 | å¯¼å‡º `period=2026-08` | HTTP 200,正常产出 | 950,783 å­—节,36.8 ç§’ | é€šè¿‡ |
| TC-2 | ä¸Ž 09-13 13:06 å½’档件比对 | åŒå‚数导出结果确定 | ä¸¤æ¬¡äº§ç‰©åŒä¸º 950,783 å­—节 | é€šè¿‡ |
| TC-3 | åŽç«¯å›žå¡«æ—¥å¿— | æœ‰å››é¡µåˆ·æ–°ç»Ÿè®¡ã€æ— ç¼ºæ•°æç¤ºã€æ— å¼‚常 | ã€Œåˆ·æ–°å››é¡µï¼ˆåŸŽå¸‚汇总/城市分市州/中口径分析/道路运输周转量)数值格数=1056」「刷新排名页数值格数=711」「缺数提示=[]」;最近 4MB æ—¥å¿—æ—  `ERROR`/`Exception`/`WARN` | é€šè¿‡ |
| TC-4 | `城市汇总` æ ‡é¢˜ | éšæŠ¥è¡¨æœŸæ”¹ä¸º 1-8 æœˆ / 8 æœˆ | `A1=2026å¹´1-8月全省累计完成城市客运量情况`、`A12=2026å¹´8月全省完成城市客运量情况` | é€šè¿‡ |
| TC-5 | `城市汇总` ç´¯è®¡å—(B3:E9) | å®¢è¿é‡/同比/周转量/同比 å„ 7 è¡Œ | 28 æ ¼å…¨éƒ¨ä¸€è‡´ï¼ˆå¦‚ B3=269628.07、C3=0.00117、D3=1681467.65、E3=0.00835) | é€šè¿‡ |
| TC-6 | `城市汇总` å½“月块(B14:E20) | åŒä¸Šï¼Œå½“月口径 | 28 æ ¼å…¨éƒ¨ä¸€è‡´ï¼ˆå¦‚ B14=32215.74、C14=0.0444) | é€šè¿‡ |
| TC-7 | `城市分市州` æ ‡é¢˜ Ã—4 | 4 å—标题随报表期 | `A1=…1-8月累计情况`、`A23=…1-8月累计情况`、`A46=…8月情况`、`A68=…8月情况` | é€šè¿‡ |
| TC-8 | `城市分市州` 4 å— Ã— 17 å¸‚å·ž Ã— 6 å€¼åˆ— | 6 ç»„值 = æ€»é‡/公交/出租/网约车/轨道/轮渡 | 408 æ ¼å…¨éƒ¨ä¸€è‡´ | é€šè¿‡ |
| TC-9 | `中口径分析` æ ‡é¢˜ + æ•°å€¼ | 2 å—标题 + æ¯å— æ€»/班线/城乡公交/城乡出租/城乡网约车 | `A1=1-8月中口径客运量及周转量`、`A10=8月中口径客运量及周转量`;B3=31528.0607、F3=1227147.4890 ç­‰ 10 æ ¼å…¨éƒ¨ä¸€è‡´ | é€šè¿‡ |
| TC-10 | `道路运输周转量` æ ‡é¢˜ + æ•°å€¼ | 2 å—标题 + 17 å¸‚å·ž G/L/Q/W/X/Y | `A1=2026å¹´1-8月全省累计完成交通周转量情况`、`A23=2026å¹´8月全省完成交通周转量情况`;102 æ ¼å…¨éƒ¨ä¸€è‡´ï¼ˆå¦‚ G4=17492129.04788293、L4=269628.07、Q4=1227147.48902;W/X/Y åŒæœŸåˆ—取 2025 å¹´åŒæœŸç´¯è®¡ï¼Œå¦‚ W4=17492068.4575、X4=269312.48、Y4=1256684.2157) | é€šè¿‡ |
### 3.2 è´Ÿå‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-N1 | ç›®æ ‡æ ¼æ˜¯æ¯ç‰ˆå…¬å¼ï¼ˆ`城市分市州` å…¨çœè¡Œ `B5=G5+L5+Q5+V5+X5`、`道路运输周转量` `J4=G4/W4-1`) | ä¸è¢«å†™å€¼è¦†ç›–,保留母版公式 | ä¸¤è€…仍为公式;四页写入格数 1056 < å››é¡µç†è®ºæ ¼æ•°ï¼Œè¯´æ˜Žå…¬å¼æ ¼è¢«è·³è¿‡ | é€šè¿‡ |
| TC-N2 | ç›®æ ‡æ ¼ä¸ºç©ºä¸”新值为 0 | ä¸æ–°å¢ž 0,避免把成品页写成满页 0 | `SM_货运` æ— æ•°æ®çš„市州轨道/轮渡值未产生新增 0 æ ¼ | é€šè¿‡ |
| TC-N3 | 2025 åŒæœŸæ— åº“数据 | å–母版台账页 2025 åˆ—缓存,不出现 NaN/Inf | åŒæ¯”全部为有限数(如 `城市汇总 C7=0.4870`、`C14=0.0444`) | é€šè¿‡ |
| TC-N4 | å°è´¦é¡µè¯»ä¸åˆ°å¸‚州数据(极端情况) | è®° `WARN` å¹¶**保持母版原值**、不写坏页 | ä»£ç è·¯å¾„ `curCum.isEmpty()` â†’ `log.warn("…台账页未读到市州数据,跳过(保持母版原值)")`;本轮台账页数据齐全,未实际触发(**代码审查用例**) | é€šè¿‡ï¼ˆå®¡æŸ¥ï¼‰ |
| TC-N5 | æ¯ç‰ˆæ ‡é¢˜ä¸º inlineStr | æ ‡é¢˜å¿…须真正落盘更新,不能只改 `<v>` | `smSetTitle` å…ˆ `setBlank()` å†å†™ï¼Œæ ‡é¢˜æ ¡éªŒå…¨éƒ¨è¯»å–到新文案 | é€šè¿‡ |
### 3.3 å›žå½’用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-R1 | å¯¼å‡º `period=2026-07` | ä¸å´©ï¼Œä¸”四页应回退成「1-7 æœˆã€å£å¾„ | HTTP 200 / 942,691 å­—节 / 25.7 ç§’;标题 `城市汇总 A1=2026å¹´1-7月…`、`城市客运客运量分市州1-7月累计情况`、`1-7月中口径客运量及周转量`、`2026å¹´1-7月全省累计完成交通周转量情况` | é€šè¿‡ |
| TC-R2 | 2026-07 å¯¼å‡ºå€¼ vs æ¯ç‰ˆ 1-7 æœˆé™æ€æƒå¨å€¼ | **应完全一致**(证明口径与母版一致,只是随月份延长) | åŸŽå¸‚汇总 B3=237412.33000000002(母版 237412.33)、中口径 B3=27524.4906、道路运输周转量 G4=15222118.44638293、城市分市州 B6=135474.77989442425 â€”— ä¸Žæ¯ç‰ˆé€é¡¹ä¸€è‡´ | é€šè¿‡ |
| TC-R3 | åŽç«¯æœåŠ¡å¯ç”¨æ€§ | å‰ç«¯é¦–页可访问 | `http://localhost:8080/` HTTP 200,标题「交通统计报表审核系统」 | é€šè¿‡ |
### 3.4 é€æ ¼ç‹¬ç«‹å¤æ ¸ï¼ˆå…³é”®è¯æ®ï¼‰
**方法**:从生成件**自身的台账页**重新聚合(不读库、不参考任何其它文件),再与四页逐格比对。
| å¤æ ¸èŒƒå›´ | æ ¼æ•° |
| --- | --- |
| æ ‡é¢˜ï¼ˆåŸŽå¸‚汇总 2 + åŸŽå¸‚分市州 4 + ä¸­å£å¾„ 2 + é“路运输周转量 2) | 10 |
| åŸŽå¸‚汇总 ç´¯è®¡å—(7 è¡Œ Ã— å®¢è¿é‡/同比/周转量/同比) | 28 |
| åŸŽå¸‚汇总 å½“月块(同上) | 28 |
| åŸŽå¸‚分市州 4 å— Ã— 17 å¸‚å·ž Ã— 6 å€¼åˆ— | 408 |
| ä¸­å£å¾„分析 2 å— Ã— 5 è¡Œ Ã— 2 åˆ— | 10 |
| é“路运输周转量 2 å— Ã— 17 å¸‚å·ž Ã— 6 åˆ—(G/L/Q + J/O/T + W/X/Y) | 102 |
| **合计** | **586** |
结果:**比对 586 æ ¼ï¼Œå·®å¼‚ 0 æ ¼**(`_v5_four.txt` æœ«å°¾ã€Œæ±‡æ€»ï¼šæ¯”对格数 = 586,差异格数 = 0」)。
补充自洽校验(全省行 = Î£17 å¸‚州):道路运输周转量 G4=17492129.04788293 = Î£(G5:G21)、L4=269628.07 = Î£(L5:L21)、Q4=1227147.48902 = Î£(Q5:Q21)、W4/X4/Y4 åŒç†ï¼ˆå·® â‰¤ 6e-11,浮点误差级)。
另:13:06 é‚£ä¸€ç‰ˆå½’档件用 `_verify_four.py` å¤æ ¸ï¼ˆå«ã€Œä¸Žæ¯ç‰ˆ 1-7 æœˆé™æ€å€¼å¯¹ç…§ã€å°èŠ‚ï¼‰ï¼ŒåŒæ ·**差异 0 æ ¼**。
与母版静态值的对照(证明「确实从静态变动态」):
| å•元格 | æ¯ç‰ˆï¼ˆ1-7 æœˆé™æ€ï¼‰ | ç”Ÿæˆä»¶ï¼ˆ1-8 æœˆï¼‰ |
| --- | --- | --- |
| åŸŽå¸‚汇总 B3 | 237412.33 | 269628.07 |
| åŸŽå¸‚汇总 A12 | 2026å¹´7月全省完成城市客运量情况 | 2026å¹´8月全省完成城市客运量情况 |
| åŸŽå¸‚分市州 B6 | 135474.779894424 | 153695.70483259356 |
| ä¸­å£å¾„分析 B3 | 27524.4906 | 31528.060700000005 |
| é“路运输周转量 G4 | 15222118.4463982 | 17492129.04788293 |
## å››ã€å‘现的问题与处置
1. **母版标题是 inlineStr**:POI ç›´æŽ¥ `setCellValue` åªå†™ `<v>`、不改 `<is>`,Excel ä»æ˜¾ç¤ºæ—§æ–‡æ¡ˆï¼ˆ7 æœˆï¼‰ã€‚→ `smSetTitle` æ”¹ä¸ºå…ˆ `setBlank()` å†å†™ã€‚
2. **成品页里混有母版公式**(城市分市州全省行、道路运输周转量 J åˆ—同比等):若一律写值会破坏母版自洽公式。→ `smSetVal` é‡ `FORMULA` ç›´æŽ¥è·³è¿‡ï¼Œå†™å…¥æ ¼æ•° 1056 å°äºŽå››é¡µç†è®ºæ ¼æ•°å³ä¸ºæ­¤ã€‚
3. **城市汇总第 5 è¡Œæ˜¯ã€Œå‡ºç§Ÿ+网约车」合计**,不是单出租。→ ä»£ç æŒ‰ã€Œå‡ºç§Ÿ + ç½‘约车」写入(母版第 6 è¡Œæ‰æ˜¯å•出租),与母版口径一致。
4. **中口径的「城际城乡」不能直接取台账总量**。→ æŒ‰ã€Œæ€»é‡ âˆ’ åŸŽå¸‚内」计算(仅公交/出租/网约车)。
5. **库中无 2025 å¹´åŸŽå¸‚客运/网约车数据**。→ åŒæ¯”基数取母版台账页 2025 å¹´é€æœˆåˆ—缓存值(方案 A çš„æ ¸å¿ƒï¼‰ï¼ŒåŽç»­ 2025 æ•°æ®å…¥åº“后可再切换为读库。
## äº”、结论
- æ­£å‘ 10 ä¾‹ã€è´Ÿå‘ 5 ä¾‹ã€å›žå½’ 3 ä¾‹ã€é€æ ¼ç‹¬ç«‹å¤æ ¸ 586 æ ¼ï¼Œå…¨éƒ¨é€šè¿‡ã€‚
- å››å¼ è·¨é¡µåˆæˆé¡µå·²ä»Ž**静态**变为**随报表期数据驱动**:导出 2026-08 æ—¶æ ‡é¢˜ä¸Žæ•°å€¼å‡ä¸º 1-8 æœˆ/8 æœˆå£å¾„;导出 2026-07 æ—¶åˆèƒ½ç²¾ç¡®å›žé€€æˆä¸Žæ¯ç‰ˆä¸€è‡´çš„ 1-7 æœˆæƒå¨å€¼ã€‚
- æ¯ç‰ˆå…¬å¼ä¿ç•™ã€åŒæ¯”基数沿用母版 2025 ç¼“存,报表打开后仍走母版自带的重算链路(`fullCalcOnLoad`)。
- é—留:中口径排名页的「同比增速」列仍是母版旧值(库中无 2025 å¹´ä¸­å£å¾„定稿数据,算不出同比),待 2025 æ•°æ®å…¥åº“后刷新。
## å…­ã€å‘布与二次独立验证(2026-09-13)
- æäº¤ `ea7cc07`(`ReportExportService.java` + æœ¬æŠ¥å‘Š + ã€Šå·¥ä½œæ—¥ç»“_2026-09-13》共 3 æ–‡ä»¶ï¼Œ+689 âˆ’2)已推送双仓库:
  `git push gitea main`(`09e612a..ea7cc07`)、`git push origin main:master`(`09e612a..ea7cc07`),并 `git branch -f master main`。
- æŽ¨é€å‰ `git fetch gitea` / `git fetch origin` å‡ç¡®è®¤è¿œç«¯æ— æ–°æäº¤ï¼ˆ`git rev-list --left-right --count` å‡ä¸º `0 0`),未覆盖同事代码。
- **发布后复跑**:重新导出 `period=2026-08` â†’ HTTP 200 / 950,783 å­—节 / 29.2 ç§’(第三次同尺寸);对发布后产物 `_t4_2026-08.xlsx`
  å†è·‘一次 586 æ ¼ç‹¬ç«‹å¤æ ¸ â†’ **差异 0 æ ¼**(`_v6_four.txt`)。
- **容器级比对**:把发布前产物 `_t3_2026-08.xlsx` ä¸Žå‘布后产物 `_t4_2026-08.xlsx` é€ zip æ¡ç›®æ¯”对,**全部内部条目
  ï¼ˆ`xl/worksheets/*`、`xl/sharedStrings.xml` ç­‰ï¼‰å­—节完全一致**;两者 MD5 ä¸åŒä»…源于 xlsx å®¹å™¨çš„ zip å¤´æ—¶é—´æˆ³ï¼ŒåŠŸèƒ½ç­‰ä»·ã€‚
- è¿è¡Œä¸­çš„后端(PID 27840)执行的代码即已提交版本(工作区对该 Java æ–‡ä»¶ `git diff HEAD` ä¸ºç©ºï¼‰ã€‚
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_²¿Êð°ü´ò°üÓë¶ÀÁ¢Æô¶¯_2026-09-13.md
New file
@@ -0,0 +1,91 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_部署包打包与独立启动_2026-09-13
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
用户需要把系统打包部署到另一台机器。本轮交付三件事:
### 1. ä¸€é”®æ‰“包脚本 `package-deploy.py`(新增)
一条命令完成:前端构建 â†’ æŠŠ `dist` å†…嵌进后端 jar(拷进 `src/main/resources/static`)→ `mvn clean package` â†’ æ¸…空 static(保持仓库干净)→ ç»„装部署目录 â†’ åŽ‹ç¼© zip。
产物:
- ç›®å½• `deploy/traffic-audit-deploy-YYYYMMDD/`
- åŽ‹ç¼©åŒ… `traffic-audit-deploy-YYYYMMDD.zip`(本次 69.9 MB / 246 ä¸ªæ–‡ä»¶ï¼‰
部署目录结构(沿用 8 æœˆç‰ˆåŒ…习惯):
```
backend/  jar(含内嵌前端)+ docs/ + application.yml + start-backend.cmd/.sh
frontend/ dist/ + nginx.conf.example        ï¼ˆä»… nginx æ–¹æ¡ˆéœ€è¦ï¼‰
db/       init.sql + sql_city_bus.sql + sql_city_taxi.sql + sql_wyc.sql
README-部署说明.txt
```
### 2. ä¿®å¤ `JwtUtils`:`jwt.secret` éžæ³• Base64 å¯¼è‡´ç™»å½• 500(根因修复)
- çŽ°è±¡ï¼ˆæœ¬æ¬¡çƒŸæµ‹å®žæµ‹ï¼‰ï¼šæŠŠå¤–ç½® `application.yml` çš„ `jwt.secret` å¡«æˆä¸­æ–‡ä¸²åŽï¼Œç™»å½•接口返回 `500 {"code":500,"message":"20018"}`。
- æ ¹å› ï¼šjjwt 0.9.1 é»˜è®¤æŠŠ secret å½“ **Base64** è§£ç ï¼Œéžæ³•字符会在 `DatatypeConverter.guessLength` æŠ›
  `ArrayIndexOutOfBoundsException`,被 `GlobalExceptionHandler` å…œæˆ 500。部署同事第一次改配置很容易踩。
- å¤„置:`JwtUtils` æ–°å¢ž `secretKeyBytes()`——能按 Base64 è§£ç å°±ç”¨è§£ç ç»“果(与历史 token å…¼å®¹ï¼‰ï¼Œå¦åˆ™æŒ‰ UTF-8 å­—节处理并打 WARN。
### 3. æ›´æ­£ã€Šæ‰“包部署说明》(`docs/打包部署说明.md`)
旧版(2026-08-27)有三处与现状不符,本次一并改正:
| æ—§è¯´æ˜Ž | å®žé™…情况 | å¤„理 |
|--------|----------|------|
| ã€ŒRedis åŽç«¯å¯åŠ¨å¿…éœ€ã€ | pom æ—  redis ä¾èµ–,代码也未使用 | æ”¹ä¸ºã€Œä¸éœ€è¦ Redis」,并去掉外置配置里的 redis æ®µ |
| ã€Œjar å·²å†…置前端静态托管(file:./web/)」 | jar å†…无任何静态文件,也没有 `static-locations` é…ç½®ï¼›æ—§åŒ…实际靠 nginx | æ”¹ä¸ºã€Œæ‰“包时把 dist å†…嵌进 jar」,访问 8090 å³å¯ï¼Œæ— éœ€ nginx |
| æ•°æ®åº“只提 3 ä¸ªè„šæœ¬ | çŽ°ç½‘è¿˜æœ‰ `wyc_total_monthly` / `wyc_order_monthly` ä¸¤å¼ è¡¨ | è¡¥ `db/sql_wyc.sql`,并补充 `summary.template-dir` ç­‰é…ç½®ä¸Žæ—¶åº |
## äºŒã€æµ‹è¯•环境
- å¼€å‘机:后端 8090(`start-dev.ps1`,重启后 PID å˜åŒ–)、前端 8080、MySQL 127.0.0.1:3308。
- éƒ¨ç½²åŒ…验证:把 `deploy/traffic-audit-deploy-20260913/backend` æ‹·åˆ° `_deploytest/`,用 **8091** ç«¯å£ç‹¬ç«‹å¯åЍ
  ï¼ˆæ•°æ®åº“用命令行参数覆盖为 3308,模拟「外置 yml + å‘½ä»¤è¡Œè¦†ç›–」两种改配置方式)。
- æµ‹è¯•时间:2026-09-13;脚本:`package-deploy.py`、`_deploy_smoke.py`(结果 `_deploytest_result.txt`、`_deploytest_backend.log`)。
## ä¸‰ã€ç”¨ä¾‹è¡¨
### 3.1 æ­£å‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-1 | `python package-deploy.py --no-build` | äº§å‡º zip,且无报错 | `traffic-audit-deploy-20260913.zip` 69.9 MB / 246 æ–‡ä»¶ | é€šè¿‡ |
| TC-2 | æ£€æŸ¥ jar å†…容 | å«å†…嵌前端 | jar 55.5 MB,`BOOT-INF/classes/static/` ä¸‹ 42 ä¸ªæ–‡ä»¶ï¼Œ`static/index.html` å­˜åœ¨ | é€šè¿‡ |
| TC-3 | æ£€æŸ¥éƒ¨ç½²ç›®å½•结构 | backend/frontend/db/README é½å…¨ | backend(jar + docs + application.yml + 2 ä¸ªå¯åŠ¨è„šæœ¬ï¼‰ã€frontend/dist、db 4 ä¸ª sql、README | é€šè¿‡ |
| TC-4 | æ‰“包后源码目录 | `src/main/resources/static` åº”被清空 | æ‰“包结束自动清空(仓库无污染) | é€šè¿‡ |
| TC-5 | ä»Žéƒ¨ç½²åŒ…独立启动(8091) | å¯åŠ¨æˆåŠŸ | æ—¥å¿—出现 `Started TrafficAuditApplication`、`Tomcat started on port(s): 8091` | é€šè¿‡ |
| TC-6 | `GET /` | 200 ä¸”返回页面 | 200,1828 å­—节,含 `<div id="app">` | é€šè¿‡ |
| TC-7 | `GET /js/app.*.js` | 200(静态资源可访问) | 200,6253 å­—节 | é€šè¿‡ |
| TC-8 | `POST /api/auth/login`(admin/123456) | 200 ä¸”返回 token | 200,`{"code":200,"message":"success","data":{...,"token":"eyJ..."}}` | é€šè¿‡ |
| TC-9 | å¸¦ token å¯¼å‡ºã€Šé“路运输量汇总表》2026-08 | 200 ä¸”是合法 xlsx | 200,950,783 å­—节,头部 `PK`(与开发机导出件同尺寸) | é€šè¿‡ |
### 3.2 è´Ÿå‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-N1 | ä¿®å¤å‰ï¼š`--jwt.secret=这是一个中文密钥` | æš´éœ²é—®é¢˜ | ç™»å½• 500,日志 `ArrayIndexOutOfBoundsException: 20018 at JwtUtils.generateToken` | å·²å¤çŽ°ï¼ˆé—®é¢˜æˆç«‹ï¼‰ |
| TC-N2 | ä¿®å¤åŽï¼šåŒæ ·ä¼ ä¸­æ–‡ secret | ä¸å› å¯†é’¥æ ¼å¼å´©ï¼Œç™»å½•仍可用 | ç™»å½• 200 æ‹¿åˆ° token;导出同样 200 / 950,783 å­—节;日志出现「jwt.secret ä¸æ˜¯åˆæ³•çš„ Base64 å­—符串…」WARN | é€šè¿‡ |
| TC-N3 | å¤–ç½® application.yml ä¿ç•™å ä½å¯†ç ï¼Œç”¨å‘½ä»¤è¡Œ `--spring.datasource.password` è¦†ç›– | å‘½ä»¤è¡Œè¦†ç›–生效 | å¯åŠ¨æˆåŠŸå¹¶æ­£å¸¸æŸ¥åº“ï¼ˆç™»å½• 200) | é€šè¿‡ |
| TC-N4 | æ‰“包脚本重复执行(旧 staging ç›®å½•含只读文件) | èƒ½æ¸…理并重建 | é¦–轮报 `PermissionError WinError 5`,加 `rmtree_force`(先清只读属性)后正常 | å·²ä¿®å¤ |
### 3.3 å›žå½’用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-R1 | å¼€å‘机重启后端(载入 JwtUtils æ”¹åŠ¨ï¼‰ | åŠŸèƒ½ä¸å›žå½’ | ç™»å½• 200;导出 2026-08 â†’ HTTP 200 / 950,783 å­—节(与部署包产物、9-13 å½’档件同尺寸) | é€šè¿‡ |
| TC-R2 | å‰ç«¯ç”Ÿäº§æž„建 | æž„建成功 | `npm run build` æˆåŠŸï¼Œ`dist/index.html` + css/js/fonts é½å…¨ | é€šè¿‡ |
## å››ã€å‘现的问题与处置
1. **`jwt.secret` éžæ³• Base64 â†’ ç™»å½• 500**(烟测抓到):已按根因修复(Base64 ä¼˜å…ˆã€å¦åˆ™ UTF-8 å…œåº• + WARN),并在打包配置里给出可直接使用的 32 ä½åå…­è¿›åˆ¶ç¤ºä¾‹å€¼ã€åœ¨è¯´æ˜Žæ–‡æ¡£é‡Œå†™æ˜Žå–值规则。
2. **打包脚本清理旧目录被只读文件挡住**:源 `docs` é‡Œæœ‰åªè¯» xlsx,`copy2` ä¼šæŠŠåªè¯»å±žæ€§å¸¦è¿‡åŽ»ï¼Œå¯¼è‡´ Windows ä¸‹ `rmtree` æŠ¥ã€Œæ‹’绝访问」→ æ–°å¢ž `rmtree_force()` å…ˆ `chmod` å†åˆ ã€‚
3. **旧《打包部署说明》三处不实**(Redis å¿…需 / jar å·²å†…置前端 / ç¼º sql_wyc.sql):已全部更正,并补上部署后验证清单与常见问题。
4. è¯´æ˜Žï¼šçƒŸæµ‹è„šæœ¬é‡Œã€Œæ‹¿åˆ° token」那一项打印为 False,是脚本把响应体截断到 120 å­—符后再判断字符串导致(截断后看不到 `"token"`),原始响应与后续带 token çš„导出请求均成功(HTTP 200),程序本身无问题。
## äº”、结论
- æ‰“包脚本一键可产出可直接部署的 zip;部署包在**脱离源码目录**、仅靠 jar + docs + å¤–置配置的前提下,独立启动、页面/静态资源/登录/导出全部通过,导出产物与开发机一致(950,783 å­—节)。
- `jwt.secret` çš„部署陷阱已根因修复,部署同事即使填了不规范的值也不会再出现「登录 500」而无从下手。
- éƒ¨ç½²è¯´æ˜Žå·²ä¸Žå½“前代码对齐(不需要 Redis、不需要 nginx、4 ä¸ªå»ºè¡¨è„šæœ¬ã€å¤–ç½®é…ç½®é¡¹é½å…¨ï¼‰ã€‚
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-13.md
New file
@@ -0,0 +1,117 @@
# å·¥ä½œæ—¥ç»“_2026-09-13
> è¡¥è®°ï¼š2026-09-12 æ— ä»£ç æ”¹åŠ¨ã€æ— æäº¤ï¼ˆ`git log --since=2026-09-12` ä¸ºç©ºï¼‰ï¼Œæ•…未单独留档。
## ä¸€ã€ä»Šå¤©åšäº†ä»€ä¹ˆ
### 1. æ±‡æ€»å¤§è¡¨ã€Œå››é¡µé™æ€ã€é—®é¢˜å®šä½ä¸Žæ–¹æ¡ˆç¡®è®¤
- ç”¨æˆ·åé¦ˆï¼šå¯¼å‡º 2026-08 æ±‡æ€»å¤§è¡¨åŽï¼Œ`城市汇总`、`城市分市州`、`中口径分析`、`道路运输周转量` å››é¡µæ ‡é¢˜ä»å†™ã€Œ1-7月/7月」、数值也是 7 æœˆé™æ€å€¼ã€‚
- ç»™å‡ºä¸‰å¥—方案(A æ²¿ç”¨æ¯ç‰ˆ 2025 ç¼“存做同比基数的快路子 / B ç­‰ 2025 æ•°æ®å…¥åº“ / C åªåšå½“月块),用户选定 **方案 A**。
### 2. å£å¾„反推(先证后写)
- æŠŠæ¯ç‰ˆå°è´¦é¡µï¼ˆ`公交`/`出租`/`网约车`/`轨道、轮渡`/`班线包车`/` è´§è¿`)逐格聚合,反推四页口径,与四页成品**逐格 0 å·®å¼‚**后才动代码。
- ç¡®è®¤å£å¾„:城市客运量 = å…¬äº¤åŸŽå¸‚内 + å‡ºç§ŸåŸŽå¸‚内 + ç½‘约车城市内 + è½¨é“ + è½®æ¸¡ï¼›ä¸­å£å¾„ = ç­çº¿åŒ…车 + (总量 âˆ’ åŸŽå¸‚内)×(公交/出租/网约车);道路货运周转量 = è§„上 + è§„下;同比 = å½“期 Ã· æ¯ç‰ˆå°è´¦é¡µ 2025 åŒæœŸåˆ— âˆ’ 1(库中无 2025 åŸŽå¸‚客运数据,故取母版缓存)。
### 3. ä»£ç å®žçŽ°ï¼ˆå”¯ä¸€æ”¹åŠ¨æ–‡ä»¶ï¼‰
- `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java`(+494 âˆ’2):
  - æ–°å¢ž `refreshSummaryCumulativeSheets(wb, year, month)`,插在 `refreshSummaryRankSheets` ä¹‹åŽã€`applySummaryReferenceLayout` ä¹‹å‰ï¼›
  - å°è´¦å–æ•° `smReadLedgerMetrics` å…¨éƒ¨**按行标签识别**(不写死行号),19 ç»´æŒ‡æ ‡ä½ï¼›2026 é€æœˆåˆ— C…Q、2025 åŒæœŸåˆ— V…AJ;
  - å†™å€¼ `smSetVal` é‡ç›®æ ‡æ ¼æ˜¯å…¬å¼**直接跳过**(保留母版公式,打开重算)、0 ä¸”原为空格不新增;`smSetTitle` å…ˆ `setBlank()` å†å†™ï¼ˆæ¯ç‰ˆæ ‡é¢˜æ˜¯ inlineStr,POI åªå†™ `<v>` ä¸æ›´æ–° `<is>` çš„坑)。
- å››é¡µéšæŠ¥è¡¨æœŸå†™å…¥ **1056 æ ¼**。
### 4. ç¼–译、重启与实测
- `mvn -o -q compile` é€šè¿‡ï¼›`start-dev.ps1` é‡å¯åŽç«¯ï¼ˆPID 27840)与前端(PID 23416)均就绪。
- å¯¼å‡º 2026-08:HTTP 200 / 950,783 å­—节 / 36.8 ç§’;日志「刷新四页…数值格数=1056、刷新排名页数值格数=711、缺数提示=[]」,最近 4MB æ—¥å¿—æ—  ERROR/WARN。
- å½’档件:`docs/生成汇总大表/输出/生成_道路运输量汇总表_2026-08_四页数据驱动_2026-09-13.xlsx`。
### 5. ç‹¬ç«‹å¤æ ¸ï¼ˆä»Žç”Ÿæˆä»¶è‡ªèº«å°è´¦é¡µé‡æ–°èšåˆï¼‰
- æ–°å†™ `_v5_four.py`:覆盖标题 10 æ ¼ + åŸŽå¸‚汇总累计块 28 æ ¼ + å½“月块 28 æ ¼ + åŸŽå¸‚分市州 408 æ ¼ + ä¸­å£å¾„ 10 æ ¼ + é“路运输周转量 102 æ ¼ = **586 æ ¼ï¼Œå·®å¼‚ 0 æ ¼**。
- å¦ç”¨ `_verify_four.py` å¤æ ¸ 13:06 å½’档件(含与母版 1-7 æœˆé™æ€å€¼å¯¹ç…§ï¼‰ï¼ŒåŒæ · **0 å·®å¼‚**。
- å…¨çœè¡Œè‡ªæ´½ï¼šG4/L4/Q4/W4/X4/Y4 = Î£17 å¸‚州(差 â‰¤ 6e-11)。
### 6. æ–‡æ¡£
- ã€ŠåŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表四页数据驱动_2026-09-13.md》写入 `docs/功能测试报告/`。
- æœ¬ã€Šå·¥ä½œæ—¥ç»“》。
### 7. æäº¤ä¸Žå‘布
- æäº¤ `ea7cc07`(`ReportExportService.java` + ã€ŠåŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表四页数据驱动_2026-09-13.md》 + æœ¬ã€Šå·¥ä½œæ—¥ç»“》,3 æ–‡ä»¶ +689 âˆ’2)已推送双仓库:`git push gitea main`(`09e612a..ea7cc07`)、`git push origin main:master`(`09e612a..ea7cc07`),并 `git branch -f master main`。
- æŽ¨é€å‰ `git fetch gitea` / `git fetch origin` å‡æ— æ–°æäº¤ï¼ˆ`rev-list --left-right --count` = `0 0`),未覆盖同事代码。
- å‘布后复跑 2026-08 å¯¼å‡ºï¼ˆHTTP 200 / 950,783 å­—节 / 29.2 ç§’),对发布后产物 `_t4_2026-08.xlsx` å†åš 586 æ ¼ç‹¬ç«‹å¤æ ¸ = å·®å¼‚ 0 æ ¼ï¼›ä¸Žå‘布前产物逐 zip æ¡ç›®æ¯”对,内部条目字节完全一致(MD5 å·®å¼‚仅来自 zip å¤´æ—¶é—´æˆ³ï¼‰ã€‚
### 8. æ‰“包部署支持(用户新需求:部署到另一台机器)
- æ–°å¢žä¸€é”®æ‰“包脚本 `package-deploy.py`:前端 `npm run build` â†’ `dist` å†…嵌进后端 jar(拷入 `resources/static`)→ `mvn clean package` â†’ æ¸…空 static â†’ ç»„装 `deploy/traffic-audit-deploy-YYYYMMDD/` â†’ åŽ‹ç¼© `traffic-audit-deploy-YYYYMMDD.zip`(本次 69.9 MB / 246 æ–‡ä»¶ï¼Œjar 55.5 MB)。
- éƒ¨ç½²åŒ…结构:`backend/`(jar + docs + application.yml + start-backend.cmd/.sh)、`frontend/`(dist + nginx ç¤ºä¾‹ï¼‰ã€`db/`(init.sql + sql_city_bus/taxi/wyc.sql)、README。
- **修复部署陷阱**:`jwt.secret` å¡«æˆéžæ³• Base64(如中文)会让登录接口 500(jjwt 0.9.1 æŒ‰ Base64 è§£ç  secret)→ `JwtUtils` æ–°å¢ž `secretKeyBytes()` å…œåº•(Base64 ä¼˜å…ˆã€å¦åˆ™ UTF-8 å­—节 + WARN)。
- æ›´æ­£ `docs/打包部署说明.md` ä¸‰å¤„不实:① ä¸éœ€è¦ Redis(pom æ— ä¾èµ–、代码未用);② å‰ç«¯æ”¹ä¸ºå†…嵌进 jar(旧说明称「已内置 file:./web/」,实际 jar å†…无静态文件);③ è¡¥ `sql_wyc.sql` ä¸Ž `summary.template-dir`。
- ç‹¬ç«‹éªŒè¯ï¼šæŠŠéƒ¨ç½²åŒ… backend æ‹·åˆ° `_deploytest/` ç”¨ 8091 ç«¯å£ç‹¬ç«‹å¯åЍ â†’ å¯åŠ¨æˆåŠŸã€`GET /` 200 æœ‰ `#app`、静态 js 200、登录 200 æ‹¿åˆ° token、导出汇总大表 200 / 950,783 å­—节(与开发机产物同尺寸);另测中文 secret å…œåº•路径同样 200。
- å¼€å‘机后端已按新代码重启(旧 PID 27840 å·²åœï¼‰ï¼Œé‡å¯åŽç™»å½• 200、导出 950,783 å­—节,无回归。
- æŠ¥å‘Šï¼š`docs/功能测试报告/功能测试报告_部署包打包与独立启动_2026-09-13.md`。
### 9. æ•°æ®åº“搬迁支持(回答「能不能直接备份数据库过去」)
- ç»“论:可以,且比跑 4 ä¸ªå»ºè¡¨è„šæœ¬æ›´çœäº‹ã€‚å·²åœ¨ã€Šæ‰“åŒ…éƒ¨ç½²è¯´æ˜Žã€‹é‡Œè¡¥ã€Œè·¯çº¿ A ç©ºåº“ 4 è„šæœ¬ / è·¯çº¿ B æ•´å¥—备份还原」二选一说明,并标注 **init.sql æ˜¯ç ´åæ€§çš„(基础表都带 DROP TABLE)**,已有数据的库上不能重复执行。
- ç”Ÿæˆå¤‡ä»½ `traffic_audit_full_20260913.sql`(45.5 MB,mysqldump --databases,含建库语句与全部 29 å¼ è¡¨æ•°æ®ï¼‰ã€‚
- è¿˜åŽŸéªŒè¯ï¼šæŠŠå¤‡ä»½é‡Œçš„åº“åæ”¹æˆä¸´æ—¶åº“ `traffic_audit_vfy` å¯¼å…¥ï¼Œé€è¡¨ `COUNT(*)` ä¸Žæºåº“比对 â€”— **29 å¼ è¡¨ 0 å·®å¼‚**,抽查 `wyc_order_monthly` 2026-08 è¡Œæ•°æ­£ç¡®ï¼ŒéªŒè¯åŽåˆ é™¤ä¸´æ—¶åº“。
- é¡ºå¸¦ç¡®è®¤ï¼šå¦å¤– 3 ä¸ªè„šæœ¬ï¼ˆsql_city_bus/taxi/wyc)没有 `USE` è¯­å¥ï¼Œå¿…é¡» `-D traffic_audit` æ‰§è¡Œï¼›init.sql è‡ªå¸¦å»ºåº“语句可独立执行。
### 10. å¤‡ä»½è¿˜åŽŸçš„ç‰ˆæœ¬æ ¸æŸ¥ä¸Žå¼ºéªŒè¯ï¼ˆå¤æŸ¥å‘çŽ°çš„é—®é¢˜ï¼‰
- å¤æŸ¥å‘现两个此前表述不准的点:① æœ¬æœº **MySQL æœåŠ¡ç«¯æ˜¯ 8.0.29**(`D:\mysql\mysql-5.7.41-winx64` åªæ˜¯ 5.7 å®¢æˆ·ç«¯å·¥å…·ï¼‰ï¼Œæ˜¨æ™šæŒ‰ã€Œæœ¬æœºæ˜¯ 5.7」写的说明不准确;② 29 å¼ è¡¨çš„æŽ’序规则统一为 `utf8mb4_0900_ai_ci`,**目标机必须装 MySQL 8.0,5.7 ä¼šå¯¼å…¥å¤±è´¥**。
- å› æ­¤æ”¹ç”¨ä¸ŽæœåŠ¡ç«¯åŒç‰ˆæœ¬çš„ `C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqldump.exe` é‡æ–°å¯¼å‡ºå¤‡ä»½ `traffic_audit_full_20260913.sql`(45.5 MB)。
- å¼ºéªŒè¯ï¼ˆ8.0 å®¢æˆ·ç«¯ï¼‰ï¼šå¤‡ä»½æ”¹åº“名导入临时库 `traffic_audit_vfy` â†’ è¡¨æ•°é‡ 29=29、表名一致、**逐表 `SHOW CREATE TABLE` ç»“构差异 0**、**逐表 `COUNT(*)` è¡Œæ•°å·®å¼‚ 0**、抽查 `wyc_order_monthly` 2026-08 `order_sum` ä¸€è‡´ â†’ éªŒè¯åŽåˆ é™¤ä¸´æ—¶åº“。
- éƒ¨ç½²è¯´æ˜Žå·²åŒæ­¥æ›´æ­£ï¼ˆçŽ¯å¢ƒè¦æ±‚è¡¨ã€è·¯çº¿ B å‘½ä»¤ä¸Žç‰ˆæœ¬è¯´æ˜Žã€`DROP TABLE` è¦†ç›–警告)。
### 11. æŸ¥è¯ Navicat .nb3 å¤‡ä»½æ ¼å¼ï¼ˆå›žç­”「不是可以打包成一个文件直接还原吗」)
- ç”¨æˆ·è®°å¿†ä¸­çš„「一个文件直接还原备份」= Navicat ç§æœ‰æ ¼å¼ **`.nb3`**;本机已有 8/27 é‚£ä»½ï¼š`D:\文档\Navicat\MySQL\Servers\mysql80\traffic_audit\20260827152017.nb3`(**已过期**:27 å¼ è¡¨ã€ç¼ºç½‘约车两张、数据停在 8/27)。
- è§£æžå‡ºæ ¼å¼ï¼š`tar` å®¹å™¨ = æ¯å¼ è¡¨ `<uuid>.meta.json.gz`(DDL/字段)+ `<uuid>.data.000NN.sql.gz`(gz æ•°æ®åˆ†ç‰‡ï¼‰+ æœ«å°¾æœªåŽ‹ç¼© `meta.json`(DatabaseType/Schema/Objects[UUID,Type,Name,Rows,Metadata{Filename,Checksum}]);`Checksum` ç»å®žæµ‹å°±æ˜¯è¯¥ meta æ–‡ä»¶çš„ **SHA1(大写)**。
- ç»“论与建议:`.nb3` åªèƒ½ç”± Navicat è‡ªèº«ç”Ÿæˆã€ä¹Ÿåªèƒ½ç”¨ã€Œè¿˜åŽŸå¤‡ä»½ã€è¿˜åŽŸï¼Œ**用 .sql ç‚¹ã€Œè¿˜åŽŸå¤‡ä»½ã€å¿…ç„¶å¤±è´¥**;我无法在命令行侧可靠合成并验证 Navicat æ˜¯å¦æŽ¥å—,所以不提供手工拼的 .nb3,改为在文档里写清「本机 Navicat ç‚¹ä¸€æ¬¡å¤‡ä»½ â†’ æ‹·åˆ°ç›®æ ‡æœºåŒè·¯å¾„ â†’ è¿˜åŽŸå¤‡ä»½ã€çš„å‚»ç“œæ­¥éª¤ã€‚
- `docs/打包部署说明.md` 5.1 èŠ‚æ–°å¢žã€Œè·¯çº¿ B2:Navicat å¤‡ä»½/还原(.nb3)」,并注明三种方式任选其一、不可叠加。
### 12. ã€Šè¿è·å¼‚常分析》——桌面《分析报告(出版)》重写(跨会话续作)
- ä¸Šä¸ªä¼šè¯ï¼ˆthread `01a08b6d-30de-7500-8c98-f6e87100e55c`)按用户 23:05 çš„要求,依据 `D:\05\_数据分类\运距异常\2026å¹´1-8月全省规上货物周转量差异率汇总表(4).xlsx`(`市州汇总`/`企业明细副本`)与 `D:\桌面\副本1-6月货运量测算数据各市州.xlsx`(`Sheet1` åˆ†å¸‚州规上货运量、`Sheet2` é«˜é€Ÿè½¦è´§æ€»é‡/总重里程/运距)重写 `D:\桌面\分析报告(出版).docx`,产出 **`D:\桌面\分析报告(出版)_新_20260913.docx`(43045 å­—节,6 é¡µï¼Œ5 å¼ è¡¨ï¼‰**,生成脚本 `_gen_report.py`;结构为 ä¸€ æ€»ä½“情况/二 ä¸Žé«˜é€Ÿå…¬è·¯æŒ‡æ ‡å¯¹æ¯”(表1 å•月、表2 ç´¯è®¡ï¼‰ï¼ä¸‰ åˆ†è¿è¾“方式(表3)/四 åˆ†å¸‚州(表4)/五 ä¼ä¸šä¸Žè¿åŠ›ï¼ˆè¡¨5)/六 ä¸‹ä¸€æ­¥å¯¹å¸‚州的要求与建议(6 æ¡ï¼‰ã€‚
- è¯¥ä¼šè¯å› ä¸Šä¸‹æ–‡è¶…限中断(requested 1,189,201 > 1,048,576 tokens),本会话续作;该会话结束前已渲染校验 `_render_check\rp01~rp06.png`。
**事故与恢复(重要)**
- æœ¬ä¼šè¯ä¸­ä¸€æ¡å‚数写反的命令把 `D:\桌面\分析报告(出版)_新_20260913.docx` è¦†ç›–成**纯文本**(10508 å­—节,`zipfile.is_zipfile=False`),Word æ‰“开即为无排版的文字流、表格被拆成一行一词——这就是用户反馈「格式全乱、内容差太多」的直接原因。
- å¤„置:误覆盖件已备份到 `_误覆盖备份_分析报告_新_20260913_纯文本.txt`;用 `D:\render_src.docx`(23:10:07 åŽŸæ ·å‰¯æœ¬ï¼‰è¿˜åŽŸæ¡Œé¢æ–‡ä»¶ï¼Œ**SHA256 ä¸€è‡´**(`1723594DC746BA4458F19381D893AC0A17060CCC5B20513A8A10AFB19B415373`),结构校验 zip æ­£å¸¸ã€5 è¡¨ 262 æ®µã€‚已核对:备份纯文本内容与还原件内容一致,**无内容丢失**。
**已查清的残留格式缺陷(待修)**
- `_gen_report.py` çš„ `borders()` æŠŠ `w:tblBorders` è¿½åŠ åœ¨ `w:tblLook` **之后**(合法顺序应为 `tblW→jc→tblBorders→tblLayout→tblLook`),且 `tblW=0`(type=auto)、`gridCol` 6 åˆ—均分 1568,列宽与内容不匹配。
- é¡µé¢å°ºå¯¸ï¼šè¿˜åŽŸä»¶ä¸º 12240×15840(Letter)+1417 twips é¡µè¾¹è·ï¼Œç”¨æˆ·åº•稿《分析报告(出版).docx》为 A4(11906×16838)+1800 twips,待统一。
- å·²èµ·è‰ä¿®å¤è„šæœ¬ `_gen_report_v3.py`(自还原件读回内容 â†’ é‡æŽ’ `tblPr` é¡ºåºã€`tblLayout=fixed`、`autofit=False`、按内容分配列宽、A4/25.4mm é¡µè¾¹è·ã€è¡¨å†…行距改单倍),**本次未执行、未产出文件**,明日继续。
## äºŒã€æµ‹è¯•结果摘要(详见《功能测试报告》)
| é¡¹ | ç»“æžœ |
| --- | --- |
| å¯¼å‡º 2026-08 | HTTP 200 / 950,783 å­—节 / 36.8 ç§’;两次导出字节数一致(确定性) |
| å››é¡µæ ‡é¢˜ | åŸŽå¸‚汇总、城市分市州(4 å—)、中口径分析(2 å—)、道路运输周转量(2 å—)全部更新为 1-8月/8月 |
| é€æ ¼å¤æ ¸ | 586 æ ¼ï¼Œå·®å¼‚ 0 æ ¼ï¼ˆä»Žç”Ÿæˆä»¶è‡ªèº«å°è´¦é¡µç‹¬ç«‹é‡ç®—) |
| æœˆä»½å›žé€€ï¼ˆå›žå½’) | å¯¼å‡º 2026-07 æ—¶å››é¡µç²¾ç¡®å›žé€€æˆæ¯ç‰ˆ 1-7 æœˆæƒå¨å€¼ï¼šåŸŽå¸‚汇总 B3=237412.33、中口径 B3=27524.4906、周转量 G4=15222118.44638293、城市分市州 B6=135474.77989442425 â€”— ä¸Žæ¯ç‰ˆé€é¡¹ä¸€è‡´ |
| è´Ÿå‘ | æ¯ç‰ˆå…¬å¼æ ¼æœªè¢«è¦†ç›–(`城市分市州!B5`、`道路运输周转量!J4` ä»ä¸ºå…¬å¼ï¼‰ï¼›2025 æ— åº“数据时同比取母版缓存、不出现 NaN/Inf;空+0 ä¸æ–°å¢ž |
| å‰ç«¯ | `http://localhost:8080/` HTTP 200(首页正常) |
## ä¸‰ã€é—留问题与待办
1. **中口径排名页「同比增速」列仍是母版旧值**:库中只有 2026 å¹´æ•°æ®ã€æ²¡æœ‰ 2025 å¹´ä¸­å£å¾„定稿数据,算不出 8 æœˆåŒæ¯”;等 2025 æ•°æ®å…¥åº“后自动刷新。
2. **网约车 8 æœˆè®¢å•数仍是 7 æœˆå‰¯æœ¬**(已查证):`wyc_order_monthly` çš„ 2026-08 è¡Œ `created_at=2026-09-11 16:35:10`,对应 `import_batch` **#317**、文件名 `_t_wyc_aug.docx`(09-11 è‡ªæµ‹æ—¶ç”¨ 7 æœˆæŠ¥å‘Šæ”¹æ ‡é¢˜çš„临时副本);你真实的 `模板_网约车订单.docx` åªå¯¼å…¥è¿‡ 2026-07 æœŸï¼ˆ#314/#318/#319)。该行只有 4 ä¸ªå…¨çœ pin æ˜¯çœŸå®ž 8 æœˆæ•°æ®ï¼ˆæ¥è‡ªã€Šç½‘约车总数》#306),17 å¸‚州订单数与 7 æœˆå®Œå…¨ç›¸åŒã€‚**待用户重传真实 8 æœˆè¿è¡Œç›‘测报告(报表期 2026-08)覆盖。**
3. **根目录临时文件**(本次经用户同意删除自测副本 `_t_wyc_aug.docx`;其余 100+ ä¸ª `_*` ä¸´æ—¶æ–‡ä»¶æœªåŠ¨ï¼‰ï¼š`_*.py` / `_*.sql` / `_*.xlsx` / `_*.txt` / `_bak_*.java` ç­‰ 100+ ä¸ªæœªè·Ÿè¸ªæ–‡ä»¶ï¼ˆç”¨æˆ·æœªåŒæ„å‰ä¸æ¸…理、不提交)。
4. `docs/生成汇总大表/生成_道路运输量汇总表.xlsx`(版式参照件)与 `docs/生成汇总大表/输出/` ä¸‹çš„历史产物均为未跟踪文件,暂按惯例不纳入提交。
5. 2026-08 å¯¼å‡ºçš„「未找到同期备份母版」告警属无害提示,是否消除待用户定。
6. **《分析报告(出版)_新_20260913.docx》表格格式未修完**:`tblPr` é¡ºåºã€åˆ—宽、页边距待重排(脚本 `_gen_report_v3.py` å·²èµ·è‰æœªæ‰§è¡Œï¼‰ã€‚
7. æŠ¥å‘Šã€Œäº”、企业与运力情况」末尾注明「规上企业户数同比、自有车辆及吨位同比两项指标本次未取得数据」,但用户明确要「今年企业减少、运力下降」这一层,数据源疑似 `D:\05_数据分类\货运\8月\道路货物运输月度生产情况(2026)_2026-09-13 18_24_49.xls`(636 å®¶ï¼‰å¯¹æ¯” `D:\需要导入的数据\货运\2025年月报\道路货物运输月度生产情况(2025å¹´8月).xls`(664 å®¶ï¼‰â€”—**尚未由我独立核实,不得直接写入报告**。
8. æ³¨æ„ï¼šæŠ¥å‘ŠåŽŸæ–‡ã€Œè¿åŠ›åŒæ¯”å¢žé•¿ 1.92%」与用户「运力下降」的说法口径不同(前者为全省营运货车吨位,后者疑为规上企业自有车辆吨位),明日需先与用户确认口径再定稿。
## å››ã€æ˜Žå¤©è®¡åˆ’
1. ç”¨æˆ·æ ¸å¯¹/重传真实 8 æœˆç½‘约车运行监测报告后,重导 2026-08 å¹¶å¤æ ¸ç½‘约车页。
2. æŒ‰ç”¨æˆ·åé¦ˆç»§ç»­æ ¸å¯¹å››é¡µæˆå“ï¼ˆé‡ç‚¹ï¼šåŸŽå¸‚分市州增速列、道路运输周转量同期列口径是否符合业务预期)。
3. è·Ÿè¿› 2025 å¹´åŸŽå¸‚客运/中口径数据入库,入库后把四页同比从「母版缓存」切到「读库」。
4. è§†ç”¨æˆ·æ„æ„¿æ¸…理根目录临时文件。
5. **(优先)** å®Œæˆã€Šåˆ†æžæŠ¥å‘Šï¼ˆå‡ºç‰ˆï¼‰_新_20260913.docx》的表格格式修复:执行/修订 `_gen_report_v3.py`,重排 5 å¼ è¡¨çš„ `tblPr` é¡ºåºã€`tblLayout=fixed`、按内容分配列宽、统一 A4 é¡µé¢ï¼Œäº§å‡ºåŽ**自查渲染**(Word→PDF→PNG,图片只用于自查不发给用户),确认表格不越界、打开不报修复提示。
6. å°±ã€Œä¼ä¸šä¸Žè¿åŠ›ã€ä¸¤é¡¹æŒ‡æ ‡å£å¾„ä¸Žç”¨æˆ·ç¡®è®¤ï¼ˆå…¨çœè¥è¿è´§è½¦å¨ä½ +1.92% vs è§„上企业自有车辆吨位下降),确认后再补数、补分析。
7. æŠ¥å‘Šå®šç¨¿åŽæŒ‰è§„范补《功能测试报告》(docs/功能测试报告/),再按 git åŒä»“库流程提交推送。
docs/´ò°ü²¿Êð˵Ã÷.md
@@ -1,6 +1,6 @@
# äº¤é€šç»Ÿè®¡æŠ¥è¡¨å®¡æ ¸ç³»ç»Ÿ â€” æ‰“包与部署说明
> é€‚用:合并后 main åˆ†æ”¯ï¼ˆ2026-08-27 èµ·ï¼‰ã€‚后端 Spring Boot 2.7(Java 8),前端 Vue 2 + Element UI。
> é€‚用:main åˆ†æ”¯ï¼ˆæœ€æ–°æ›´æ–° 2026-09-13)。后端 Spring Boot 2.7(Java 8),前端 Vue 2 + Element UI。
> æœ¬æ–‡æ¡£é¢å‘开发与部署同事,命令均在 Windows ä¸‹éªŒè¯ã€‚
## ä¸€ã€é¡¹ç›®ç»“æž„
@@ -9,156 +9,213 @@
|------|------|
| `traffic-audit-server` | åŽç«¯ï¼ˆSpring Boot,Java 8,端口 8090,接口全部在 `/api/**`) |
| `traffic-audit-web` | å‰ç«¯ï¼ˆVue 2 + Element UI,开发端口 8080,`/api` ä»£ç†åˆ° 8090) |
| `docs` | è¿è¡Œæ•°æ®ç›®å½•(报表 Word/Excel æ¨¡æ¿ + ç›®å½•导入数据),**发布必须带上** |
| `docs/init.sql` | æ•°æ®åº“初始化脚本(建库建表 + åˆå§‹è´¦å·ï¼‰ |
| `docs` | è¿è¡Œæ•°æ®ç›®å½•(报表模板、汇总大表母版等),**发布必须带上** |
| `package-deploy.py` | ä¸€é”®æ‰“包脚本(前端构建 â†’ å†…嵌进 jar â†’ ç»„装部署包 â†’ æ‰“ zip) |
## äºŒã€çŽ¯å¢ƒè¦æ±‚
| ç»„ä»¶ | ç‰ˆæœ¬ | ç”¨é€” |
| ç»„ä»¶ | ç‰ˆæœ¬ | è¯´æ˜Ž |
|------|------|------|
| JDK | 1.8 | åŽç«¯ç¼–译与运行 |
| Maven | 3.x | åŽç«¯æ‰“包 |
| Node.js | 14+ | å‰ç«¯æ‰“包(vue-cli 4.5) |
| MySQL | 8.0 | æ•°æ®åº“(库名 `traffic_audit`) |
| JDK | 1.8 | éƒ¨ç½²æœºå¿…须;后端编译/运行 |
| MySQL | **8.0** | éƒ¨ç½²æœºå¿…须,库名 `traffic_audit`;现网/开发机为 8.0.29,表排序规则是 `utf8mb4_0900_ai_ci`,**目标机装 5.7 ä¼šå¯¼å…¥å¤±è´¥** |
| Maven 3.x / Node.js 14+ | â€” | **只有「在部署机上打包」才需要**;用一键脚本产出的包部署则不需要 |
| Redis | **不需要** | ä»£ç æœªä½¿ç”¨ Redis(旧说明中的 Redis é…ç½®æ˜¯é—留项,可忽略) |
| nginx / IIS | **不需要** | å‰ç«¯å·²å†…嵌进 jar,访问 8090 å³å¯ï¼›è¦å‰åŽç«¯åˆ†ç¦»æ—¶æ‰æ˜¯å¤‡é€‰ |
## ä¸‰ã€æ•°æ®åº“初始化
先执行建库建表脚本,再按需导入城市客运数据:
```bash
mysql --default-character-set=utf8mb4 -h <主机> -P 3306 -u root -p < docs/init.sql
mysql --default-character-set=utf8mb4 -h <主机> -P 3306 -u root -p traffic_audit < docs/sql_city_bus.sql
mysql --default-character-set=utf8mb4 -h <主机> -P 3306 -u root -p traffic_audit < docs/sql_city_taxi.sql
```
- `init.sql` ä¼šé‡å»º `traffic_audit` åº“并预置初始账号:`admin / admin123`(生产环境请登录后改密码)
- `sql_city_bus.sql`、`sql_city_taxi.sql` ä¸ºåŸŽå¸‚客运(公交/出租)示例数据,按需导入
## å››ã€æ‰“包
### 4.1 åŽç«¯ï¼ˆé¡¹ç›®æ ¹ç›®å½•执行)
## ä¸‰ã€æ‰“包(在开发机上执行,推荐一键脚本)
```powershell
cd D:\文档\ChatGPT\traffic-audit
python package-deploy.py            # å…¨æµç¨‹ï¼šå‰ç«¯æž„建 + åŽç«¯æ‰“包 + ç»„装 + åŽ‹ç¼©
python package-deploy.py --no-build # è·³è¿‡å‰ç«¯æž„建(dist å·²æ˜¯æœ€æ–°æ—¶æ›´å¿«ï¼‰
```
脚本做了什么:
1. `traffic-audit-web` ä¸‹ `npm run build` â†’ `dist`;
2. æŠŠ `dist` å¤åˆ¶è¿› `traffic-audit-server/src/main/resources/static`,使 **jar è‡ªå¸¦å‰ç«¯**,再 `mvn clean package -DskipTests`(跑完自动清空 static,保持仓库干净);
3. ç»„装 `deploy/traffic-audit-deploy-YYYYMMDD/`;
4. åŽ‹ç¼©ä¸º `traffic-audit-deploy-YYYYMMDD.zip`(约 70 MB)。
手工等价命令(不想用脚本时):
```powershell
cd traffic-audit-web ; npm run build ; cd ..
robocopy traffic-audit-web\dist traffic-audit-server\src\main\resources\static /E
cmd /c "mvn -q clean package -DskipTests -f traffic-audit-server\pom.xml"
```
产物:`traffic-audit-server\target\traffic-audit-server-1.0.0-SNAPSHOT.jar`(可执行 fat jar,约 57MB)。
> æ³¨æ„ï¼šæ‰‹å·¥æ–¹å¼è·‘完记得删掉 `traffic-audit-server/src/main/resources/static` ä¸‹çš„内容,否则会污染源码目录。
### 4.2 å‰ç«¯ï¼ˆ`traffic-audit-web` ç›®å½•执行)
```powershell
cd traffic-audit-web
npm run build
```
产物:`traffic-audit-web\dist\`(`index.html` + `css/js/fonts`,纯静态文件)。
## äº”、部署目录结构(推荐)
## å››ã€éƒ¨ç½²åŒ…内容
```
deploy\
├─ app.jar                    # åŽç«¯ jar(可改名)
├─ docs\                      # å¿…须:模板与目录导入数据目录
│  â”œâ”€ åŸŽå¸‚客运\
│  â”œâ”€ å…¬è·¯æ—…客\
│  â”œâ”€ è´§è¿\
│  â”œâ”€ èƒ½è€—\
│  â”œâ”€ æŠ•资\
│  â”‚  â””─ æ¨¡æ¿\
│  â””─ (6月\ 7月\ ç­‰å¯¼å…¥æ•°æ®ç›®å½•,按需)
└─ web\                       # å‰ç«¯ dist çš„内容(由后端 8090 æ‰˜ç®¡ï¼‰
   â”œâ”€ index.html
   â”œâ”€ css\  js\  fonts\
traffic-audit-deploy-YYYYMMDD/
├─ backend/
│  â”œâ”€ traffic-audit-server-1.0.0-SNAPSHOT.jar   åŽç«¯ + å†…嵌前端(约 55 MB)
│  â”œâ”€ docs/                                     æ¨¡æ¿ä¸Žè¿è¡Œæ•°æ®ï¼ˆå¿…需,勿删)
│  â”œâ”€ application.yml                           å¤–部配置(改这里即可,无需重打包)
│  â””─ start-backend.cmd / start-backend.sh       å¯åŠ¨è„šæœ¬
├─ frontend/
│  â”œâ”€ dist/                                     å‰ç«¯äº§ç‰©ï¼ˆä»… nginx æ–¹æ¡ˆéœ€è¦ï¼‰
│  â””─ nginx.conf.example                         nginx é…ç½®ç¤ºä¾‹
├─ db/
│  â”œâ”€ init.sql                                  å»ºåº“建表 + é¢„置账号(必须执行)
│  â”œâ”€ sql_city_bus.sql                          åŸŽå¸‚客运-公交月度表(必须执行)
│  â”œâ”€ sql_city_taxi.sql                         åŸŽå¸‚客运-出租月度表/授权表(必须执行)
│  â””─ sql_wyc.sql                               ç½‘约车全省总量/订单分市州表(必须执行)
└─ README-部署说明.txt
```
关键点:
## äº”、目标机器部署步骤
- **`docs` å¿…须放在 jar åŒçº§**。后端运行时按启动目录向上查找 `docs`(报表导出的 Word æ¨¡æ¿ã€å¯¼å…¥é¡µã€Œä¸‹è½½æ¨¡æ¿ã€ã€ç›®å½•导入数据都从这里读),找不到会报「未找到数据目录 docs」或「未找到模板文件 xxx」。
- é¦–次部署直接把项目里的整个 `docs` æ‹·è¿‡åŽ»æœ€ç¨³å¦¥ï¼›åŽç»­å¯ç²¾ç®€ï¼ˆåªç•™æ¨¡æ¿å­ç›®å½•ï¼‰ï¼Œè¯¦è§ã€Œå¸¸è§é—®é¢˜ã€ã€‚
- `application.yml` æ‰“包在 jar å†…,已内置前端静态托管(`file:./web/`);部署机需要改配置时,可在 jar åŒçº§æ”¾ä¸€ä¸ªå¤–ç½® `application.yml`(外置优先级高于内置,注意需包含完整配置),或改完再打包。
- å¯åŠ¨åŽæµè§ˆå™¨ç›´æŽ¥è®¿é—® `http://<服务器>:8090`,无需额外 Web æœåŠ¡å™¨ã€‚
### 5.1 å‡†å¤‡æ•°æ®åº“(两条路线二选一,**不要混着做**)
## å…­ã€å…³é”®é…ç½®ï¼ˆapplication.yml)
#### è·¯çº¿ A:全新空库(要跑 4 ä¸ªè„šæœ¬ï¼‰
`init.sql` è‡ªå¸¦ `CREATE DATABASE` ä¸Ž `USE traffic_audit`,可独立执行;另外 3 ä¸ªè„šæœ¬æ²¡æœ‰ `USE` è¯­å¥ï¼Œå¿…须用 `-D traffic_audit`(或 `mysql ... traffic_audit < æ–‡ä»¶`)指定库:
```bash
cd db
mysql --default-character-set=utf8mb4 -u root -p            < init.sql          # å»ºåº“ + 24 å¼ åŸºç¡€è¡¨ + é¢„置账号
mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_city_bus.sql
mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_city_taxi.sql
mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_wyc.sql
```
> âš ï¸ **`init.sql` æ˜¯ç ´åæ€§çš„**:里面的基础表都带 `DROP TABLE IF EXISTS`。**已有数据的库上千万不要再跑 init.sql**,否则表数据会被清空重建。
> 4 ä¸ªè„šæœ¬å…±è¦†ç›– 29 å¼ è¡¨ï¼ˆinit.sql 24 å¼  + åŸŽå¸‚客运 3 å¼  + ç½‘约车 2 å¼ ï¼‰ã€‚
#### è·¯çº¿ B:把现网/开发机的数据整套搬过去(推荐,不用跑脚本)
先确认两端 MySQL éƒ½æ˜¯ **8.0**(本库表排序规则为 `utf8mb4_0900_ai_ci`,5.7 ä¸è¯†åˆ«ä¼šå¯¼è‡´å¯¼å…¥å¤±è´¥ï¼‰ï¼Œå¹¶ä¸”**用与服务端同版本的 mysqldump**(不要用 5.7 çš„客户端去导 8.0 æœåŠ¡ç«¯ï¼‰ã€‚
在源机器(本机 MySQL ç«¯å£ç¤ºä¾‹ 3308,按实际改):
```bash
"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqldump.exe" ^
  --host=127.0.0.1 --port=3308 --user=root --password=你的密码 ^
  --default-character-set=utf8mb4 --single-transaction --routines --triggers --events ^
  --databases traffic_audit > traffic_audit_full_YYYYMMDD.sql
```
在目标机器:
```bash
mysql --default-character-set=utf8mb4 -u root -p < traffic_audit_full_YYYYMMDD.sql
```
- `--databases` ä¼šæŠŠ `CREATE DATABASE` + `USE` ä¸€èµ·å†™è¿›å¤‡ä»½ï¼Œæ‰€ä»¥ç›®æ ‡æœºå™¨**不用先建库、也不用跑那 4 ä¸ªè„šæœ¬**。
- å¤‡ä»½é‡ŒåŒ…含 `sys_user`,账号密码与源机器完全一致(本机是 `admin / 123456`),不会被重置成 `admin/admin123`。
- âš ï¸ å¤‡ä»½é‡Œæ¯å¼ è¡¨éƒ½å¸¦ `DROP TABLE IF EXISTS`(共 29 å¼ ï¼‰ï¼š**目标库要是已有数据,会被直接覆盖**。请还原到空库,或先自己备份目标库。
- æœ¬æ¬¡å·²ç”Ÿæˆå¹¶**验证过**备份:`traffic_audit_full_20260913.sql`(45.5 MB,8.0.29 å®¢æˆ·ç«¯å¯¼å‡ºï¼‰ã€‚
  éªŒè¯æ–¹å¼ï¼šæŠŠå¤‡ä»½åº“名改成临时库导入,逐表比对 `SHOW CREATE TABLE`(表结构)与 `COUNT(*)`(行数)——**29 å¼ è¡¨ç»“构与行数均 0 å·®å¼‚**,抽查 `wyc_order_monthly` 2026-08 æ•°æ®ä¸€è‡´ï¼ŒéªŒè¯åŽå·²åˆ é™¤ä¸´æ—¶åº“。
#### è·¯çº¿ B2:用 Navicat çš„「备份 / è¿˜åŽŸå¤‡ä»½ã€ï¼ˆ.nb3,最贴近日常习惯)
`.nb3` æ˜¯ Navicat ç§æœ‰å¤‡ä»½æ ¼å¼ï¼ˆtar å®¹å™¨ï¼šæ¯å¼ è¡¨ä¸€ä»½ meta.json + gz åŽ‹ç¼©çš„æ•°æ®åˆ†ç‰‡ + SHA1 æ ¡éªŒ + æ€» meta.json),
只能由 Navicat è‡ªå·±ç”Ÿæˆã€ä¹Ÿåªèƒ½åœ¨ Navicat é‡Œç”¨ã€Œè¿˜åŽŸå¤‡ä»½ã€è¿˜åŽŸâ€”â€”**用 `.sql` ç‚¹ã€Œè¿˜åŽŸå¤‡ä»½ã€ä¸€å®šå¤±è´¥**(那个入口只认 .nb3)。
在源机器(30 ç§’):
1. Navicat â†’ è¿žæŽ¥ `mysql80` â†’ é€‰ä¸­ `traffic_audit` â†’ å³é”® â†’「备份…」→ èµ·ä¸ªåå­— â†’ ç¡®å®šï¼Œç­‰å®ƒè·‘完;
2. ç”Ÿæˆçš„备份文件在 Navicat çš„ profile ç›®å½•:`D:\文档\Navicat\MySQL\Servers\mysql80\traffic_audit\<时间戳>.nb3`
   ï¼ˆæœ¬æœº 8/27 é‚£ä»½å°±åœ¨è¿™é‡Œï¼Œä½†å®ƒ**已过期**:只有 27 å¼ è¡¨ã€ç¼ºç½‘约车两张表、数据是 8/27 çš„,不要用)。
在目标机器:
3. å…ˆåœ¨ Navicat é‡Œå»ºä¸€ä¸ªè¿žåˆ°æœ¬æœº MySQL 8.0 çš„连接(**连接名建议与源机器一致,比如 `mysql80`**),并新建空库 `traffic_audit`(字符集 utf8mb4);
4. æŠŠ `.nb3` æ”¾åˆ°ç›®æ ‡æœºå™¨çš„对应目录 `<Navicat profile>\MySQL\Servers\<连接名>\traffic_audit\`(没有就照着建目录);
5. å³é”®è¯¥åº“ â†’「还原备份」→ åœ¨é€‰æ‹©åˆ—表里勾选这份备份 â†’ å¼€å§‹ï¼ˆåˆ—表里看不到就重启一次 Navicat)。
> ä¸‰ç§æ–¹å¼ï¼ˆè·¯çº¿ A ç©ºåº“ 4 è„šæœ¬ / è·¯çº¿ B å‘½ä»¤è¡Œè¿˜åŽŸ .sql / è·¯çº¿ B2 Navicat .nb3)**任选其一**即可,
> ä½†**不要叠加**:还原完备份后再跑 `init.sql` ä¼šæŠŠæ•°æ®æ¸…空重建。
> æ³¨æ„ï¼š`docs/` ç›®å½•(报表模板、汇总大表母版)**与数据库无关,无论哪条路线都必须一并拷贝**。
### 5.2 æ”¹é…ç½® `backend/application.yml`
| é…ç½®é¡¹ | è¯´æ˜Ž |
|--------|------|
| `spring.datasource.url/username/password` | æ•°æ®åº“连接(模板默认 `localhost:3305/root/root`,部署机**必须改**;生产建议用环境变量 `MYSQL_PASSWORD` æ³¨å…¥ï¼‰ |
| `deepseek.api-key` | AI åˆ†æžæŽ¥å£å¯†é’¥ï¼ˆçŽ¯å¢ƒå˜é‡ `DEEPSEEK_API_KEY`,不使用 AI åˆ†æžå¯ç•™ç©ºï¼‰ |
| `jwt.secret` | ç™»å½•令牌密钥(生产必须改,环境变量 `JWT_SECRET`) |
| `docs.template-root` | å¯¼å…¥æ¨¡æ¿ä¸‹è½½æ ¹ç›®å½•(默认 `docs`) |
| `city-passenger.template-dir` | åŸŽå¸‚客运报表模板目录(默认 `docs/城市客运`) |
| `passenger.template-dir` | å…¬è·¯æ—…客报表模板目录(默认 `docs/公路旅客`) |
| `freight.template-dir` | è´§è¿æŠ¥è¡¨æ¨¡æ¿ç›®å½•(默认 `docs/货运`) |
| `energy.template-dir` | èƒ½è€—报表模板目录(默认 `docs/能耗`) |
| `investment.template-dir` | æŠ•资报表模板目录(默认 `docs/投资/模板`) |
| `spring.datasource.url` | æ•°æ®åº“地址(出厂值 `127.0.0.1:3306/traffic_audit`,按实际改) |
| `spring.datasource.username/password` | æ•°æ®åº“账号密码(**必改**) |
| `jwt.secret` | ç™»å½•令牌密钥;**必须是 Base64 åˆæ³•字符且长度是 4 çš„倍数**(出厂值是一串 32 ä½åå…­è¿›åˆ¶ï¼‰ï¼›å¡«ä¸­æ–‡æˆ–非法字符会导致登录 500(程序已做兜底并打 WARN,但仍建议填规范值) |
| `deepseek.api-key` | AI åˆ†æžç”¨ï¼Œä¸ç”¨å¯ç•™ç©º |
| `server.port` | é»˜è®¤ 8090 |
> æ³¨æ„ï¼šä»“库只提交占位模板 `application.yml.example`,真实 `application.yml` ä¸å…¥åº“。
> å¤–ç½® `application.yml` ä¸Ž jar åŒçº§å³ç”Ÿæ•ˆï¼ˆä¼˜å…ˆçº§é«˜äºŽ jar å†…置配置),改完不用重新打包。
## ä¸ƒã€å¯åŠ¨åŽç«¯
### 5.3 å¯åŠ¨ï¼ˆå¿…é¡»åœ¨ `backend` ç›®å½•下)
```powershell
cd deploy
java -jar app.jar
# Windows:双击 start-backend.cmd;或
cd backend ; java -jar traffic-audit-server-1.0.0-SNAPSHOT.jar
```
- æˆåŠŸæ ‡å¿—ï¼šæ—¥å¿—å‡ºçŽ° `Started TrafficAuditApplication` å’Œ `Tomcat started on port(s): 8090`。
- åœ¨ `deploy` ç›®å½•下启动即可命中同级的 `docs`。
## å…«ã€å‰ç«¯éƒ¨ç½²ï¼ˆæŽ¨èï¼šSpring Boot ç›´æŽ¥æ‰˜ç®¡ï¼Œæ— éœ€ IIS/nginx)
- æŠŠå‰ç«¯æž„建产物 `dist` çš„内容放到 jar åŒçº§ç›®å½• `web\`(即 `web\index.html`、`web\js\`、`web\css\`、`web\fonts\`)。
- åŽç«¯å·²å†…置:`spring.web.resources.static-locations: file:./web/`(静态托管)+ å®‰å…¨é…ç½®æ”¾è¡Œé™æ€èµ„源,启动后直接访问 `http://<服务器>:8090` å³å¯ï¼Œé¡µé¢ä¸Ž `/api` åŒæºï¼Œ**不需要 IIS/nginx åä»£**。
- å‰ç«¯è·¯ç”±ä¸º hash æ¨¡å¼ï¼Œåˆ·æ–°/直达无 404 é—®é¢˜ã€‚
### å¤‡é€‰ï¼šnginx æ‰˜ç®¡ + /api åä»£ï¼ˆå¦‚需前后端分离)
```nginx
server {
    listen 80;
    server_name _;
    root D:/deploy/web;            # å‰ç«¯ dist ç›®å½•
    index index.html;
    location / {
        try_files $uri $uri/ /index.html;
    }
    location /api {
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    client_max_body_size 100m;     # ä¸Šä¼  Excel éœ€è¦ï¼Œä¸ŽåŽç«¯ multipart 100MB å¯¹é½
}
```bash
# Linux
cd backend && sh start-backend.sh
```
> è‹¥å‰ç«¯ç”± IIS æ‰˜ç®¡ï¼Œéœ€å®‰è£… URL Rewrite 2.0 + ARR 3.0 å¹¶å¯ç”¨ proxy,站点根目录放 `docs/iis-web.config.example`(重命名 web.config),规则示例见该文件。
成功标志:日志出现 `Started TrafficAuditApplication` ä¸Ž `Tomcat started on port(s): 8090`。
## ä¹ã€éƒ¨ç½²åŽéªŒè¯
### 5.4 è®¿é—®
1. æµè§ˆå™¨æ‰“å¼€ `http://<服务器>:8090`;
2. ç”¨ `admin / admin123` ç™»å½•(生产改密后使用新密码);
3. æ•°æ®å¯¼å…¥ï¼šä¸‹è½½æ¨¡æ¿ â†’ ä¸Šä¼  Excel(或目录导入);
4. æŠ¥è¡¨ï¼šæŸ¥è¯¢ â†’ é¢„览/导出(依赖 `docs` æ¨¡æ¿æ–‡ä»¶ï¼‰ï¼›
5. AI åˆ†æžï¼šéœ€é…ç½® `deepseek.api-key` åŽå¯ç”¨ã€‚
浏览器打开 `http://<部署机IP>:8090`(前端已内嵌,页面与接口同源,无需 nginx),账号 `admin / admin123`。
## åã€å¸¸è§é—®é¢˜
## å…­ã€å…³é”®é…ç½®ï¼ˆapplication.yml)与运行数据
| é…ç½®é¡¹ | é»˜è®¤å€¼ | è¯´æ˜Ž |
|--------|--------|------|
| `docs.template-root` | `docs` | å¯¼å…¥æ¨¡æ¿ä¸‹è½½æ ¹ç›®å½• |
| `city-passenger.template-dir` | `docs/城市客运` | åŸŽå¸‚客运(含《模板_网约车订单.docx》) |
| `passenger.template-dir` | `docs/公路旅客` | å…¬è·¯æ—…客 |
| `freight.template-dir` | `docs/货运` | è´§è¿ |
| `energy.template-dir` | `docs/能耗` | èƒ½è€— |
| `investment.template-dir` | `docs/投资/模板` | æŠ•资 |
| `summary.template-dir` | `docs/生成汇总大表` | **汇总大表母版与版式参照件**(导出报表依赖) |
`docs/生成汇总大表` è‡³å°‘要有:
- `生成_道路运输量汇总表.xlsx`(版式参照件,缺失会退回默认列宽);
- éœ€è¦å¯¼å‡ºçš„各月份母版 `YYYYå¹´M月道路运输量汇总表.xlsx`(缺哪个月,导那个月就会明确报错)。
## ä¸ƒã€å‰ç«¯éƒ¨ç½²ï¼ˆä¸¤ç§æ–¹å¼ï¼‰
1. **推荐:jar å†…嵌**——一键脚本已把 `dist` æ‰“è¿› jar,直接访问 `http://<IP>:8090` å³å¯ï¼Œæ— éœ€é¢å¤– Web æœåŠ¡å™¨ã€‚
2. å¤‡é€‰ï¼šnginx æ‰˜ç®¡ `frontend/dist` å¹¶æŠŠ `/api` åä»£åˆ° 8090(配置见 `frontend/nginx.conf.example`);上传 100MB å¤§æ–‡ä»¶æ—¶è®°å¾—设 `client_max_body_size 100m`。
前端路由为 hash æ¨¡å¼ï¼Œåˆ·æ–°/直达不会 404。
## å…«ã€éƒ¨ç½²åŽéªŒè¯æ¸…单
1. `http://<IP>:8090` æ‰“开登录页(页面不是空白);
2. `admin / admin123` èƒ½ç™»å½•,左上角显示「管理员」;
3. ã€Œæ•°æ®å¯¼å…¥ã€é¡µèƒ½ä¸‹è½½æ¨¡æ¿ã€èƒ½ä¸Šä¼  Excel;
4. ã€ŒæŠ¥è¡¨ã€é¡µæŸ¥è¯¢ + å¯¼å‡ºã€Šé“路运输量汇总表》,能下载到 xlsx;
5. æ‰“开导出的 xlsx,核对当月数值是否为最新报表期。
## ä¹ã€å¸¸è§é—®é¢˜
| çŽ°è±¡ | åŽŸå›  / å¤„理 |
|------|------------|
| å¯åŠ¨æŠ¥ã€Œæœªæ‰¾åˆ°æ•°æ®ç›®å½• docs」 | å½“前运行目录不对;把 `docs` æ”¾åˆ° jar åŒçº§å†å¯åЍ |
| å¯¼å‡ºæŠ¥ã€Œæœªæ‰¾åˆ°æ¨¡æ¿æ–‡ä»¶ xxx」 | `docs` å¯¹åº”子目录缺模板文件;首次部署拷整个 `docs` |
| é¡µé¢èƒ½å¼€ä½†æŽ¥å£ 404(验证码加载失败) | å‰ç«¯ç”± IIS/nginx æ‰˜ç®¡æ—¶æ²¡é… `/api` åä»£ï¼›æ”¹ç”¨ Spring Boot æ‰˜ç®¡ï¼ˆweb\ ç›®å½•)则无此问题 |
| æ•°æ®åº“连接失败 | æ£€æŸ¥ `application.yml` åœ°å€/账号/密码;先执行 `init.sql` |
| ä¸Šä¼ å¤§æ–‡ä»¶å¤±è´¥ | æ£€æŸ¥ nginx `client_max_body_size` ä¸ŽåŽç«¯ multipart é™åˆ¶ |
| ç«¯å£å ç”¨ | åŽç«¯ 8090、前端 80/8080 |
| å¯åŠ¨æŠ¥ã€Œæœªæ‰¾åˆ°æ•°æ®ç›®å½• docs」 | å¿…须在 `backend` ç›®å½•下启动(jar ä¸Ž docs åŒçº§ï¼‰ |
| å¯¼å‡ºæŠ¥ã€Œæœªæ‰¾åˆ°æ¨¡æ¿æ–‡ä»¶ xxx」 | `docs` å¯¹åº”子目录缺模板;从源机器拷贝 |
| å¯¼å‡ºæŠ¥ã€Œæœªæ‰¾åˆ°æ±‡æ€»è¡¨æ¯ç‰ˆã€ŠYYYYå¹´M月…》」 | `docs/生成汇总大表` ç¼ºè¯¥æœˆæ¯ç‰ˆ |
| ç™»å½•接口 500 | `jwt.secret` å¡«äº†éžæ³•字符(如中文);改成 32 ä½åå…­è¿›åˆ¶ä¸² |
| é¡µé¢èƒ½å¼€ä½†æŽ¥å£ 404 | èµ° nginx æ–¹æ¡ˆæ—¶æ²¡é… `/api` åä»£ï¼›æ”¹ç”¨ jar å†…嵌访问 8090 |
| æ•°æ®åº“连接失败 | æ£€æŸ¥ `application.yml`;确认 4 ä¸ª SQL è„šæœ¬å·²æ‰§è¡Œ |
| ç«¯å£å ç”¨ | æ”¹ `server.port`;前端开发端口 8080 |
## åä¸€ã€å¼€å‘环境日常启动
项目根目录运行一键脚本(自动拉起后端 8090 + å‰ç«¯ 8080):
## åã€å¼€å‘环境日常启动
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File start-dev.ps1
```
```
后端 8090(`mvn spring-boot:run`)+ å‰ç«¯ 8080(`npm run serve`),访问 `http://localhost:8080`。
## åä¸€ã€å˜æ›´è®°å½•
- **2026-09-13**:新增一键打包脚本 `package-deploy.py`;jar æ”¹ä¸ºå†…嵌前端(部署机不再需要 nginx);删除 Redis ç›¸å…³è¯´æ˜Žï¼ˆä»£ç æœªä½¿ç”¨ï¼‰ï¼›è¡¥ `sql_wyc.sql` ä¸Ž `summary.template-dir`;补 `jwt.secret` å–值规则;补部署后验证清单。
- 2026-08-27:首版(zip éƒ¨ç½²åŒ… + nginx æ–¹æ¡ˆï¼‰ã€‚
package-deploy.py
New file
@@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
"""TrafficAudit ä¸€é”®æ‰“包脚本:前端构建 -> å†…嵌进后端 jar -> ç»„装部署包 -> æ‰“ zip
用法:python package-deploy.py [--no-build]
"""
import io, os, sys, shutil, zipfile, subprocess, datetime
ROOT = os.path.dirname(os.path.abspath(__file__))
WEB = os.path.join(ROOT, "traffic-audit-web")
SERVER = os.path.join(ROOT, "traffic-audit-server")
STATIC = os.path.join(SERVER, "src", "main", "resources", "static")
STAMP = datetime.datetime.now().strftime("%Y%m%d")
STAGE = os.path.join(ROOT, "deploy", "traffic-audit-deploy-" + STAMP)
ZIP = os.path.join(ROOT, "traffic-audit-deploy-" + STAMP + ".zip")
JAR_NAME = "traffic-audit-server-1.0.0-SNAPSHOT.jar"
SKIP_DOC_DIRS = {"ai_demo", "usage_screenshots", "功能测试报告", "工作日结"}
def rmtree_force(path):
    """删除目录:先清掉只读属性(源目录里存在只读文件,copy2 ä¼šå¸¦è¿‡æ¥ï¼Œå¯¼è‡´ Windows ä¸‹ rmtree æŠ¥æ‹’绝访问)"""
    if not os.path.isdir(path):
        return
    for r, d, fs in os.walk(path):
        for f in fs:
            try:
                os.chmod(os.path.join(r, f), 0o666)
            except OSError:
                pass
    shutil.rmtree(path, ignore_errors=True)
def run(cmd, cwd, log):
    with io.open(log, "w", encoding="utf-8", newline="") as f:
        p = subprocess.run(cmd, cwd=cwd, stdout=f, stderr=subprocess.STDOUT, shell=True)
    return p.returncode
def main():
    do_build = "--no-build" not in sys.argv
    print("[1/6] å‰ç«¯æž„建 ...")
    if do_build:
        rc = run('cmd /c "npm.cmd run build 1>build_prod.log 2>&1"', WEB, os.path.join(WEB, "build_prod.log"))
        print("      npm run build rc=%d" % rc)
        if rc != 0:
            raise SystemExit("前端构建失败,见 traffic-audit-web/build_prod.log")
    dist = os.path.join(WEB, "dist")
    if not os.path.isfile(os.path.join(dist, "index.html")):
        raise SystemExit("未找到 dist/index.html,请先构建前端")
    print("[2/6] æŠŠ dist å†…嵌进后端 static å¹¶æ‰“ jar ...")
    copied = []
    try:
        rmtree_force(STATIC)
        shutil.copytree(dist, STATIC)
        for r, d, fs in os.walk(STATIC):
            for f in fs:
                copied.append(os.path.join(r, f))
        rc = run('cmd /c "mvn.cmd -o -q clean package -DskipTests -f traffic-audit-server\\pom.xml"',
                 ROOT, os.path.join(ROOT, "package_build.log"))
        if rc != 0:
            print("      ç¦»çº¿æ‰“包失败,改为联网打包 ...")
            rc = run('cmd /c "mvn.cmd -q clean package -DskipTests -f traffic-audit-server\\pom.xml"',
                     ROOT, os.path.join(ROOT, "package_build.log"))
        if rc != 0:
            raise SystemExit("后端打包失败,见 package_build.log")
    finally:
        rmtree_force(STATIC)
        os.makedirs(STATIC, exist_ok=True)
    jar = os.path.join(SERVER, "target", JAR_NAME)
    if not os.path.isfile(jar):
        raise SystemExit("未找到 jar: " + jar)
    print("      jar=%.1f MB" % (os.path.getsize(jar) / 1048576.0))
    print("[3/6] ç»„装部署目录 %s ..." % STAGE)
    rmtree_force(STAGE)
    be = os.path.join(STAGE, "backend")
    fe = os.path.join(STAGE, "frontend")
    db = os.path.join(STAGE, "db")
    for d in (be, fe, db):
        os.makedirs(d)
    shutil.copy2(jar, os.path.join(be, JAR_NAME))
    rmtree_force(os.path.join(be, "docs"))
    dst_docs = os.path.join(be, "docs")
    os.makedirs(dst_docs)
    for name in os.listdir(os.path.join(ROOT, "docs")):
        if name in SKIP_DOC_DIRS:
            continue
        src = os.path.join(ROOT, "docs", name)
        dst = os.path.join(dst_docs, name)
        if os.path.isdir(src):
            shutil.copytree(src, dst, ignore=shutil.ignore_patterns("输出"))
        else:
            shutil.copy2(src, dst)
    shutil.copytree(dist, os.path.join(fe, "dist"))
    for s in ("init.sql", "sql_city_bus.sql", "sql_city_taxi.sql", "sql_wyc.sql"):
        shutil.copy2(os.path.join(ROOT, "docs", s), os.path.join(db, s))
    print("[4/6] å†™å…¥é…ç½®ä¸Žå¯åŠ¨è„šæœ¬ ...")
    io.open(os.path.join(be, "application.yml"), "w", encoding="utf-8", newline="\r\n").write(APPLICATION_YML)
    io.open(os.path.join(be, "start-backend.cmd"), "w", encoding="utf-8", newline="\r\n").write(START_CMD)
    io.open(os.path.join(be, "start-backend.sh"), "w", encoding="utf-8", newline="\n").write(START_SH)
    io.open(os.path.join(STAGE, "frontend", "nginx.conf.example"), "w", encoding="utf-8", newline="\n").write(NGINX)
    io.open(os.path.join(STAGE, "README-部署说明.txt"), "w", encoding="utf-8", newline="\r\n").write(README)
    print("[5/6] æ‰“ zip ...")
    if os.path.isfile(ZIP):
        os.remove(ZIP)
    n = 0
    with zipfile.ZipFile(ZIP, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z:
        for r, d, fs in os.walk(STAGE):
            for f in fs:
                full = os.path.join(r, f)
                z.write(full, os.path.relpath(full, STAGE))
                n += 1
    print("[6/6] å®Œæˆï¼š%s (%.1f MB, %d ä¸ªæ–‡ä»¶)" % (ZIP, os.path.getsize(ZIP) / 1048576.0, n))
APPLICATION_YML = u"""# ============================================================
# TrafficAudit åŽç«¯å¤–部配置(与 jar åŒçº§å³å¯ç”Ÿæ•ˆï¼Œä¼˜å…ˆçº§é«˜äºŽ jar å†…置配置)
# éƒ¨ç½²åˆ°æ–°æœºå™¨åŽï¼Œè¯·**至少**修改下面标注 TODO çš„三处数据库配置!
# ============================================================
server:
  port: 8090
app:
  captcha:
    required: false        # ç”Ÿäº§çŽ¯å¢ƒå¦‚éœ€å›¾å½¢éªŒè¯ç ï¼Œæ”¹ä¸º true
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/traffic_audit?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
    username: root                     # TODO æ”¹æˆä½ çš„ MySQL è´¦å·
    password: è¯·æ”¹æˆä½ çš„æ•°æ®åº“密码      # TODO æ”¹æˆä½ çš„ MySQL å¯†ç 
    driver-class-name: com.mysql.cj.jdbc.Driver
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  type-aliases-package: com.trafficaudit
  configuration:
    map-underscore-to-camel-case: true
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0
jwt:
  # å¿…须是「Base64 åˆæ³•字符 + é•¿åº¦æ˜¯ 4 çš„倍数」;下面这串 32 ä½åå…­è¿›åˆ¶å¯ç›´æŽ¥ç”¨ï¼Œ
  # ç”Ÿäº§çŽ¯å¢ƒå»ºè®®æ¢æˆè‡ªå·±çš„éšæœºä¸²ï¼ˆå¡«ä¸­æ–‡æˆ–ä¸åˆæ³•å­—ç¬¦ä¼šå¯¼è‡´ç™»å½• 500)
  secret: 8f2c1d64a7b34e09c5d81f6a2b0e7c93
  expiration: 86400000
docs:
  template-root: docs
city-passenger:
  template-dir: docs/城市客运
passenger:
  template-dir: docs/公路旅客
freight:
  template-dir: docs/货运
energy:
  template-dir: docs/能耗
investment:
  template-dir: docs/投资/模板
summary:
  template-dir: docs/生成汇总大表         # æ±‡æ€»å¤§è¡¨æ¯ç‰ˆä¸Žç‰ˆå¼å‚照件所在目录(必需)
deepseek:
  api-key: ""                          # AI åˆ†æžç”¨ï¼›ç•™ç©ºåˆ™ AI åˆ†æžä¸å¯ç”¨
  api-url: https://api.deepseek.com/v1/chat/completions
  model: deepseek-chat
"""
START_CMD = u"""@echo off
chcp 65001 >nul
cd /d %~dp0
where java >nul 2>nul
if errorlevel 1 (
  echo [ERROR] æœªæ‰¾åˆ° java,请先安装 JDK 8 å¹¶åŠ å…¥ PATH
  pause
  exit /b 1
)
echo ============================================
echo  TrafficAudit å¯åŠ¨ä¸­ï¼šhttp://localhost:8090
echo  å‰ç«¯å·²å†…嵌,直接访问 8090 å³å¯ï¼ˆä¸éœ€è¦ nginx)
echo  æ—¥å¿—显示在本窗口,关闭窗口即停止服务
echo ============================================
java -jar traffic-audit-server-1.0.0-SNAPSHOT.jar
pause
"""
START_SH = u"""#!/bin/sh
cd "$(dirname "$0")" || exit 1
echo "TrafficAudit å¯åŠ¨ä¸­ï¼šhttp://localhost:8090 ï¼ˆCtrl+C åœæ­¢ï¼‰"
java -jar traffic-audit-server-1.0.0-SNAPSHOT.jar 2>&1 | tee app.log
"""
NGINX = u"""# å‰ç«¯ nginx é…ç½®ç¤ºä¾‹ï¼ˆåªæœ‰åœ¨ä¸æƒ³ç”¨ã€Œjar å†…嵌前端」时才需要)
server {
    listen       8080;
    server_name  localhost;
    root   /opt/traffic-audit/frontend/dist;   # æ”¹æˆå®žé™…路径
    index  index.html;
    location /api/ {
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 300s;
    }
    location / { try_files $uri $uri/ /index.html; }
}
"""
README = u"""TrafficAudit äº¤é€šè´§è¿ç»Ÿè®¡å®¡æ ¸ç³»ç»Ÿ â€”— éƒ¨ç½²åŒ…({stamp})
========================================================
【目录结构】
  backend/
    traffic-audit-server-1.0.0-SNAPSHOT.jar  åŽç«¯ç¨‹åºï¼ˆå«å…¨éƒ¨ä¾èµ–,**已内嵌前端**)
    docs/                                    æŠ¥è¡¨æ¨¡æ¿ä¸Žè¿è¡Œæ•°æ®ç›®å½•(必需,勿删)
    application.yml                          å¤–部配置(改这里即可,无需重新打包)
    start-backend.cmd / start-backend.sh     å¯åŠ¨è„šæœ¬
  frontend/
    dist/                                    å‰ç«¯æž„建产物(备用:想用 nginx æ‰˜ç®¡æ—¶ç”¨ï¼‰
    nginx.conf.example                       nginx é…ç½®ç¤ºä¾‹
  db/
    init.sql                                 å»ºåº“建表 + é¢„置账号(必须执行)
    sql_city_bus.sql                         åŸŽå¸‚客运-公交月度表(必须执行)
    sql_city_taxi.sql                        åŸŽå¸‚客运-出租月度表/出租授权表(必须执行)
    sql_wyc.sql                              ç½‘约车全省总量表/订单分市州表(必须执行)
  README-部署说明.txt
【部署机环境要求】
  JDK 8(必需)
  MySQL 5.7 / 8.0(必需)
  **不需要 Redis**(代码未使用;旧版说明里的 Redis æ˜¯é—留配置,可忽略)
  **不需要 nginx**(前端已内嵌进 jar,访问 8090 å³å¯ï¼›nginx ä»…为备选方案)
【部署步骤】
  1. åˆå§‹åŒ–数据库(4 ä¸ªè„šæœ¬éƒ½è¦æ‰§è¡Œï¼Œé¡ºåºå¦‚ä¸‹ï¼‰
       mysql --default-character-set=utf8mb4 -u root -p < db/init.sql
       mysql --default-character-set=utf8mb4 -u root -p traffic_audit < db/sql_city_bus.sql
       mysql --default-character-set=utf8mb4 -u root -p traffic_audit < db/sql_city_taxi.sql
       mysql --default-character-set=utf8mb4 -u root -p traffic_audit < db/sql_wyc.sql
     é¢„置账号:admin / admin123(首次登录后请改密码)
  2. æ”¹é…ç½® backend/application.yml
     - spring.datasource.url / username / password ï¼šæ”¹æˆéƒ¨ç½²æœºçš„ MySQL(默认端口 3306)
     - jwt.secret ï¼šæ”¹æˆä¸€ä¸²éšæœºå­—符串(**必须是 Base64 åˆæ³•字符且长度是 4 çš„倍数**,如 32 ä½åå…­è¿›åˆ¶ï¼›å¡«ä¸­æ–‡ä¼šç™»å½•失败)
     - deepseek.api-key ï¼šéœ€è¦ AI åˆ†æžæ—¶å¡«ï¼Œä¸éœ€è¦å¯ç•™ç©º
     æ³¨ï¼šapplication.yml ä¸Ž jar åŒçº§å³ç”Ÿæ•ˆï¼Œä¸ç”¨é‡æ–°æ‰“包。
  3. å¯åŠ¨åŽç«¯ï¼ˆåœ¨ backend ç›®å½•下)
     Windows:双击 start-backend.cmd
     Linux:  sh start-backend.sh
     æˆåŠŸæ ‡å¿—ï¼šå‡ºçŽ° "Started TrafficAuditApplication" ä¸Ž "Tomcat started on port(s): 8090"
  4. è®¿é—®ç³»ç»Ÿ
     æµè§ˆå™¨æ‰“å¼€ http://<部署机IP>:8090  ï¼ˆå‰ç«¯å·²å†…嵌,页面与接口同源)
     è´¦å·ï¼šadmin / admin123
【数据迁移(可选)】
  æƒ³æŠŠæœ¬æœºæ•°æ®ä¸€å¹¶æ¬åˆ°æ–°æœºå™¨ï¼Œç”¨ mysqldump(本机 MySQL è‹¥ä¸åœ¨ 3306,按实际端口改):
     mysqldump -h 127.0.0.1 -P 3306 -u root -p --databases traffic_audit > traffic_audit_full.sql
  å†åœ¨æ–°æœºå™¨æ‰§è¡Œï¼š
     mysql -u root -p < traffic_audit_full.sql
  æ³¨æ„ï¼šå…¨é‡ dump ä¼šå¸¦ä¸Šæœ¬æœºçš„账号密码,覆盖 init.sql é¢„置的 admin è´¦å·ã€‚
【常见问题】
  1. å¯åŠ¨æŠ¥ã€Œæœªæ‰¾åˆ°æ•°æ®ç›®å½• docs」/ å¯¼å‡ºæŠ¥ã€Œæœªæ‰¾åˆ°æ¨¡æ¿æ–‡ä»¶ã€ï¼š
     å¿…须在 backend ç›®å½•下启动(jar ä¸Ž docs åŒçº§ï¼‰ï¼Œä¸è¦åªæ‹· jar。
  2. æŠ¥è¡¨å¯¼å‡ºæŠ¥ã€Œæœªæ‰¾åˆ°æ±‡æ€»è¡¨æ¯ç‰ˆã€ŠYYYYå¹´M月道路运输量汇总表.xlsx》」:
     docs/生成汇总大表 ä¸‹ç¼ºå°‘该月份母版;把源机器的该文件拷过去。
  3. 8090 ç«¯å£è¢«å ç”¨ï¼šæ”¹ application.yml çš„ server.port åŽé‡å¯ã€‚
  4. é¡µé¢æ‰“开是空白:确认访问的是 http://<IP>:8090 æ ¹è·¯å¾„,且浏览器强制刷新(Ctrl+F5)。
  5. ä¸Šä¼ å¤§æ–‡ä»¶å¤±è´¥ï¼šåŽç«¯é™åˆ¶ 100MB,已配置;如前面还挂了 nginx,需同步放大 client_max_body_size。
【停止服务】
  Windows:关闭 start-backend.cmd çª—口
  Linux:  Ctrl+C,或 kill å¯¹åº” java è¿›ç¨‹ï¼ˆps -ef | grep java)
"""
if __name__ == "__main__":
    main()
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3353,10 +3353,12 @@
            int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
            int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
            int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // å››å¼ è·¨é¡µåˆæˆé¡µéšæŠ¥è¡¨æœŸåˆ·æ–°
            int widthApplied = applySummaryReferenceLayout(wb);
            log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={},"
                    + "刷新排名页数值格数={},按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, widthApplied, dbProblems);
                    + "刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={},"
                    + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems);
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
@@ -3654,6 +3656,496 @@
    }
    /** å®šä½ç‰ˆå¼å‚ç…§ä»¶ docs/生成汇总大表/生成_道路运输量汇总表.xlsx(优先级同母版:配置目录 â†’ user.dir ç›¸å¯¹ â†’ ä¸Šçº§ï¼‰ */
    // ==================== æ±‡æ€»å¤§è¡¨ï¼šå››å¼ â€œè·¨é¡µåˆæˆâ€é¡µéšæŠ¥è¡¨æœŸåˆ·æ–° ====================
    // åŸŽå¸‚汇总 / åŸŽå¸‚分市州 / ä¸­å£å¾„分析 / é“路运输周转量 æ­¤å‰æ•´é¡µæ²¿ç”¨æ¯ç‰ˆé™æ€å€¼ï¼ˆæ ‡é¢˜åœåœ¨ 1-7 æœˆï¼‰ã€‚
    // æœ¬æ®µæŒ‰ 2026-09-13 å·²éªŒè¯å£å¾„(与 8 æœˆæ¯ç‰ˆé€æ ¼ 0 å·®å¼‚)从母版台账页现算:
    //   åŸŽå¸‚客运量     = åŸŽå¸‚内公交 + åŸŽå¸‚内巡游出租 + åŸŽå¸‚内网约车 + è½¨é“ + è½®æ¸¡
    //   é“路客运周转量 = å…¬è·¯ç­çº¿ + åŸŽé™…城乡公交 + åŸŽé™…城乡巡游出租 + åŸŽé™…城乡网约车
    //   é“路货运周转量 = è´§è¿é¡µ å¸‚å·ž è§„上货物周转量 + è§„下货物周转量
    // 2026 å¹´ 1..N æœˆå–台账页 2026 åˆ—(C/E/G…),去年同期取母版缓存 2025 åˆ—(V/X/Z…)——库里没有 2025 å¹´åŸŽå¸‚客运数据。
    /** åŸŽå¸‚客运口径指标位(0..17 åŸŽå¸‚客运,18 è´§è¿å‘¨è½¬é‡ï¼‰ */
    private static final int SM_BUS_PAX = 0, SM_BUS_TURN = 1;
    private static final int SM_TAXI_PAX = 2, SM_TAXI_TURN = 3;
    private static final int SM_WYC_PAX = 4, SM_WYC_TURN = 5;
    private static final int SM_RAIL_PAX = 6, SM_RAIL_TURN = 7;
    private static final int SM_FERRY_PAX = 8, SM_FERRY_TURN = 9;
    private static final int SM_BX_PAX = 10, SM_BX_TURN = 11;
    private static final int SM_BUS_CX_PAX = 12, SM_BUS_CX_TURN = 13;
    private static final int SM_TAXI_CX_PAX = 14, SM_TAXI_CX_TURN = 15;
    private static final int SM_WYC_CX_PAX = 16, SM_WYC_CX_TURN = 17;
    private static final int SM_FREIGHT_TURN = 18;
    private static final int SM_DIM = 19;
    /** 2026 å¹´ 1 æœˆå½“月值列(0 åŸºï¼ŒC åˆ—);2025 å¹´ 1 æœˆå½“月值列(0 åŸºï¼ŒV åˆ—) */
    private static final int SM_CUR_COL0 = 2;
    private static final int SM_LAST_COL0 = 21;
    /**
     * åˆ·æ–°å››å¼ è·¨é¡µåˆæˆé¡µï¼šå½“年累计(1..month æœˆï¼‰ã€å½“月(month æœˆï¼‰åŠå¯¹åº”同比;
     * 2026 ä¸Ž 2025 åŒæœŸéƒ½å–自母版台账页缓存,返回写入单元格数。
     */
    private int refreshSummaryCumulativeSheets(XSSFWorkbook wb, int year, int month) {
        if (wb == null || month <= 0) return 0;
        int[] cumMonths = new int[month];
        for (int m = 1; m <= month; m++) cumMonths[m - 1] = m;
        int[] oneMonth = new int[]{month};
        Map<String, double[]> curCum = smReadLedgerMetrics(wb, true, cumMonths);
        Map<String, double[]> lastCum = smReadLedgerMetrics(wb, false, cumMonths);
        Map<String, double[]> curMon = smReadLedgerMetrics(wb, true, oneMonth);
        Map<String, double[]> lastMon = smReadLedgerMetrics(wb, false, oneMonth);
        if (curCum.isEmpty()) {
            log.warn("汇总大表四页刷新:台账页未读到市州数据,跳过(保持母版原值)");
            return 0;
        }
        double[] pCurCum = smProvinceSum(curCum);
        double[] pLastCum = smProvinceSum(lastCum);
        double[] pCurMon = smProvinceSum(curMon);
        double[] pLastMon = smProvinceSum(lastMon);
        int n = 0;
        n += smWriteCitySummary(wb, year, month, pCurCum, pLastCum, pCurMon, pLastMon);
        n += smWriteCityByRegion(wb, month, curCum, lastCum, curMon, lastMon);
        n += smWriteMidAnalysis(wb, month, pCurCum, pLastCum, pCurMon, pLastMon);
        n += smWriteTurnoverSheets(wb, year, month, curCum, lastCum, curMon, lastMon);
        return n;
    }
    /** 17 å¸‚州合计(不含台账页“全省”公式行) */
    private double[] smProvinceSum(Map<String, double[]> m) {
        double[] p = new double[SM_DIM];
        for (String c : RegionUtil.cityList()) {
            double[] a = m.get(c);
            if (a == null) continue;
            for (int i = 0; i < SM_DIM; i++) p[i] += a[i];
        }
        return p;
    }
    /** å°è´¦é¡µç´¯è®¡å–数:市州规范名 -> 19 ç»´æŒ‡æ ‡ */
    private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, boolean curYear, int[] months) {
        Map<String, double[]> out = new LinkedHashMap<>();
        smReadCityPaxPage(wb, "公交", curYear, months, out, SM_BUS_PAX, SM_BUS_TURN, SM_BUS_CX_PAX, SM_BUS_CX_TURN);
        smReadCityPaxPage(wb, "出租", curYear, months, out, SM_TAXI_PAX, SM_TAXI_TURN, SM_TAXI_CX_PAX, SM_TAXI_CX_TURN);
        smReadCityPaxPage(wb, "网约车", curYear, months, out, SM_WYC_PAX, SM_WYC_TURN, SM_WYC_CX_PAX, SM_WYC_CX_TURN);
        smReadRailFerry(wb, curYear, months, out);
        smReadBanxian(wb, curYear, months, out);
        smReadFreightTurnover(wb, curYear, months, out);
        return out;
    }
    /** å…¬äº¤/出租/网约车页:每市州 4 è¡Œï¼ˆå®¢è¿é‡/旅客周转量/其中城市内客运量/其中城市内旅客周转量) */
    private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, boolean curYear, int[] months,
                                   Map<String, double[]> out, int paxIdx, int turnIdx, int cxPaxIdx, int cxTurnIdx) {
        Sheet sh = wb.getSheet(sheetName);
        if (sh == null) return;
        int last = sh.getLastRowNum();
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = smCityOf(row.getCell(0));
            if (city == null) continue;
            int paxRow = -1, turnRow = -1, cxPaxRow = -1, cxTurnRow = -1;
            for (int k = r; k <= Math.min(last, r + 4); k++) {
                Row kr = sh.getRow(k);
                if (kr == null) continue;
                String b = smText(kr.getCell(1));
                if (b == null || b.contains("个体")) continue;
                boolean inner = b.contains("城市内");
                boolean turn = b.contains("周转量");
                if (inner) {
                    if (turn) { if (cxTurnRow < 0) cxTurnRow = k; }
                    else if (cxPaxRow < 0) cxPaxRow = k;
                } else {
                    if (turn) { if (turnRow < 0) turnRow = k; }
                    else if (paxRow < 0) paxRow = k;
                }
            }
            double totalPax = smSumMonths(sh, paxRow, curYear, months);
            double totalTurn = smSumMonths(sh, turnRow, curYear, months);
            double cityPax = smSumMonths(sh, cxPaxRow, curYear, months);
            double cityTurn = smSumMonths(sh, cxTurnRow, curYear, months);
            double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
            arr[paxIdx] += cityPax;                 // åŸŽå¸‚客运口径取“城市内”
            arr[turnIdx] += cityTurn;
            arr[cxPaxIdx] += totalPax - cityPax;    // åŸŽé™…城乡 = æ€»é‡ - åŸŽå¸‚内
            arr[cxTurnIdx] += totalTurn - cityTurn;
        }
    }
    /** è½¨é“、轮渡页:轨道块(武汉/黄石)+ è½®æ¸¡å—(武汉) */
    private void smReadRailFerry(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
        Sheet sh = wb.getSheet("轨道、轮渡");
        if (sh == null) return;
        int last = sh.getLastRowNum();
        int ferryTitle = -1;
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String a = smText(row.getCell(0));
            if (a != null && a.contains("轮渡客运量")) { ferryTitle = r; break; }
        }
        smReadRailFerryBlock(sh, curYear, months, out, sh.getFirstRowNum(),
                ferryTitle < 0 ? last : ferryTitle - 1, SM_RAIL_PAX);
        if (ferryTitle >= 0) smReadRailFerryBlock(sh, curYear, months, out, ferryTitle, last, SM_FERRY_PAX);
    }
    private void smReadRailFerryBlock(Sheet sh, boolean curYear, int[] months, Map<String, double[]> out,
                                      int start, int end, int baseIdx) {
        for (int r = start; r <= end; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = smCityOf(row.getCell(0));
            if (city == null) continue;
            int paxRow = -1, turnRow = -1;
            for (int k = r; k <= Math.min(end, r + 3); k++) {
                Row kr = sh.getRow(k);
                if (kr == null) continue;
                String b = smText(kr.getCell(1));
                if (b == null) continue;
                if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; }
                else if (paxRow < 0) paxRow = k;
            }
            double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
            arr[baseIdx] += smSumMonths(sh, paxRow, curYear, months);
            arr[baseIdx + 1] += smSumMonths(sh, turnRow, curYear, months);
        }
    }
    /** ç­çº¿åŒ…车页:每市州 å®¢è¿é‡ / æ—…客周转量(“其中个体”行不重复计入) */
    private void smReadBanxian(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
        Sheet sh = wb.getSheet("班线包车");
        if (sh == null) return;
        int last = sh.getLastRowNum();
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = smCityOf(row.getCell(0));
            if (city == null) continue;
            int paxRow = -1, turnRow = -1;
            for (int k = r; k <= Math.min(last, r + 4); k++) {
                Row kr = sh.getRow(k);
                if (kr == null) continue;
                String b = smText(kr.getCell(1));
                if (b == null || b.contains("个体")) continue;
                if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; }
                else if (b.contains("客运量") && paxRow < 0) paxRow = k;
            }
            double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
            arr[SM_BX_PAX] += smSumMonths(sh, paxRow, curYear, months);
            arr[SM_BX_TURN] += smSumMonths(sh, turnRow, curYear, months);
        }
    }
    /** è´§è¿é¡µï¼šæ¯å¸‚å·ž è§„上货物周转量 + è§„下货物周转量(“规上+规下”行本身是求和公式,不直接取) */
    private void smReadFreightTurnover(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
        Sheet sh = wb.getSheet(" è´§è¿");
        if (sh == null) sh = wb.getSheet("货运");
        if (sh == null) return;
        int last = sh.getLastRowNum();
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = smCityOf(row.getCell(0));
            if (city == null) continue;
            int above = -1, below = -1;
            for (int k = r; k <= Math.min(last, r + 6); k++) {
                Row kr = sh.getRow(k);
                if (kr == null) continue;
                String b = smText(kr.getCell(1));
                if (b == null || b.contains("其中")) continue;
                if (b.contains("规上货物周转量")) { if (above < 0) above = k; }
                else if (b.contains("规下货物周转量")) { if (below < 0) below = k; }
            }
            double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
            arr[SM_FREIGHT_TURN] += smSumMonths(sh, above, curYear, months) + smSumMonths(sh, below, curYear, months);
        }
    }
    private double smSumMonths(Sheet sh, int rowIdx, boolean curYear, int[] months) {
        if (sh == null || rowIdx < 0) return 0;
        Row row = sh.getRow(rowIdx);
        if (row == null) return 0;
        double s = 0;
        for (int m : months) {
            int col = (curYear ? SM_CUR_COL0 : SM_LAST_COL0) + 2 * (m - 1);
            s += smNum(row.getCell(col));
        }
        return s;
    }
    /** å°è´¦é¡µå•元格取数(数字 / æ•°å­—文本 / å…¬å¼ç¼“存) */
    private double smNum(Cell c) {
        if (c == null) return 0;
        try {
            CellType t = c.getCellType();
            if (t == CellType.NUMERIC) return c.getNumericCellValue();
            if (t == CellType.FORMULA) {
                return c.getCachedFormulaResultType() == CellType.NUMERIC ? c.getNumericCellValue() : 0;
            }
            if (t == CellType.STRING) {
                String s = c.getStringCellValue();
                if (s == null) return 0;
                s = s.replace(",", "").replace("%", "").trim();
                if (s.isEmpty()) return 0;
                return Double.parseDouble(s);
            }
        } catch (Exception ignore) {
            return 0;
        }
        return 0;
    }
    private String smText(Cell c) {
        if (c == null) return null;
        try {
            return c.getCellType() == CellType.STRING ? c.getStringCellValue() : null;
        } catch (Exception e) {
            return null;
        }
    }
    /** åˆ— A æ–‡æ¡ˆ -> è§„范市州名(“全省/湖北省”与非市州行返回 null) */
    private String smCityOf(Cell a) {
        String t = smText(a);
        if (t == null || t.trim().isEmpty()) return null;
        String norm = RegionUtil.normalizeCityName(t.trim());
        if (norm == null) return null;
        return RegionUtil.cityList().contains(norm) ? norm : null;
    }
    private double smYoy(double cur, double last) {
        return last == 0.0 ? 0.0 : (cur - last) / last;
    }
    /** å†™å€¼ï¼šå…¬å¼æ ¼ä¸åŠ¨ï¼ˆä¿ç•™æ¯ç‰ˆå…¬å¼ï¼Œæ‰“å¼€å³é‡ç®—ï¼‰ï¼›0 ä¸”原为空格则不新增 */
    private int smSetVal(Sheet sh, int r0, int c0, double v) {
        Row row = sh.getRow(r0);
        if (row == null) return 0;
        Cell c = row.getCell(c0);
        if (c != null && c.getCellType() == CellType.FORMULA) return 0;
        if (c == null) {
            if (v == 0.0) return 0;
            c = row.createCell(c0);
            Cell left = row.getCell(c0 - 1);
            if (left != null) c.setCellStyle(left.getCellStyle());
        } else if (v == 0.0 && c.getCellType() == CellType.BLANK) {
            return 0;
        }
        c.setBlank();
        c.setCellValue(v);
        return 1;
    }
    private void smSetTitle(Sheet sh, int r0, int c0, String text) {
        if (sh == null) return;
        Row row = sh.getRow(r0);
        if (row == null) return;
        Cell c = row.getCell(c0);
        if (c == null || c.getCellType() == CellType.FORMULA) return;
        c.setBlank();
        c.setCellValue(text);
    }
    /** åŸŽå¸‚汇总页:块1(1..N æœˆç´¯è®¡ï¼‰è¡Œ 3-9,块2(当月)行 14-20;列 B/C/D/E = å®¢è¿é‡/同比/周转量/同比 */
    private int smWriteCitySummary(XSSFWorkbook wb, int year, int month,
                                   double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) {
        Sheet sh = wb.getSheet("城市汇总");
        if (sh == null) return 0;
        smSetTitle(sh, 0, 0, year + "å¹´1-" + month + "月全省累计完成城市客运量情况");
        smSetTitle(sh, 11, 0, year + "å¹´" + month + "月全省完成城市客运量情况");
        int n = 0;
        int[] rows = {2, 3, 4, 5, 6, 7, 8};
        for (int i = 0; i < rows.length; i++) {
            double[] v = smCitySummaryRow(pCurCum, pLastCum, i);
            n += smSetVal(sh, rows[i], 1, v[0]);
            n += smSetVal(sh, rows[i], 2, v[2]);
            n += smSetVal(sh, rows[i], 3, v[1]);
            n += smSetVal(sh, rows[i], 4, v[3]);
        }
        for (int i = 0; i < rows.length; i++) {
            double[] v = smCitySummaryRow(pCurMon, pLastMon, i);
            n += smSetVal(sh, rows[i] + 11, 1, v[0]);
            n += smSetVal(sh, rows[i] + 11, 2, v[2]);
            n += smSetVal(sh, rows[i] + 11, 3, v[1]);
            n += smSetVal(sh, rows[i] + 11, 4, v[3]);
        }
        return n;
    }
    /** i=0 æ€»å®¢è¿é‡ 1 å…¬äº¤ 2 å‡ºç§Ÿè½¦ 3 å·¡æ¸¸å‡ºç§Ÿ 4 ç½‘约车 5 è½¨é“ 6 è½®æ¸¡ï¼›è¿”回 [客运量, å‘¨è½¬é‡, å®¢è¿é‡åŒæ¯”, å‘¨è½¬é‡åŒæ¯”] */
    private double[] smCitySummaryRow(double[] p, double[] pl, int i) {
        double pax, turn, lPax, lTurn;
        if (i == 0) {
            pax = p[SM_BUS_PAX] + p[SM_TAXI_PAX] + p[SM_WYC_PAX] + p[SM_RAIL_PAX] + p[SM_FERRY_PAX];
            turn = p[SM_BUS_TURN] + p[SM_TAXI_TURN] + p[SM_WYC_TURN] + p[SM_RAIL_TURN] + p[SM_FERRY_TURN];
            lPax = pl[SM_BUS_PAX] + pl[SM_TAXI_PAX] + pl[SM_WYC_PAX] + pl[SM_RAIL_PAX] + pl[SM_FERRY_PAX];
            lTurn = pl[SM_BUS_TURN] + pl[SM_TAXI_TURN] + pl[SM_WYC_TURN] + pl[SM_RAIL_TURN] + pl[SM_FERRY_TURN];
        } else if (i == 1) {
            pax = p[SM_BUS_PAX]; turn = p[SM_BUS_TURN]; lPax = pl[SM_BUS_PAX]; lTurn = pl[SM_BUS_TURN];
        } else if (i == 2) {
            pax = p[SM_TAXI_PAX] + p[SM_WYC_PAX]; turn = p[SM_TAXI_TURN] + p[SM_WYC_TURN];
            lPax = pl[SM_TAXI_PAX] + pl[SM_WYC_PAX]; lTurn = pl[SM_TAXI_TURN] + pl[SM_WYC_TURN];
        } else if (i == 3) {
            pax = p[SM_TAXI_PAX]; turn = p[SM_TAXI_TURN]; lPax = pl[SM_TAXI_PAX]; lTurn = pl[SM_TAXI_TURN];
        } else if (i == 4) {
            pax = p[SM_WYC_PAX]; turn = p[SM_WYC_TURN]; lPax = pl[SM_WYC_PAX]; lTurn = pl[SM_WYC_TURN];
        } else if (i == 5) {
            pax = p[SM_RAIL_PAX]; turn = p[SM_RAIL_TURN]; lPax = pl[SM_RAIL_PAX]; lTurn = pl[SM_RAIL_TURN];
        } else {
            pax = p[SM_FERRY_PAX]; turn = p[SM_FERRY_TURN]; lPax = pl[SM_FERRY_PAX]; lTurn = pl[SM_FERRY_TURN];
        }
        return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)};
    }
    /** åŸŽå¸‚分市州页:4 å—(累计客运量/累计周转量/当月客运量/当月周转量),每块 å…¨çœ 1 è¡Œ + 17 å¸‚å·ž */
    private int smWriteCityByRegion(XSSFWorkbook wb, int month,
                                    Map<String, double[]> curCum, Map<String, double[]> lastCum,
                                    Map<String, double[]> curMon, Map<String, double[]> lastMon) {
        Sheet sh = wb.getSheet("城市分市州");
        if (sh == null) return 0;
        smSetTitle(sh, 0, 0, "城市客运客运量分市州1-" + month + "月累计情况");
        smSetTitle(sh, 22, 0, "城市客运客运周转量分市州1-" + month + "月累计情况");
        smSetTitle(sh, 45, 0, "城市客运客运量分市州" + month + "月情况");
        smSetTitle(sh, 67, 0, "城市客运客运周转量分市州" + month + "月情况");
        int n = 0;
        n += smWriteCityByRegionBlock(sh, 4, true, curCum, lastCum);
        n += smWriteCityByRegionBlock(sh, 26, false, curCum, lastCum);
        n += smWriteCityByRegionBlock(sh, 49, true, curMon, lastMon);
        n += smWriteCityByRegionBlock(sh, 71, false, curMon, lastMon);
        return n;
    }
    private int smWriteCityByRegionBlock(Sheet sh, int provRow, boolean pax,
                                         Map<String, double[]> cur, Map<String, double[]> last) {
        int n = 0;
        double[] provCur = smProvinceSum(cur);
        double[] provLast = smProvinceSum(last);
        List<String> cities = RegionUtil.cityList();
        for (int i = -1; i < cities.size(); i++) {
            int r = provRow + i + 1;
            double[] c = i < 0 ? provCur : cur.get(cities.get(i));
            double[] l = i < 0 ? provLast : last.get(cities.get(i));
            if (c == null) c = new double[SM_DIM];
            if (l == null) l = new double[SM_DIM];
            double[] g = smCityGroupValues(c, l, pax);
            n += smSetVal(sh, r, 1, g[0]);
            n += smSetVal(sh, r, 4, g[6]);
            n += smSetVal(sh, r, 6, g[1]);
            n += smSetVal(sh, r, 9, g[7]);
            n += smSetVal(sh, r, 11, g[2]);
            n += smSetVal(sh, r, 14, g[8]);
            n += smSetVal(sh, r, 16, g[3]);
            n += smSetVal(sh, r, 19, g[9]);
            n += smSetVal(sh, r, 21, g[4]);
            n += smSetVal(sh, r, 22, g[10]);
            n += smSetVal(sh, r, 23, g[5]);
            n += smSetVal(sh, r, 24, g[11]);
        }
        return n;
    }
    /** 6 ç»„值 + 6 ç»„增速:[总量, å…¬äº¤, å‡ºç§Ÿ, ç½‘约车, è½¨é“, è½®æ¸¡, æ€»é‡å¢žé€Ÿ, å…¬äº¤â€¦, è½®æ¸¡] */
    private double[] smCityGroupValues(double[] c, double[] l, boolean pax) {
        int busI = pax ? SM_BUS_PAX : SM_BUS_TURN;
        int taxiI = pax ? SM_TAXI_PAX : SM_TAXI_TURN;
        int wycI = pax ? SM_WYC_PAX : SM_WYC_TURN;
        int railI = pax ? SM_RAIL_PAX : SM_RAIL_TURN;
        int ferryI = pax ? SM_FERRY_PAX : SM_FERRY_TURN;
        double bus = c[busI], taxi = c[taxiI], wyc = c[wycI], rail = c[railI], ferry = c[ferryI];
        double lBus = l[busI], lTaxi = l[taxiI], lWyc = l[wycI], lRail = l[railI], lFerry = l[ferryI];
        double total = bus + taxi + wyc + rail + ferry;
        double lTotal = lBus + lTaxi + lWyc + lRail + lFerry;
        return new double[]{total, bus, taxi, wyc, rail, ferry,
                smYoy(total, lTotal), smYoy(bus, lBus), smYoy(taxi, lTaxi), smYoy(wyc, lWyc),
                smYoy(rail, lRail), smYoy(ferry, lFerry)};
    }
    /** ä¸­å£å¾„分析页:块1(累计)行 3-7,块2(当月)行 12-16;列 B/C/F/G = å®¢è¿é‡/同比/周转量/同比 */
    private int smWriteMidAnalysis(XSSFWorkbook wb, int month,
                                   double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) {
        Sheet sh = wb.getSheet("中口径分析");
        if (sh == null) return 0;
        smSetTitle(sh, 0, 0, "1-" + month + "月中口径客运量及周转量");
        smSetTitle(sh, 9, 0, month + "月中口径客运量及周转量");
        int n = 0;
        int[] rows = {2, 3, 4, 5, 6};
        for (int i = 0; i < rows.length; i++) {
            double[] v = smMidRow(pCurCum, pLastCum, i);
            n += smSetVal(sh, rows[i], 1, v[0]);
            n += smSetVal(sh, rows[i], 2, v[2]);
            n += smSetVal(sh, rows[i], 5, v[1]);
            n += smSetVal(sh, rows[i], 6, v[3]);
        }
        for (int i = 0; i < rows.length; i++) {
            double[] v = smMidRow(pCurMon, pLastMon, i);
            n += smSetVal(sh, rows[i] + 9, 1, v[0]);
            n += smSetVal(sh, rows[i] + 9, 2, v[2]);
            n += smSetVal(sh, rows[i] + 9, 5, v[1]);
            n += smSetVal(sh, rows[i] + 9, 6, v[3]);
        }
        return n;
    }
    /** i=0 æ€» 1 å…¬è·¯ç­çº¿ 2 åŸŽé™…城乡公交 3 åŸŽé™…城乡巡游出租 4 åŸŽé™…城乡网约车;返回 [客运量, å‘¨è½¬é‡, å®¢è¿é‡åŒæ¯”, å‘¨è½¬é‡åŒæ¯”] */
    private double[] smMidRow(double[] p, double[] pl, int i) {
        int[] paxIdx = {SM_BX_PAX, SM_BX_PAX, SM_BUS_CX_PAX, SM_TAXI_CX_PAX, SM_WYC_CX_PAX};
        int[] turnIdx = {SM_BX_TURN, SM_BX_TURN, SM_BUS_CX_TURN, SM_TAXI_CX_TURN, SM_WYC_CX_TURN};
        double pax, turn, lPax, lTurn;
        if (i == 0) {
            pax = p[SM_BX_PAX] + p[SM_BUS_CX_PAX] + p[SM_TAXI_CX_PAX] + p[SM_WYC_CX_PAX];
            turn = p[SM_BX_TURN] + p[SM_BUS_CX_TURN] + p[SM_TAXI_CX_TURN] + p[SM_WYC_CX_TURN];
            lPax = pl[SM_BX_PAX] + pl[SM_BUS_CX_PAX] + pl[SM_TAXI_CX_PAX] + pl[SM_WYC_CX_PAX];
            lTurn = pl[SM_BX_TURN] + pl[SM_BUS_CX_TURN] + pl[SM_TAXI_CX_TURN] + pl[SM_WYC_CX_TURN];
        } else {
            pax = p[paxIdx[i]]; turn = p[turnIdx[i]];
            lPax = pl[paxIdx[i]]; lTurn = pl[turnIdx[i]];
        }
        return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)};
    }
    /** é“路运输周转量页:块1(累计)行 4-21,块2(当月)行 26-43;只写值列(B/E/V ç­‰å…¬å¼ä¿ç•™ï¼‰ */
    private int smWriteTurnoverSheets(XSSFWorkbook wb, int year, int month,
                                      Map<String, double[]> curCum, Map<String, double[]> lastCum,
                                      Map<String, double[]> curMon, Map<String, double[]> lastMon) {
        Sheet sh = wb.getSheet("道路运输周转量");
        if (sh == null) return 0;
        smSetTitle(sh, 0, 0, year + "å¹´1-" + month + "月全省累计完成交通周转量情况");
        smSetTitle(sh, 22, 0, year + "å¹´" + month + "月全省完成交通周转量情况");
        int n = 0;
        n += smWriteTurnoverBlock(sh, 3, curCum, lastCum);
        n += smWriteTurnoverBlock(sh, 25, curMon, lastMon);
        return n;
    }
    private int smWriteTurnoverBlock(Sheet sh, int provRow, Map<String, double[]> cur, Map<String, double[]> last) {
        int n = 0;
        double[] provCur = smProvinceSum(cur);
        double[] provLast = smProvinceSum(last);
        List<String> cities = RegionUtil.cityList();
        for (int i = -1; i < cities.size(); i++) {
            int r = provRow + i + 1;
            double[] c = i < 0 ? provCur : cur.get(cities.get(i));
            double[] l = i < 0 ? provLast : last.get(cities.get(i));
            if (c == null) c = new double[SM_DIM];
            if (l == null) l = new double[SM_DIM];
            double freight = c[SM_FREIGHT_TURN], lFreight = l[SM_FREIGHT_TURN];
            double pax = c[SM_BUS_PAX] + c[SM_TAXI_PAX] + c[SM_WYC_PAX] + c[SM_RAIL_PAX] + c[SM_FERRY_PAX];
            double lPax = l[SM_BUS_PAX] + l[SM_TAXI_PAX] + l[SM_WYC_PAX] + l[SM_RAIL_PAX] + l[SM_FERRY_PAX];
            double road = c[SM_BX_TURN] + c[SM_BUS_CX_TURN] + c[SM_TAXI_CX_TURN] + c[SM_WYC_CX_TURN];
            double lRoad = l[SM_BX_TURN] + l[SM_BUS_CX_TURN] + l[SM_TAXI_CX_TURN] + l[SM_WYC_CX_TURN];
            n += smSetVal(sh, r, 6, freight);
            n += smSetVal(sh, r, 9, smYoy(freight, lFreight));
            n += smSetVal(sh, r, 11, pax);
            n += smSetVal(sh, r, 14, smYoy(pax, lPax));
            n += smSetVal(sh, r, 16, road);
            n += smSetVal(sh, r, 19, smYoy(road, lRoad));
            n += smSetVal(sh, r, 22, lFreight);
            n += smSetVal(sh, r, 23, lPax);
            n += smSetVal(sh, r, 24, lRoad);
        }
        return n;
    }
    private File resolveSummaryLayoutRef() {
        String rel = summaryTemplateDir;
        while (rel != null && rel.startsWith("./")) rel = rel.substring(2);
traffic-audit-server/src/main/java/com/trafficaudit/security/utils/JwtUtils.java
@@ -3,9 +3,13 @@
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.TextCodec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@@ -13,11 +17,27 @@
@Component
public class JwtUtils {
    private static final Logger log = LoggerFactory.getLogger(JwtUtils.class);
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    /**
     * ç­¾åå¯†é’¥å­—节:jjwt 0.9.x é»˜è®¤æŠŠ secret å½“ Base64 è§£ç ï¼Œè‹¥é…ç½®æˆä¸­æ–‡ç­‰éžæ³• Base64 ä¼šåœ¨ç™»å½•时抛
     * ArrayIndexOutOfBoundsException,表现为「登录接口 500」。这里做兜底:能按 Base64 è§£ç å°±ç”¨è§£ç ç»“æžœ
     * ï¼ˆä¸ŽåŽ†å² token å…¼å®¹ï¼‰ï¼Œå¦åˆ™æŒ‰ UTF-8 å­—节处理,并把提示打到日志里。
     */
    private byte[] secretKeyBytes() {
        try {
            return TextCodec.BASE64.decode(secret);
        } catch (Exception e) {
            log.warn("jwt.secret ä¸æ˜¯åˆæ³•çš„ Base64 å­—符串,已按 UTF-8 å­—节作为签名密钥;建议改成 32 ä½éšæœºåå…­è¿›åˆ¶ä¸²ã€‚");
            return secret.getBytes(StandardCharsets.UTF_8);
        }
    }
    public String generateToken(String username) {
        Map<String, Object> claims = new HashMap<>();
@@ -27,19 +47,19 @@
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(SignatureAlgorithm.HS256, secret)
            .signWith(SignatureAlgorithm.HS256, secretKeyBytes())
            .compact();
    }
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser().setSigningKey(secret)
        Claims claims = Jwts.parser().setSigningKey(secretKeyBytes())
            .parseClaimsJws(token).getBody();
        return claims.getSubject();
    }
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            Jwts.parser().setSigningKey(secretKeyBytes()).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;