359 lines
11 KiB
Bash
Executable File
359 lines
11 KiB
Bash
Executable File
#!/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}"
|
||
# 8800 是项目默认开发端口并由脚本独占;其他显式端口发生冲突时只报错。
|
||
BACKEND_PORT_EXPLICIT=false
|
||
if [ -n "${BACKEND_PORT+x}" ]; then
|
||
BACKEND_PORT_EXPLICIT=true
|
||
fi
|
||
BACKEND_PORT="${BACKEND_PORT:-8800}"
|
||
FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}"
|
||
FRONTEND_PORT="${FRONTEND_PORT:-5174}"
|
||
LOG_LEVEL="${LOG_LEVEL:-INFO}"
|
||
MYSQL_IMAGE="${MYSQL_IMAGE:-docker.m.daocloud.io/library/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}"
|
||
DEV_YYB_WORKER_URL="${DEV_YYB_WORKER_URL:-http://127.0.0.1:${YYB_WORKER_PORT}}"
|
||
YYB_WORKER_DATA_DIR="${YYB_WORKER_DATA_DIR:-$ROOT_DIR/data/yyb-worker-dev}"
|
||
YYB_WORKER_NODE_MODULES="$ROOT_DIR/services/yyb-worker/node_modules"
|
||
LOG_DIR="$ROOT_DIR/logs"
|
||
APP_LOG="$LOG_DIR/app-$(date '+%Y-%m-%d').log"
|
||
|
||
# 本地一键启动始终写入项目内的持久化日志目录,避免与旧的 /Users/.../app.log 混淆。
|
||
if [ "${1:-}" = "logs" ]; then
|
||
mkdir -p "$LOG_DIR"
|
||
echo "跟踪本地应用日志: ${APP_LOG}(Ctrl+C 退出)"
|
||
exec tail -n "${LOG_TAIL_LINES:-200}" -F "$APP_LOG"
|
||
fi
|
||
|
||
# 本地回归测试:仅依赖 uv,不需要 MySQL / .env 配置。
|
||
# --group dev 确保 pytest 等 dev 依赖已安装。
|
||
if [ "${1:-}" = "test" ]; then
|
||
shift
|
||
exec uv run --group dev pytest "$@"
|
||
fi
|
||
|
||
if [ "${1:-}" = "format" ]; then
|
||
shift
|
||
exec uv run --group dev ruff format "$@"
|
||
fi
|
||
|
||
if [ "${1:-}" = "format-check" ]; then
|
||
shift
|
||
exec uv run --group dev ruff format --check "$@"
|
||
fi
|
||
|
||
BACKEND_PID=""
|
||
FRONTEND_PID=""
|
||
WORKER_PID=""
|
||
|
||
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_PID"
|
||
}
|
||
|
||
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
|
||
# Vite 是本机进程,回环代理最稳定;LAN 客户端仍可经 Vite 的 0.0.0.0 监听访问。
|
||
echo "127.0.0.1"
|
||
}
|
||
|
||
port_in_use() {
|
||
command -v lsof >/dev/null 2>&1 && \
|
||
lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1
|
||
}
|
||
|
||
show_port_owner() {
|
||
lsof -nP -iTCP:"$1" -sTCP:LISTEN 2>/dev/null | awk 'NR == 2 {print " 占用进程: " $1 " (pid " $2 ")"}'
|
||
}
|
||
|
||
ensure_backend_port_available() {
|
||
if ! port_in_use "$BACKEND_PORT"; then
|
||
return
|
||
fi
|
||
# 本地开发默认端口由本项目独占;编辑器残留的端口转发会让健康检查命中错误进程。
|
||
if [ "$BACKEND_PORT" = "8800" ]; then
|
||
local pids pid
|
||
pids="$(lsof -tiTCP:"$BACKEND_PORT" -sTCP:LISTEN 2>/dev/null || true)"
|
||
echo "默认后端端口 $BACKEND_PORT 已被占用,正在停止占用进程..."
|
||
show_port_owner "$BACKEND_PORT"
|
||
for pid in $pids; do
|
||
kill -TERM "$pid" 2>/dev/null || true
|
||
done
|
||
for _ in $(seq 1 5); do
|
||
if ! port_in_use "$BACKEND_PORT"; then
|
||
echo "端口 $BACKEND_PORT 已释放。"
|
||
return
|
||
fi
|
||
sleep 1
|
||
done
|
||
for pid in $pids; do
|
||
kill -KILL "$pid" 2>/dev/null || true
|
||
done
|
||
sleep 1
|
||
if ! port_in_use "$BACKEND_PORT"; then
|
||
echo "端口 $BACKEND_PORT 已释放。"
|
||
return
|
||
fi
|
||
echo "端口 $BACKEND_PORT 仍被占用,未启动后端。"
|
||
show_port_owner "$BACKEND_PORT"
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$BACKEND_PORT_EXPLICIT" = true ]; then
|
||
echo "后端端口 $BACKEND_PORT 已被占用,无法启动。"
|
||
show_port_owner "$BACKEND_PORT"
|
||
echo "请关闭占用程序,或在 .env 中设置一个空闲的 BACKEND_PORT 后重试。"
|
||
exit 1
|
||
fi
|
||
echo "后端端口 $BACKEND_PORT 已被占用,无法启动。"
|
||
show_port_owner "$BACKEND_PORT"
|
||
echo "请在 .env 中设置空闲的 BACKEND_PORT 后重试。"
|
||
exit 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
|
||
|
||
ensure_backend_port_available
|
||
|
||
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() {
|
||
# Worker 跟随 dev.sh 生命周期:发现遗留进程(外部启动)时先停止,再自启,
|
||
# 确保运行的是当前代码且 Ctrl+C 时一并退出。
|
||
if curl -fsS "${DEV_YYB_WORKER_URL}/health" >/dev/null 2>&1; then
|
||
local leaked
|
||
leaked="$(lsof -ti tcp:"$YYB_WORKER_PORT" -sTCP:LISTEN 2>/dev/null | head -1 || true)"
|
||
if [ -n "$leaked" ]; then
|
||
echo "发现遗留 YYB Worker(pid $leaked),正在停止并重新启动..."
|
||
kill "$leaked" 2>/dev/null || true
|
||
sleep 1
|
||
fi
|
||
fi
|
||
if [ ! -d "$YYB_WORKER_NODE_MODULES/jsdom" ]; then
|
||
echo "正在安装本地 YYB Worker Node 依赖..."
|
||
(cd "$ROOT_DIR/services/yyb-worker" && npm ci)
|
||
fi
|
||
|
||
echo "正在启动本地 YYB Worker..."
|
||
(
|
||
cd "$ROOT_DIR/services/yyb-worker/runtime"
|
||
YYB_WORKER_KEY="$YYB_WORKER_KEY" \
|
||
uv run --project "$ROOT_DIR" python scripts/yyb-worker.py \
|
||
--host 127.0.0.1 \
|
||
--port "$YYB_WORKER_PORT" \
|
||
--data-dir "$YYB_WORKER_DATA_DIR"
|
||
) &
|
||
WORKER_PID=$!
|
||
|
||
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 启动超时,查看日志:"
|
||
echo "请查看当前终端中的 Worker 输出。"
|
||
exit 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
}
|
||
|
||
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 "正在执行数据库迁移..."
|
||
(
|
||
export DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD
|
||
uv run python -c 'from web.backend.database import run_migrations; run_migrations()'
|
||
)
|
||
echo "数据库迁移完成"
|
||
|
||
start_yyb_worker
|
||
|
||
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 " 数据库迁移: 已检查至最新版本"
|
||
echo " 应用日志: ${APP_LOG}"
|
||
echo " 跟踪日志: ./dev.sh logs"
|
||
echo " 前端与 Worker 输出: 当前终端"
|
||
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 LOG_DIR
|
||
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
|
||
) &
|
||
BACKEND_PID=$!
|
||
|
||
echo "等待本地后端就绪..."
|
||
for i in $(seq 1 20); do
|
||
if curl -fsS --max-time 2 "http://127.0.0.1:${BACKEND_PORT}/api/health" 2>/dev/null | grep -q '"status":"ok"'; then
|
||
echo "后端已就绪,日志持续写入: $APP_LOG"
|
||
break
|
||
fi
|
||
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||
echo "后端启动失败,最近应用日志:"
|
||
tail -n 80 "$APP_LOG" 2>/dev/null || true
|
||
wait "$BACKEND_PID" || true
|
||
exit 1
|
||
fi
|
||
if [ "$i" = "20" ]; then
|
||
echo "后端启动超时,最近应用日志:"
|
||
tail -n 80 "$APP_LOG" 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
(
|
||
cd web/frontend
|
||
VITE_BACKEND_TARGET="$BACKEND_PROXY_TARGET" \
|
||
npm run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT"
|
||
) &
|
||
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
|