# -*- 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://: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()