#!/bin/bash # ============================================================================== # 铁路培训实训教学平台 - 全自动平滑部署与自检脚本 # 路径规范:/data2/wzb/deploy # ============================================================================== DEPLOY_ROOT="/data2/wzb/deploy" BACKEND_DIR="$DEPLOY_ROOT/backend" FRONTEND_DIR="$DEPLOY_ROOT/frontend" LOG_DIR="$DEPLOY_ROOT/logs" PID_FILE="$BACKEND_DIR/app.pid" JAR_PATH="$BACKEND_DIR/ruoyi-admin.jar" LOG_PATH="$LOG_DIR/backend.log" JAVA_BIN="/data2/wzb/tools/jdk8/bin/java" # 服务运行端口与数据库参数 SERVER_PORT=8092 DB_URL="jdbc:kingbase8://192.168.139.218:4321/training?currentSchema=public,sys_catalog&useUnicode=true&characterEncoding=utf8" mkdir -p "$BACKEND_DIR" "$FRONTEND_DIR" "$LOG_DIR" echo "=== 0. 检查运行时安全配置 ===" MISSING_CONFIG=0 if [ -z "${TRAIN_STUDENT_IDENTITY_ENCRYPTION_KEY:-}" ]; then echo "错误:缺少 TRAIN_STUDENT_IDENTITY_ENCRYPTION_KEY" MISSING_CONFIG=1 fi if [ -z "${TRAIN_STUDENT_IDENTITY_HASH_KEY:-}" ]; then echo "错误:缺少 TRAIN_STUDENT_IDENTITY_HASH_KEY" MISSING_CONFIG=1 fi if [ -z "${SPRING_REDIS_PASSWORD:-}" ]; then echo "错误:缺少 SPRING_REDIS_PASSWORD" MISSING_CONFIG=1 fi if [ "$MISSING_CONFIG" -ne 0 ]; then echo "运行时安全配置检查失败" exit 1 fi echo "required runtime configuration present." echo "=== 1. 检查并停止当前用户的旧后端进程 ===" TARGET_PID="" if [ -f "$PID_FILE" ]; then SAVED_PID=$(cat "$PID_FILE") if kill -0 "$SAVED_PID" 2>/dev/null; then TARGET_PID="$SAVED_PID" fi fi if [ -z "$TARGET_PID" ]; then TARGET_PID=$(pgrep -u $(whoami) -f "$JAR_PATH" | head -n 1) fi if [ -n "$TARGET_PID" ]; then echo "正在停止旧进程 PID: $TARGET_PID ..." kill -15 $TARGET_PID 2>/dev/null sleep 3 if kill -0 $TARGET_PID 2>/dev/null; then echo "强制终止 PID: $TARGET_PID" kill -9 $TARGET_PID 2>/dev/null fi fi rm -f "$PID_FILE" echo "=== 2. 启动新的后端服务(端口: $SERVER_PORT) ===" if [ ! -f "$JAR_PATH" ]; then echo "错误:未找到 $JAR_PATH" exit 1 fi nohup $JAVA_BIN -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m \ -Dfile.encoding=UTF-8 \ -jar "$JAR_PATH" \ --server.port=$SERVER_PORT \ --spring.datasource.druid.master.url="$DB_URL" \ --spring.datasource.druid.master.username="${DB_USERNAME:-kingbase}" \ --spring.datasource.druid.master.password="${DB_PASSWORD:-sict123456}" > "$LOG_PATH" 2>&1 & NEW_PID=$! echo $NEW_PID > "$PID_FILE" echo "后端已在后台启动,PID: $NEW_PID" echo "实时日志: $LOG_PATH" echo "等待服务启动与健康自检..." HEALTH_OK=0 for i in {1..35}; do RES=$(curl -s "http://127.0.0.1:${SERVER_PORT}/captchaImage" 2>/dev/null) if echo "$RES" | grep -q '"code":200'; then HEALTH_OK=1 echo "后端健康检查通过!验证码接口返回正常!服务已在端口 $SERVER_PORT 成功运行!" break fi if ! kill -0 "$NEW_PID" 2>/dev/null; then echo "错误:后端进程在启动阶段提前退出,请检查 $LOG_PATH" cat "$LOG_PATH" | tail -n 50 || true rm -f "$PID_FILE" exit 1 fi sleep 2 done if [ "$HEALTH_OK" != "1" ]; then echo "Backend health check failed: 后端健康检查失败或超时,请检查 $LOG_PATH" if kill -0 "$NEW_PID" 2>/dev/null; then kill -15 "$NEW_PID" 2>/dev/null || true sleep 2 if kill -0 "$NEW_PID" 2>/dev/null; then kill -9 "$NEW_PID" 2>/dev/null || true fi fi rm -f "$PID_FILE" exit 1 fi echo "=== 3. 检查并启动/重载前端 Web 服务 (Nginx 8082) ===" NGINX_DIR="$DEPLOY_ROOT/nginx" NGINX_CONF="$NGINX_DIR/nginx.conf" mkdir -p "$NGINX_DIR/temp" if [ ! -f "$NGINX_CONF" ]; then cat << 'NGINX_EOF' > "$NGINX_CONF" pid /data2/wzb/deploy/nginx/nginx.pid; error_log /data2/wzb/deploy/nginx/error.log; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; client_body_temp_path /data2/wzb/deploy/nginx/temp/client_body; proxy_temp_path /data2/wzb/deploy/nginx/temp/proxy; fastcgi_temp_path /data2/wzb/deploy/nginx/temp/fastcgi; uwsgi_temp_path /data2/wzb/deploy/nginx/temp/uwsgi; scgi_temp_path /data2/wzb/deploy/nginx/temp/scgi; access_log /data2/wzb/deploy/nginx/access.log; sendfile on; keepalive_timeout 65; server { listen 8082; server_name localhost; location / { root /data2/wzb/deploy/frontend; try_files $uri $uri/ /index.html; index index.html; } location /prod-api/ { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8092/; } } } NGINX_EOF fi echo "验证 Nginx 配置文件 (Nginx Gate)..." if ! /usr/sbin/nginx -t -c "$NGINX_CONF"; then echo "错误:Nginx 配置文件测试失败,禁止启动或重载" exit 1 fi NGINX_PID_FILE="$NGINX_DIR/nginx.pid" IS_RUNNING=0 TARGET_NGINX_PID="" # 1. 检查专属 PID 文件并校验进程归属 if [ -f "$NGINX_PID_FILE" ]; then RECORDED_PID=$(cat "$NGINX_PID_FILE" 2>/dev/null | tr -d '[:space:]') if [ -n "$RECORDED_PID" ] && kill -0 "$RECORDED_PID" 2>/dev/null; then if ps -p "$RECORDED_PID" -o args= 2>/dev/null | grep -F -- "$NGINX_CONF" >/dev/null; then IS_RUNNING=1 TARGET_NGINX_PID="$RECORDED_PID" else echo "警告:$NGINX_PID_FILE 中的 PID $RECORDED_PID 不属于本实例,清理失效 PID 文件..." rm -f "$NGINX_PID_FILE" fi else echo "发现已退出的过期 Nginx PID (${RECORDED_PID:-空}),清理失效 PID 文件..." rm -f "$NGINX_PID_FILE" fi fi # 2. 兜底探测:仅按专用配置文件路径查找存活 master 进程 if [ "$IS_RUNNING" -eq 0 ]; then FALLBACK_PID=$(pgrep -u "$(whoami)" -f "nginx: master.*$NGINX_CONF" | head -n 1) if [ -n "$FALLBACK_PID" ] && kill -0 "$FALLBACK_PID" 2>/dev/null; then echo "发现运行中但缺少有效 PID 文件的专属 Nginx 实例 (PID: $FALLBACK_PID),回填 PID..." echo "$FALLBACK_PID" > "$NGINX_PID_FILE" IS_RUNNING=1 TARGET_NGINX_PID="$FALLBACK_PID" fi fi # 3. 根据实际存活状态执行平滑重载或全新启动 if [ "$IS_RUNNING" -eq 1 ]; then echo "专属 Nginx 服务运行中 (PID: $TARGET_NGINX_PID),执行平滑重载..." if ! /usr/sbin/nginx -c "$NGINX_CONF" -s reload; then echo "错误:专属 Nginx 平滑重载失败" exit 1 fi else echo "未发现运行中的专属 Nginx 实例,执行全新启动..." rm -f "$NGINX_PID_FILE" if ! /usr/sbin/nginx -c "$NGINX_CONF"; then echo "错误:专属 Nginx 启动失败" exit 1 fi fi # 4. 验证专属 Nginx 实例运行状态 sleep 1 FINAL_PID=$(cat "$NGINX_PID_FILE" 2>/dev/null | tr -d '[:space:]') if [ -z "$FINAL_PID" ] || ! kill -0 "$FINAL_PID" 2>/dev/null; then echo "错误:Nginx 启动或重载后未检测到存活的 master 进程" exit 1 fi echo "=== 4. 执行生产环境自动化冒烟测试 (Production Smoke Gate) ===" SMOKE_PASS=1 # A. 验证前端应用首页 HTTP_STATUS_A=$(curl -o /dev/null -s -w "%{http_code}" "http://127.0.0.1:8082/") if [ "$HTTP_STATUS_A" = "200" ]; then echo "[Smoke PASS] A: http://127.0.0.1:8082/ -> HTTP $HTTP_STATUS_A" else echo "[Smoke FAIL] A: http://127.0.0.1:8082/ -> HTTP $HTTP_STATUS_A (预期 200)" SMOKE_PASS=0 fi # B. 验证后端 API 反向代理及业务状态 RES_B=$(curl -s "http://127.0.0.1:8082/prod-api/captchaImage" 2>/dev/null) HTTP_STATUS_B=$(curl -o /dev/null -s -w "%{http_code}" "http://127.0.0.1:8082/prod-api/captchaImage") if [ "$HTTP_STATUS_B" = "200" ] && echo "$RES_B" | grep -q '"code":200'; then echo "[Smoke PASS] B: http://127.0.0.1:8082/prod-api/captchaImage -> HTTP $HTTP_STATUS_B 且 code:200" else echo "[Smoke FAIL] B: http://127.0.0.1:8082/prod-api/captchaImage -> HTTP $HTTP_STATUS_B 业务响应异常" SMOKE_PASS=0 fi # C. 验证 Human Documentation Portal 首页 HTTP_STATUS_C=$(curl -o /dev/null -s -w "%{http_code}" "http://127.0.0.1:8082/docs/visual/index.html") if [ "$HTTP_STATUS_C" = "200" ]; then echo "[Smoke PASS] C: http://127.0.0.1:8082/docs/visual/index.html -> HTTP $HTTP_STATUS_C" else echo "[Smoke FAIL] C: http://127.0.0.1:8082/docs/visual/index.html -> HTTP $HTTP_STATUS_C (预期 200)" SMOKE_PASS=0 fi # D. 验证文档静态资源 docs.css 与 docs.js HTTP_STATUS_CSS=$(curl -o /dev/null -s -w "%{http_code}" "http://127.0.0.1:8082/docs/visual/assets/docs.css") HTTP_STATUS_JS=$(curl -o /dev/null -s -w "%{http_code}" "http://127.0.0.1:8082/docs/visual/assets/docs.js") if [ "$HTTP_STATUS_CSS" = "200" ] && [ "$HTTP_STATUS_JS" = "200" ]; then echo "[Smoke PASS] D: docs.css (HTTP $HTTP_STATUS_CSS) & docs.js (HTTP $HTTP_JS) -> HTTP 200" else echo "[Smoke FAIL] D: docs.css (HTTP $HTTP_STATUS_CSS), docs.js (HTTP $HTTP_JS) (预期 200)" SMOKE_PASS=0 fi if [ "$SMOKE_PASS" != "1" ]; then echo "错误:生产环境冒烟测试未全部通过,部署失败!" exit 1 fi echo "=== 生产环境冒烟测试全部通过 (Production Smoke Gate PASSED) ===" echo "APPLICATION_URL=http://192.168.139.230:8082/" echo "DOCUMENTATION_URL=http://192.168.139.230:8082/docs/visual/index.html" echo "=== 部署全部就绪!前端访问地址: http://192.168.139.230:8082 ==="