#!/bin/bash # 管理会计OS — 一键拉取、构建并部署到生产 set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" echo "===== 管理会计OS 生产部署 =====" echo "" # 0. 提交纪律检查:本地有未提交修改则中止,防止 git pull 覆盖丢失 echo "[0/5] 提交纪律检查..." cd "$SCRIPT_DIR" DIRTY=$(git status --porcelain 2>/dev/null | grep -v "__pycache__" | head -20) if [ -n "$DIRTY" ]; then echo "❌ 检测到本地未提交的修改,中止部署(防止 git pull 覆盖丢失代码):" echo "$DIRTY" echo "" echo "请先提交代码: git add -A && git commit -m '...' && git push origin main" exit 1 fi echo " ✓ 工作区干净,无未提交修改" # 1. 拉取最新代码 echo "[1/5] 拉取最新代码..." git pull origin main echo " ✓ 代码已更新" # 2. 构建前端 echo "[2/5] 构建前端..." cd "$SCRIPT_DIR/frontend" pnpm build echo " ✓ 构建完成" # 3. 部署前端到 Nginx echo "[3/5] 部署前端..." rm -rf /var/www/cma/assets/ cp -r dist/* /var/www/cma/ echo " ✓ 部署完成" # 4. 更新后端依赖(失败不阻断 — 依赖已在系统Python中,避免PEP668中断部署) echo "[4/5] 更新后端依赖..." cd "$SCRIPT_DIR/backend" if [ -f venv/bin/activate ]; then source venv/bin/activate pip install -r requirements.txt --quiet --no-cache-dir || echo " ⚠️ 依赖更新失败(不阻断)" deactivate else echo " ⚠️ venv为软链精简版,跳过依赖更新" fi echo " ✓ 依赖检查完成" # 5. 重启后端服务 echo "[5/5] 重启后端服务..." systemctl daemon-reload systemctl restart cma-backend systemctl reload nginx 2>/dev/null || nginx -s reload echo " ✓ 服务重启完成" # ── 部署后冒烟测试 ── echo "" echo "===== 冒烟测试 =====" # 就绪轮询(2026-09-14 cma-defaultcred-code-fix):journalctl 实测冷启动就绪间隔 2/2/3/3/5s, # 固定 sleep 3 会让贴边/超时的冷启动误判为冒烟红;改为每 0.5s 探一次 /health,命中即进入判据。 # 轮询用尽仍不健康 → 下面的健康判据照常判 ❌(不静默放行)。 # 探测上限可由 SMOKE_READY_MAX 覆盖(离线演练用短上限,生产不设即 30s)。 SMOKE_READY_MAX="${SMOKE_READY_MAX:-30}" HEALTH_URL="http://127.0.0.1:8010/health" READY_TRIES=$(( SMOKE_READY_MAX * 2 )) READY_TRY=0 READY_T0=$(date +%s) while [ "$READY_TRY" -lt "$READY_TRIES" ]; do if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then break; fi sleep 0.5 READY_TRY=$((READY_TRY + 1)) done READY_ELAPSED=$(( $(date +%s) - READY_T0 )) echo " ⏱ 就绪探测: 耗时 ${READY_ELAPSED}s / 轮询 ${READY_TRY} 次(上限 ${SMOKE_READY_MAX}s,命中健康即停)" SMOKE_FAIL=0 if curl -sf http://127.0.0.1:8010/health > /dev/null 2>&1; then echo " ✅ 后端健康检查通过" else echo " ❌ 后端健康检查失败"; SMOKE_FAIL=1 fi if curl -sf http://127.0.0.1:8010/api/cma/bot/ping > /dev/null 2>&1; then echo " ✅ BOT桥接API通过" else echo " ❌ BOT桥接API失败"; SMOKE_FAIL=1 fi # Schema 一致性检查(数据库与ORM结构对齐) if /root/cma-management/backend/venv/bin/python3 /root/cma-management/backend/scripts/schema_check.py > /dev/null 2>&1; then echo " ✅ Schema一致性通过" else echo " ❌ Schema不一致,请检查: cd /root/cma-management/backend && venv/bin/python3 scripts/schema_check.py" SMOKE_FAIL=1 fi # 凭据同源(2026-09-14 cma-defaultcred-code-fix):登录冒烟不再硬编码默认账号/口令 —— 轮换后必须自动跟随。 # 落点:$CMA_ADMIN_ENV 或 600 密钥文件(默认 wecom-ops 主副本);缺失/缺键 → 显式报错并中止,禁止静默退回默认口令。 CMA_ADMIN_ENV_FILE="${CMA_ADMIN_ENV:-/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env}" if [ ! -f "$CMA_ADMIN_ENV_FILE" ]; then echo " ❌ 管理员凭据文件缺失: $CMA_ADMIN_ENV_FILE" echo " 请准备 600 权限密钥文件(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS),或用 CMA_ADMIN_ENV 指定路径" exit 1 fi set -a # shellcheck disable=SC1090 . "$CMA_ADMIN_ENV_FILE" set +a if [ -z "${CMA_ADMIN_USER:-}" ] || [ -z "${CMA_ADMIN_PASS:-}" ]; then echo " ❌ 凭据文件缺少 CMA_ADMIN_USER / CMA_ADMIN_PASS: $CMA_ADMIN_ENV_FILE" exit 1 fi echo " ℹ️ 登录冒烟凭据来源: $CMA_ADMIN_ENV_FILE(账号 ${CMA_ADMIN_USER},口令不回显)" TOKEN=$(curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \ -H "Content-Type: application/json" \ -d "{\"username\":\"${CMA_ADMIN_USER}\",\"password\":\"${CMA_ADMIN_PASS}\",\"entity_id\":1}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null) if [ -n "$TOKEN" ]; then echo " ✅ 登录API通过" if curl -sf -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8010/api/cma/kpis > /dev/null 2>&1; then echo " ✅ KPI接口通过" else echo " ❌ KPI接口失败"; SMOKE_FAIL=1 fi else echo " ❌ 登录API失败"; SMOKE_FAIL=1 fi if [ "$SMOKE_FAIL" -eq 1 ]; then echo "" echo "🚨 冒烟测试未全部通过!请检查后端日志: journalctl -u cma-backend -n 50" exit 1 fi echo " ✅ 全部冒烟测试通过" echo "" echo "===== 部署完成 =====" echo " 前端: https://cma.sxbh.ltd" echo " 后端: http://127.0.0.1:8010/docs" echo " 后端状态: systemctl status cma-backend"