zhizhijie
2026-09-14 d1cfd849c6a61800dcd86ffb45f80882ae20b2e7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
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()