#!/usr/bin/env bash set -e # ===== 斗鱼批量登录 Docker 部署脚本 ===== ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$ROOT_DIR" if [ -f "$ROOT_DIR/.env" ]; then set -a # shellcheck disable=SC1091 source "$ROOT_DIR/.env" set +a fi # 使用新的项目标识;可通过环境变量覆盖。 COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-live-hub-py}" export COMPOSE_PROJECT_NAME # 迁移期间优先复用旧项目卷,避免目录改名导致创建空数据库。 MYSQL_VOLUME_NAME="${MYSQL_VOLUME_NAME:-}" MYSQL_VOLUME_EXTERNAL="${MYSQL_VOLUME_EXTERNAL:-false}" if [ -z "$MYSQL_VOLUME_NAME" ] && command -v docker >/dev/null 2>&1 \ && docker volume inspect douyu_login_py_mysql-data >/dev/null 2>&1; then MYSQL_VOLUME_NAME="douyu_login_py_mysql-data" MYSQL_VOLUME_EXTERNAL="true" fi export MYSQL_VOLUME_NAME export MYSQL_VOLUME_EXTERNAL APP_PORT="${APP_PORT:-8000}" # ── 工具函数 ────────────────────────────────────────────── detect_compose() { if docker compose version &> /dev/null; then echo "docker compose" elif command -v docker-compose &> /dev/null; then echo "docker-compose" else echo "" fi } stop_legacy_containers() { local name project for name in douyu-login-db douyu-login; do if ! docker inspect "$name" >/dev/null 2>&1; then continue fi project="$(docker inspect "$name" --format '{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null || true)" if [ -n "$project" ] && [ "$project" != "$COMPOSE_PROJECT_NAME" ]; then echo "停止旧 Compose 项目容器: $name (项目 $project)" docker stop "$name" >/dev/null fi done } # 同一台机器只允许一个部署任务构建/重建,避免两个 deploy.sh 抢缓存和互相覆盖。 acquire_deploy_lock() { local lock_file="${DEPLOY_LOCK_FILE:-/tmp/live-hub-py-deploy.lock}" if ! command -v flock >/dev/null 2>&1; then echo "⚠️ 未安装 flock,跳过部署并发锁" return 0 fi exec 9>"$lock_file" if ! flock -n 9; then echo "❌ 已有部署任务在运行(锁: $lock_file),请稍后再试" exit 1 fi echo " 已获取部署锁: $lock_file" } # 给镜像构建加超时,避免网络卡住时脚本无限挂起。 build_app_image() { local build_timeout="${DEPLOY_BUILD_TIMEOUT:-1800}" echo " 构建超时: ${build_timeout}s" if command -v timeout >/dev/null 2>&1; then timeout "$build_timeout" $COMPOSE build douyu-login || { local code=$? echo "❌ 镜像构建失败或超过 ${build_timeout}s(退出码 ${code})" exit "$code" } else $COMPOSE build douyu-login fi } # 等待 Docker 自身 HEALTHCHECK 通过(同时覆盖 Web 和同容器 Worker)。 wait_for_container_healthy() { local cid status cid="$($COMPOSE ps -q douyu-login 2>/dev/null | head -n 1)" if [ -z "$cid" ]; then return 1 fi for _ in $(seq 1 60); do status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$cid" 2>/dev/null || true)" if [ "$status" = "healthy" ]; then return 0 fi if [ "$status" = "unhealthy" ]; then return 1 fi sleep 2 done return 1 } # ── 子命令 ────────────────────────────────────────────── cmd_deploy() { echo "" echo "==============================" echo " 斗鱼批量登录 - Docker 部署" echo "==============================" acquire_deploy_lock if ! command -v docker &> /dev/null; then echo "❌ 未安装 Docker,请先安装:" echo " curl -fsSL https://get.docker.com | sh" exit 1 fi COMPOSE=$(detect_compose) if [ -z "$COMPOSE" ]; then echo "❌ 未安装 docker compose,请先安装 Docker Compose" exit 1 fi echo " 使用: $COMPOSE" echo "" if [ -z "${YYB_WORKER_KEY:-}" ]; then echo "❌ 请先在 .env 中设置 YYB_WORKER_KEY" exit 1 fi if ! $COMPOSE config --quiet; then echo "❌ Docker Compose 配置无效,请检查 .env 中的必填变量" exit 1 fi # 创建数据和日志目录 mkdir -p data logs # 构建并启动 echo "正在构建应用镜像(首次构建可能较久;有缓存时通常几十秒)..." stop_legacy_containers # 生产部署不构建含大型开发依赖的 test 镜像;测试镜像仅在本地按需构建。 build_app_image echo "" echo "正在启动服务..." # 只处理应用服务,按需重建:镜像或配置变了 compose 才会重建它。 # 曾经这里是不带服务名的 `up -d --force-recreate`,会把 mysql 也一起重建 —— # 每次部署都有约 90 秒的 502(mysql 重启期间应用连不上库)。 # 现在 mysql 只在它自己的配置真的变了时才动,代码部署不再影响数据库。 $COMPOSE up -d douyu-login echo "" echo "等待服务启动..." # 健康检查 # 30 秒不够:mysql 一起重建时应用就绪约 35 秒,会误报「启动超时」。 for i in $(seq 1 40); do if curl -s "http://localhost:${APP_PORT}/api/health" | grep -q "ok" 2>/dev/null; then echo "" echo "服务已响应,等待 Docker 健康检查..." if ! wait_for_container_healthy; then echo "❌ 服务已响应,但容器未进入 healthy 状态,查看日志:" $COMPOSE ps $COMPOSE logs --tail 50 douyu-login exit 1 fi echo "" echo "==============================" echo " ✅ 部署成功!" echo "==============================" echo " 访问地址: http://localhost:${APP_PORT}" echo " 容器健康: healthy" echo " 默认账号见环境变量 ADMIN_USERNAME/ADMIN_PASSWORD(未设置则为 admin/admin123)" echo "" echo " 查看应用日志: ./deploy.sh logs" echo " 查看容器输出: ./deploy.sh logs container" echo " 停止服务: $COMPOSE down" echo " 重启服务: $COMPOSE restart" echo " 重置数据: ./deploy.sh reset" echo "==============================" exit 0 fi echo " 等待中... ($i/40)" sleep 2 done echo "" echo "❌ 服务启动超时,查看日志:" $COMPOSE logs --tail 30 exit 1 } cmd_reset() { echo "" echo "==============================" echo " ⚠️ 重置所有数据" echo "==============================" echo "" echo " 将删除以下内容:" echo " • MySQL 数据卷" echo " • 应用宝 Worker 任务数据(扫码会话、二维码和订单记录)" echo " • 历史Cookie (data/cookies/)" echo " • 应用日志 (logs/)" echo " • Docker容器和镜像" echo "" # 交互确认 read -rp " 确认删除所有数据?输入 YES 继续: " confirm if [ "$confirm" != "YES" ]; then echo " 已取消" exit 0 fi COMPOSE=$(detect_compose) # 停止并删除容器 if [ -n "$COMPOSE" ]; then echo " 停止并删除容器..." $COMPOSE down --volumes --rmi local 2>/dev/null || $COMPOSE down --volumes 2>/dev/null || true fi # 删除数据文件 echo " 删除旧 SQLite 数据..." rm -rf data/web.db data/web.db-shm data/web.db-wal echo " 删除Cookie..." rm -rf data/cookies/ echo " 删除应用宝 Worker 任务..." rm -rf data/yyb-worker-jobs/ data/yyb-worker-dev/ echo " 删除日志..." rm -rf logs/*.log # 删除其他临时文件 rm -rf data/accounts.json data/gui_state.json data/app.db 2>/dev/null || true echo "" echo "==============================" echo " ✅ 数据已重置" echo "==============================" echo " 运行 ./deploy.sh 重新部署" echo "==============================" } cmd_logs() { local mode="${1:-app}" if [ "$mode" = "app" ]; then local app_log app_log="$ROOT_DIR/logs/app-$(date '+%Y-%m-%d').log" mkdir -p "$ROOT_DIR/logs" echo "跟踪应用文件日志: ${app_log}(Ctrl+C 退出)" exec tail -n "${LOG_TAIL_LINES:-200}" -F "$app_log" fi if [ "$mode" != "container" ]; then echo "用法: ./deploy.sh logs [app|container]" exit 1 fi COMPOSE=$(detect_compose) if [ -z "$COMPOSE" ]; then echo "❌ 未检测到 docker compose" exit 1 fi $COMPOSE logs --tail "${LOG_TAIL_LINES:-200}" -f } cmd_migrate_mysql() { COMPOSE=$(detect_compose) if [ -z "$COMPOSE" ]; then echo "❌ 未检测到 docker compose" exit 1 fi if [ ! -f data/web.db ]; then echo "❌ 未找到 data/web.db,无法迁移 SQLite 数据" exit 1 fi echo "正在启动 MySQL..." stop_legacy_containers $COMPOSE up -d mysql echo "等待 MySQL 就绪..." for i in $(seq 1 30); do if $COMPOSE exec -T mysql sh -c 'mysqladmin ping -h localhost -uroot -p"$MYSQL_ROOT_PASSWORD" --silent' >/dev/null 2>&1; then echo "MySQL 已就绪" break fi if [ "$i" = "30" ]; then echo "❌ MySQL 启动超时,查看日志:" $COMPOSE logs --tail 50 mysql exit 1 fi sleep 2 done echo "正在构建迁移运行镜像..." $COMPOSE build douyu-login echo "开始迁移 SQLite 到 MySQL..." $COMPOSE run --rm --no-deps douyu-login \ python scripts/migrate_sqlite_to_mysql.py --source /app/data/web.db } cmd_migrate() { COMPOSE=$(detect_compose) if [ -z "$COMPOSE" ]; then echo "❌ 未检测到 docker compose" exit 1 fi echo "正在启动 MySQL..." stop_legacy_containers $COMPOSE up -d mysql echo "等待 MySQL 就绪..." for i in $(seq 1 30); do if $COMPOSE exec -T mysql sh -c 'mysqladmin ping -h localhost -uroot -p"$MYSQL_ROOT_PASSWORD" --silent' >/dev/null 2>&1; then break fi if [ "$i" = "30" ]; then echo "❌ MySQL 启动超时,查看日志:" $COMPOSE logs --tail 50 mysql exit 1 fi sleep 2 done echo "正在执行数据库迁移..." echo "正在构建最新应用镜像..." $COMPOSE build douyu-login $COMPOSE run --rm --no-deps douyu-login \ python -c 'from web.backend.database import run_migrations; run_migrations()' echo "✅ 数据库迁移完成" } cmd_stop() { COMPOSE=$(detect_compose) if [ -z "$COMPOSE" ]; then echo "❌ 未检测到 docker compose" exit 1 fi $COMPOSE down echo "✅ 服务已停止" } cmd_restart() { COMPOSE=$(detect_compose) if [ -z "$COMPOSE" ]; then echo "❌ 未检测到 docker compose" exit 1 fi stop_legacy_containers # 重建镜像和应用容器,使新迁移文件与入口迁移逻辑都能生效。 $COMPOSE up -d --build --force-recreate douyu-login echo "✅ 服务已重建并重启(已自动检查数据库迁移)" } cmd_dev() { exec "$ROOT_DIR/dev.sh" } # ── 帮助 ────────────────────────────────────────────────── cmd_help() { echo "" echo "==============================" echo " 斗鱼批量登录 - 部署工具" echo "==============================" echo "" echo " 用法: ./deploy.sh <命令>" echo "" echo " 命令:" echo " (无参数) 构建并部署" echo " reset 删除所有数据并重置(需确认)" echo " migrate-mysql 先启动 MySQL 并迁移 data/web.db" echo " migrate 仅执行数据库结构迁移" echo " logs [app|container] 查看应用文件日志(默认)或容器输出" echo " stop 停止服务" echo " restart 重启服务" echo " dev 本地调试启动(后端 reload + 前端热更新)" echo " help 显示帮助" echo "" echo " 访问地址: http://localhost:${APP_PORT}" echo " 调试地址: http://localhost:5174" echo " 默认账号见环境变量 ADMIN_USERNAME/ADMIN_PASSWORD" echo "" } # ── 入口 ────────────────────────────────────────────────── case "${1:-}" in reset) cmd_reset ;; migrate-mysql) cmd_migrate_mysql ;; migrate) cmd_migrate ;; logs) shift; cmd_logs "${1:-app}" ;; stop) cmd_stop ;; restart) cmd_restart ;; dev) cmd_dev ;; help|-h) cmd_help ;; *) cmd_deploy ;; esac