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