#!/usr/bin/env bash set -euo pipefail # ===== 本地调试启动脚本:后端 reload + 前端热更新 ===== ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$ROOT_DIR" if [ -f "$ROOT_DIR/.env" ]; then set -a # 调试模式复用 Docker 部署的环境变量,尤其是 JWT 和敏感字段加密密钥。 # shellcheck disable=SC1091 source "$ROOT_DIR/.env" set +a fi BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}" BACKEND_PORT="${BACKEND_PORT:-8800}" FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}" FRONTEND_PORT="${FRONTEND_PORT:-5174}" LOG_LEVEL="${LOG_LEVEL:-DEBUG}" MYSQL_IMAGE="${MYSQL_IMAGE:-mysql:8.4}" MYSQL_BIND_HOST="${MYSQL_BIND_HOST:-127.0.0.1}" MYSQL_HOST_PORT="${MYSQL_HOST_PORT:-3307}" MYSQL_DATABASE="${MYSQL_DATABASE:-douyu_login}" MYSQL_USER="${MYSQL_USER:-douyu_login}" DEV_DB_HOST="${DEV_DB_HOST:-127.0.0.1}" DEV_DB_PORT="${DEV_DB_PORT:-$MYSQL_HOST_PORT}" DB_HOST="$DEV_DB_HOST" DB_PORT="$DEV_DB_PORT" DB_NAME="${DB_NAME:-$MYSQL_DATABASE}" DB_USER="${DB_USER:-$MYSQL_USER}" DB_PASSWORD="${DB_PASSWORD:-${MYSQL_PASSWORD:-}}" DB_POOL_SIZE="${DB_POOL_SIZE:-20}" DB_MAX_OVERFLOW="${DB_MAX_OVERFLOW:-20}" DB_POOL_TIMEOUT="${DB_POOL_TIMEOUT:-30}" DB_POOL_RECYCLE="${DB_POOL_RECYCLE:-1800}" YYB_WORKER_PORT="${YYB_WORKER_PORT:-8810}" YYB_WORKER_IMAGE="${YYB_WORKER_IMAGE:-douyu-yyb-worker:local}" DEV_YYB_WORKER_URL="${DEV_YYB_WORKER_URL:-http://127.0.0.1:${YYB_WORKER_PORT}}" YYB_WORKER_CONTAINER="${YYB_WORKER_CONTAINER:-douyu-yyb-worker-dev}" YYB_WORKER_DATA_DIR="${YYB_WORKER_DATA_DIR:-$ROOT_DIR/data/yyb-worker-dev}" LOG_DIR="$ROOT_DIR/logs" LOG_DATE="$(date +%Y-%m-%d)" BACKEND_LOG="$LOG_DIR/backend-$LOG_DATE.log" FRONTEND_LOG="$LOG_DIR/frontend-$LOG_DATE.log" WORKER_LOG="$LOG_DIR/yyb-worker-$LOG_DATE.log" BACKEND_PID="" FRONTEND_PID="" WORKER_LOG_PID="" WORKER_STARTED_BY_DEV=false stop_tree() { local pid="$1" local children if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then return fi children="$(pgrep -P "$pid" 2>/dev/null || true)" for child in $children; do stop_tree "$child" done kill "$pid" 2>/dev/null || true } cleanup() { trap - EXIT INT TERM stop_tree "$FRONTEND_PID" stop_tree "$BACKEND_PID" stop_tree "$WORKER_LOG_PID" if [ "$WORKER_STARTED_BY_DEV" = true ]; then docker stop "$YYB_WORKER_CONTAINER" >/dev/null 2>&1 || true fi } require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "缺少命令: $1" exit 1 fi } require_cmd uv require_cmd npm require_cmd docker detect_compose() { if docker compose version >/dev/null 2>&1; then echo "docker compose" elif command -v docker-compose >/dev/null 2>&1; then echo "docker-compose" else echo "" fi } detect_backend_proxy_host() { if [ "$BACKEND_HOST" != "0.0.0.0" ] && [ "$BACKEND_HOST" != "::" ]; then echo "$BACKEND_HOST" return fi # 绑定 0.0.0.0 时,Vite 代理不要固定连 127.0.0.1;某些本地工具会抢占 loopback 端口。 local ip ip="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)" if [ -z "$ip" ] && command -v hostname >/dev/null 2>&1; then ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)" fi echo "${ip:-127.0.0.1}" } if [ -n "${DATABASE_URL:-}" ] && [[ "$DATABASE_URL" != mysql+* ]]; then echo "dev.sh 只支持 MySQL,请移除 SQLite DATABASE_URL: $DATABASE_URL" exit 1 fi if [ -z "${MYSQL_PASSWORD:-}" ] || [ -z "${MYSQL_ROOT_PASSWORD:-}" ]; then echo "请先在 .env 中设置 MYSQL_PASSWORD 和 MYSQL_ROOT_PASSWORD" exit 1 fi if [ -z "$DB_PASSWORD" ]; then echo "请先在 .env 中设置 DB_PASSWORD 或 MYSQL_PASSWORD" exit 1 fi if [ -z "${YYB_WORKER_KEY:-}" ]; then echo "请先在 .env 中设置 YYB_WORKER_KEY" exit 1 fi COMPOSE=$(detect_compose) if [ -z "$COMPOSE" ]; then echo "缺少 docker compose" exit 1 fi BACKEND_PROXY_HOST="${BACKEND_PROXY_HOST:-$(detect_backend_proxy_host)}" BACKEND_PROXY_TARGET="${VITE_BACKEND_TARGET:-http://${BACKEND_PROXY_HOST}:${BACKEND_PORT}}" mkdir -p data "$LOG_DIR" mkdir -p "$YYB_WORKER_DATA_DIR" trap cleanup EXIT INT TERM start_yyb_worker() { if docker ps --format '{{.Names}}' | grep -qx "$YYB_WORKER_CONTAINER"; then echo "本地 YYB Worker 已运行: ${DEV_YYB_WORKER_URL}" return fi if docker ps -a --format '{{.Names}}' | grep -qx "$YYB_WORKER_CONTAINER"; then echo "发现已停止的本地 YYB Worker,正在清理旧容器..." docker rm "$YYB_WORKER_CONTAINER" >/dev/null fi echo "正在构建本地 YYB Worker..." YYB_WORKER_KEY="$YYB_WORKER_KEY" $COMPOSE build yyb-worker echo "正在启动本地 YYB Worker..." docker run -d --rm \ --name "$YYB_WORKER_CONTAINER" \ -p "127.0.0.1:${YYB_WORKER_PORT}:8810" \ -e TZ="${TZ:-Asia/Shanghai}" \ -e YYB_WORKER_KEY="$YYB_WORKER_KEY" \ -v "$YYB_WORKER_DATA_DIR:/app/config/worker-jobs" \ "$YYB_WORKER_IMAGE" >/dev/null WORKER_STARTED_BY_DEV=true echo "等待本地 YYB Worker 就绪..." for i in $(seq 1 15); do if curl -fsS "${DEV_YYB_WORKER_URL}/health" >/dev/null 2>&1; then echo "YYB Worker 已就绪" return fi if [ "$i" = "15" ]; then echo "YYB Worker 启动超时,查看日志:" docker logs "$YYB_WORKER_CONTAINER" || true exit 1 fi sleep 1 done } stream_yyb_worker_logs() { # docker logs 默认只输出到终端;持续转存后保留 Worker 的异常和协议脚本输出。 (docker logs --tail 100 --follow "$YYB_WORKER_CONTAINER" 2>&1 | tee -a "$WORKER_LOG") & WORKER_LOG_PID=$! } start_yyb_worker stream_yyb_worker_logs echo "正在启动本地 MySQL..." MYSQL_IMAGE="$MYSQL_IMAGE" \ MYSQL_BIND_HOST="$MYSQL_BIND_HOST" \ MYSQL_HOST_PORT="$MYSQL_HOST_PORT" \ $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 "" echo "==============================" echo " 本地调试模式" echo "==============================" echo " 后端: http://${BACKEND_HOST}:${BACKEND_PORT}" echo " 前端: http://localhost:${FRONTEND_PORT}" echo " API 代理: ${BACKEND_PROXY_TARGET}" echo " MySQL: ${DB_HOST}:${DB_PORT}/${DB_NAME}" echo " YYB Worker: ${DEV_YYB_WORKER_URL}" echo " 后端日志: ${BACKEND_LOG}" echo " 前端日志: ${FRONTEND_LOG}" echo " Worker 日志: ${WORKER_LOG}" echo " 退出: Ctrl+C" echo "==============================" echo "" ( export DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD export DB_POOL_SIZE DB_MAX_OVERFLOW DB_POOL_TIMEOUT DB_POOL_RECYCLE export YYB_WORKER_KEY LOG_LEVEL export YYB_WORKER_URL="$DEV_YYB_WORKER_URL" uv run uvicorn web.backend.main:app \ --host "$BACKEND_HOST" \ --port "$BACKEND_PORT" \ --reload \ --reload-dir core \ --reload-dir utils \ --reload-dir web/backend 2>&1 | tee -a "$BACKEND_LOG" ) & BACKEND_PID=$! ( cd web/frontend VITE_BACKEND_TARGET="$BACKEND_PROXY_TARGET" \ npm run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT" 2>&1 | tee -a "$FRONTEND_LOG" ) & FRONTEND_PID=$! while true; do if ! kill -0 "$BACKEND_PID" 2>/dev/null; then wait "$BACKEND_PID" exit $? fi if ! kill -0 "$FRONTEND_PID" 2>/dev/null; then wait "$FRONTEND_PID" exit $? fi sleep 1 done