feat: 打包部署支持(一键打包脚本 + jar 内嵌前端 + jwt.secret 部署陷阱修复)

需求:把系统打包部署到另一台机器。

- 新增 package-deploy.py:前端 npm run build → dist 内嵌进后端 jar(拷入 resources/static)→ mvn clean package
→ 清空 static(保持仓库干净)→ 组装 deploy/traffic-audit-deploy-YYYYMMDD/ → 压缩 zip(69.9 MB / 246 文件,jar 55.5 MB)。
- 部署包结构沿用 8 月版习惯:backend(jar + docs + application.yml + 启停脚本)、frontend(dist + nginx 示例)、
db(init.sql + sql_city_bus/taxi/wyc.sql)、README。
- 修复部署陷阱:jwt.secret 填成非法 Base64(如中文)时,jjwt 0.9.1 解码抛 ArrayIndexOutOfBoundsException,
登录接口返回 500。JwtUtils 新增 secretKeyBytes():Base64 合法则按 Base64 解码(与历史 token 兼容),
否则按 UTF-8 字节并打 WARN。打包配置给出可直接使用的 32 位十六进制示例值。
- 更正 docs/打包部署说明.md 三处不实:① 不需要 Redis(pom 无依赖、代码未用);② 前端改为内嵌进 jar
(旧说明称已内置 file:./web/,实际 jar 内无静态文件,旧包靠 nginx);③ 补 sql_wyc.sql、summary.template-dir、
jwt.secret 规则、部署后验证清单与常见问题。
- 验证:部署包 backend 拷到 _deploytest/ 用 8091 独立启动 → 启动成功、GET / 200(含 #app)、静态 js 200、
登录 200 拿 token、带 token 导出汇总大表 200 / 950,783 字节(与开发机产物同尺寸);中文 secret 兜底路径同样 200。
开发机后端已重启加载新代码,登录与导出无回归(950,783 字节)。
- .gitignore 增加 deploy/、traffic-audit-deploy-*.zip、_deploytest/。
- 文档:docs/功能测试报告/功能测试报告_部署包打包与独立启动_2026-09-13.md、docs/工作日结/工作日结_2026-09-13.md。
4个文件已修改
2个文件已添加
653 ■■■■■ 已修改文件
.gitignore 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_部署包打包与独立启动_2026-09-13.md 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-13.md 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/打包部署说明.md 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-deploy.py 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/security/utils/JwtUtils.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -67,3 +67,9 @@
# ==== packaged static dir (copied from dist, not committed) ====
web/
traffic-audit-server/web/
# ==== éƒ¨ç½²äº§ç‰©ï¼ˆä¸å…¥åº“)====
deploy/
traffic-audit-deploy-*.zip
_t_wyc_aug.docx
_deploytest/
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
@@ -39,6 +39,16 @@
- æŽ¨é€å‰ `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`。
## äºŒã€æµ‹è¯•结果摘要(详见《功能测试报告》)
| é¡¹ | ç»“æžœ |
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,161 @@
|------|------|
| `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 | 5.7 / 8.0 | éƒ¨ç½²æœºå¿…须,库名 `traffic_audit` |
| 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 åˆå§‹åŒ–数据库(4 ä¸ªè„šæœ¬éƒ½è¦æ‰§è¡Œï¼‰
## å…­ã€å…³é”®é…ç½®ï¼ˆapplication.yml)
```bash
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`(登录后请立即改密码)。
- æƒ³è¿žæ•°æ®ä¸€èµ·æ¬ï¼šåœ¨æºæœºå™¨ `mysqldump -u root -p --databases traffic_audit > traffic_audit_full.sql`,在目标机器 `mysql -u root -p < traffic_audit_full.sql`(会覆盖预置账号)。
### 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/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;