#!/usr/bin/env bash # ============================================================================ # 全栈部署脚本:从零到上线(首次部署 / 服务器重装 / 数据迁移后的完整拉起) # # 与 deploy.sh 区别: # deploy.sh = App 发版专用(编译 APK → 部署 → 同步 DB 版本号) # deploy-full.sh = 整个项目首装/重装/迁移恢复,覆盖 Docker / 后端 / nginx / 后台前端 # # 用法: # bash deploy-full.sh # 完整执行(首次部署) # bash deploy-full.sh --skip-apk # 跳过 App 编译(已有 APK 时提速) # bash deploy-full.sh --skip-nginx # 跳过 nginx 配置写入(仅重启服务) # bash deploy-full.sh --skip-admin # 跳过 admin 前端构建 # bash deploy-full.sh --only-backend # 只重启后端 + 健康检查 # # 前置条件: # - Debian/Ubuntu 系统,已安装:nginx, docker, mysql-client, python3.11+, # nodejs 20+, npm, flutter SDK (在 /opt/flutter) # - 项目代码已 clone 到 /root/project/video # - DNS 已把 h5/vm/vapi.256kb.top 解析到本机 # ============================================================================ set -euo pipefail # ---------- 参数解析 ---------- SKIP_APK=0 SKIP_NGINX=0 SKIP_ADMIN=0 ONLY_BACKEND=0 for arg in "$@"; do case "$arg" in --skip-apk) SKIP_APK=1 ;; --skip-nginx) SKIP_NGINX=1 ;; --skip-admin) SKIP_ADMIN=1 ;; --only-backend) ONLY_BACKEND=1 ;; -h|--help) sed -n '1,30p' "$0"; exit 0 ;; *) echo "未知参数: $arg"; exit 1 ;; esac done PROJECT_ROOT="/root/project/video" SERVER_DIR="$PROJECT_ROOT/server" APP_DIR="$PROJECT_ROOT/app" ADMIN_DIR="$PROJECT_ROOT/admin" RUNTIME_DIR="$PROJECT_ROOT/runtime" H5_DIR="$PROJECT_ROOT/h5" NGINX_CONF_DIR="/www/server/panel/vhost/nginx" ACME_WEBROOT="/www/wwwroot/acme-challenge" DB_NAME="video" DB_USER="root" DB_PASS="256kb.cn" DB_PORT="3307" log() { echo; echo "▶ $*"; } ok() { echo " ✅ $*"; } warn() { echo " ⚠️ $*"; } die() { echo " ❌ $*"; exit 1; } # --------------------------------------------------------------------------- # 0. 前置检查 # --------------------------------------------------------------------------- log "0. 前置检查" [ -d "$PROJECT_ROOT" ] || die "项目目录不存在:$PROJECT_ROOT" [ -f "$SERVER_DIR/.env" ] || die "缺少 $SERVER_DIR/.env,请参考 docs/04-部署运行文档.md 4.2 节配置" [ -d "$SERVER_DIR/.venv" ] || warn "$SERVER_DIR/.venv 不存在,稍后需要执行 python -m venv .venv && pip install -r requirements.txt" command -v nginx >/dev/null || die "未安装 nginx" command -v docker >/dev/null || die "未安装 docker" command -v mysql >/dev/null || die "未安装 mysql-client" ok "前置检查通过" # --------------------------------------------------------------------------- # 1. 目录结构 # --------------------------------------------------------------------------- log "1. 创建运行目录" mkdir -p "$RUNTIME_DIR"/{uploads,videos,downloads,backups} "$H5_DIR" "$ACME_WEBROOT" chmod 755 "$RUNTIME_DIR" "$RUNTIME_DIR"/{uploads,videos,downloads,backups} ok "目录创建完成" [ "$ONLY_BACKEND" = "1" ] && { SKIP_APK=1; SKIP_NGINX=1; SKIP_ADMIN=1; } # --------------------------------------------------------------------------- # 2. Docker 组件(MySQL / Redis) # --------------------------------------------------------------------------- log "2. 启动 Docker 容器" # 2.1 MySQL 8(容器名 mysql8,端口 3307,数据 /opt/mysql8-data) if ! docker ps -a --format '{{.Names}}' | grep -qx mysql8; then log "2.1 首次创建 mysql8 容器" mkdir -p /opt/mysql8-data docker run -d --name mysql8 \ --restart unless-stopped \ -p 3307:3306 \ -e MYSQL_ROOT_PASSWORD="$DB_PASS" \ -e TZ=Asia/Shanghai \ -v /opt/mysql8-data:/var/lib/mysql \ mysql:8 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci \ --default-authentication-plugin=mysql_native_password ok "mysql8 创建完成" else docker start mysql8 >/dev/null && ok "mysql8 已启动" fi # 2.2 Redis(容器名 my-redis,端口 6379) if ! docker ps -a --format '{{.Names}}' | grep -qx my-redis; then log "2.2 首次创建 my-redis 容器" mkdir -p /home/redis/data /home/redis/conf docker run -d --name my-redis \ --restart unless-stopped \ -p 6379:6379 \ -v /home/redis/data:/data \ -v /home/redis/conf:/usr/local/etc/redis \ redis:latest \ redis-server --appendonly yes ok "my-redis 创建完成" else docker start my-redis >/dev/null && ok "my-redis 已启动" fi # 2.3 等待容器就绪 echo -n " 等待 MySQL 就绪..." for i in $(seq 1 30); do docker exec mysql8 mysqladmin ping -h 127.0.0.1 -uroot -p"$DB_PASS" >/dev/null 2>&1 && { echo " OK"; break; } [ "$i" = "30" ] && die "MySQL 启动超时" sleep 1 done echo -n " 等待 Redis 就绪..." for i in $(seq 1 15); do [ "$(docker exec my-redis redis-cli ping 2>/dev/null)" = "PONG" ] && { echo " OK"; break; } [ "$i" = "15" ] && die "Redis 启动超时" sleep 1 done # 2.4 创建数据库(首次) docker exec mysql8 mysql -uroot -p"$DB_PASS" -e \ "CREATE DATABASE IF NOT EXISTS $DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" \ && ok "数据库 $DB_NAME 已确认存在" # --------------------------------------------------------------------------- # 3. 后端服务(video-api.service) # --------------------------------------------------------------------------- log "3. 部署后端服务" # 3.1 Python 虚拟环境 if [ ! -d "$SERVER_DIR/.venv" ]; then log "3.1 创建虚拟环境" cd "$SERVER_DIR" python3 -m venv .venv .venv/bin/pip install -U pip .venv/bin/pip install -r requirements.txt ok ".venv 安装完成" fi # 3.2 数据库迁移 log "3.2 执行 alembic 迁移" cd "$SERVER_DIR" .venv/bin/alembic upgrade head ok "数据库迁移完成" # 3.3 systemd 单元 log "3.3 写入 systemd 单元" cat > /etc/systemd/system/video-api.service <<'UNIT' [Unit] Description=Video App FastAPI Backend (uvicorn) After=network.target docker.service redis.service Requires=docker.service [Service] Type=simple User=root WorkingDirectory=/root/project/video/server ExecStart=/root/project/video/server/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2 Restart=always RestartSec=3 [Install] WantedBy=multi-user.target UNIT systemctl daemon-reload systemctl enable --now video-api.service sleep 3 systemctl is-active --quiet video-api.service && ok "video-api.service 已启动并设为开机自启" \ || die "video-api.service 启动失败,请 journalctl -u video-api -n 50 查看" # 3.4 后端健康检查 echo -n " 健康检查..." for i in $(seq 1 10); do code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8000/health || true) [ "$code" = "200" ] && { echo " OK ($code)"; break; } [ "$i" = "10" ] && die "后端健康检查失败(10 次尝试后仍非 200)" sleep 2 done # --------------------------------------------------------------------------- # 4. nginx 配置 # --------------------------------------------------------------------------- if [ "$SKIP_NGINX" = "0" ]; then log "4. 写入 nginx 配置(h5 / vm / vapi 三个站点)" # 通用 SSL 配置片段(这里用 heredoc 内嵌,避免每次写三个文件重复) _ssl_block() { local d="$1" cat < "$NGINX_CONF_DIR/h5.256kb.top.conf" < "$NGINX_CONF_DIR/vm.256kb.top.conf" < "$NGINX_CONF_DIR/vapi.256kb.top.conf" </dev/null 2>&1) || true ok "APK 已部署到 $RUNTIME_DIR/downloads/video-app.apk" # 同步 DB 版本号(让老用户收到更新提示) PUBSPEC_VERSION=$(grep -E '^version:' "$APP_DIR/pubspec.yaml" | awk '{print $2}') VERSION_NAME="${PUBSPEC_VERSION%+*}" VERSION_CODE="${PUBSPEC_VERSION##*+}" MIN_CODE=$(mysql -h 127.0.0.1 -u"$DB_USER" -p"$DB_PASS" -P"$DB_PORT" "$DB_NAME" -Nse \ "SELECT JSON_UNQUOTE(JSON_EXTRACT(config_value,'\$.min_version_code')) \ FROM system_configs WHERE config_key='app.version.android';" 2>/dev/null || echo "") MIN_CODE="${MIN_CODE:-1}" UPDATE_JSON="{\"latest_version\":\"$VERSION_NAME\",\"latest_version_code\":$VERSION_CODE,\"min_version_code\":$MIN_CODE,\"update_url\":\"https://h5.256kb.top/downloads/video-app.apk\",\"notice\":\"\"}" mysql -h 127.0.0.1 -u"$DB_USER" -p"$DB_PASS" -P"$DB_PORT" "$DB_NAME" -e " INSERT INTO system_configs (config_key, config_value, description) VALUES ('app.version.android', '$UPDATE_JSON', 'Android 版本检查配置') ON DUPLICATE KEY UPDATE config_value=VALUES(config_value), updated_at=NOW(); " docker exec my-redis redis-cli DEL config:system >/dev/null 2>&1 || true ok "DB 版本号同步:$VERSION_NAME+$VERSION_CODE" fi # --------------------------------------------------------------------------- # 8. 端到端验证 # --------------------------------------------------------------------------- log "8. 端到端验证" echo -n " 后端本地接口..." curl -sf http://127.0.0.1:8000/health >/dev/null && echo " OK" || echo " FAIL" echo -n " vapi 公网接口..." curl -sf https://vapi.256kb.top/api/v1/app/version >/dev/null && echo " OK" || echo " FAIL" echo -n " h5 下载页..." curl -sfI https://h5.256kb.top/downloads/video-app.apk >/dev/null && echo " OK" || echo " FAIL" echo -n " vm 后台首页..." curl -sf https://vm.256kb.top/ >/dev/null && echo " OK" || echo " FAIL" echo echo "============================================================================" echo "✅ 部署完成" echo "============================================================================" echo " 后端: http://127.0.0.1:8000 (systemd: video-api.service)" echo " 后台: https://vm.256kb.top (admin/dist 静态站点)" echo " 接口: https://vapi.256kb.top/api/v1/..." echo " 下载页: https://h5.256kb.top/downloads/video-app.apk" echo echo " 启停: bash $PROJECT_ROOT/project-ctl.sh {start|stop|restart|status}" echo " 日志: journalctl -u video-api -f" echo " 备份: https://vm.256kb.top → 运营管理 → 数据库备份" echo "============================================================================"