#!/usr/bin/env bash # 一键启动前后端开发服务 set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PIDS=() log() { echo ">>> $*"; } # 加载仓库根目录 .env(source 后可供 PORT / FRONTEND_PORT 使用) if [[ -f "$ROOT/.env" ]]; then set -a # shellcheck disable=SC1091 source "$ROOT/.env" set +a log "已加载 $ROOT/.env" elif [[ -f "$ROOT/.env.example" ]]; then log "未找到 .env,可执行: cp .env.example .env" fi BACKEND_PORT="${PORT:-8080}" FRONTEND_PORT="${FRONTEND_PORT:-5173}" POSTGRES_USER="${POSTGRES_USER:-affiliate}" POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-affiliate_dev_password}" POSTGRES_DB="${POSTGRES_DB:-affiliate_dash}" POSTGRES_PORT="${POSTGRES_PORT:-5432}" POSTGRES_IMAGE="${POSTGRES_IMAGE:-docker.m.daocloud.io/library/postgres:16-alpine}" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-affiliate_dash}" POSTGRES_DATA_VOLUME="${POSTGRES_DATA_VOLUME:-affiliate_dash_postgres_data}" LEGACY_DEV_DB_CONTAINER="${DEV_DB_CONTAINER:-affiliate_dash_postgres_dev}" TZ="${TZ:-Asia/Shanghai}" DATABASE_URL="${DATABASE_URL:-postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable&TimeZone=${TZ}}" export DATABASE_URL export TZ export COMPOSE_PROJECT_NAME export POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB POSTGRES_PORT POSTGRES_IMAGE POSTGRES_DATA_VOLUME # 结束指定端口上的进程(避免残留占用) free_port() { local port=$1 local pids pids=$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true) if [[ -n "${pids}" ]]; then log "端口 ${port} 已被占用,正在释放: ${pids}" # shellcheck disable=SC2086 kill ${pids} 2>/dev/null || true sleep 0.5 pids=$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true) if [[ -n "${pids}" ]]; then # shellcheck disable=SC2086 kill -9 ${pids} 2>/dev/null || true fi fi } docker_compose_available() { command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1 } postgres_port_reachable() { (echo >"/dev/tcp/127.0.0.1/${POSTGRES_PORT}") >/dev/null 2>&1 } legacy_postgres_container_exists() { docker ps -a --format '{{.Names}}' | grep -qx "$LEGACY_DEV_DB_CONTAINER" } legacy_postgres_container_running() { docker ps --format '{{.Names}}' | grep -qx "$LEGACY_DEV_DB_CONTAINER" } compose_postgres_running() { docker compose ps --services --status running 2>/dev/null | grep -qx "postgres" } prepare_postgres_volume() { local legacy_volume="${LEGACY_DEV_DB_CONTAINER}-data" if [[ "${POSTGRES_DATA_VOLUME}" == "affiliate_dash_postgres_data" ]] && docker volume inspect "$legacy_volume" >/dev/null 2>&1; then POSTGRES_DATA_VOLUME="$legacy_volume" export POSTGRES_DATA_VOLUME log "检测到旧开发库数据卷,Compose 将继续复用: ${POSTGRES_DATA_VOLUME}" fi } stop_legacy_postgres_container() { if legacy_postgres_container_exists; then if legacy_postgres_container_running; then log "检测到旧 PostgreSQL 开发容器,停止后交由 Docker Compose 管理: ${LEGACY_DEV_DB_CONTAINER}" docker stop "$LEGACY_DEV_DB_CONTAINER" >/dev/null else log "检测到旧 PostgreSQL 开发容器已停止,保留容器和数据卷: ${LEGACY_DEV_DB_CONTAINER}" fi fi } ensure_postgres() { if ! docker_compose_available; then log "未找到 Docker Compose,请确认本机 PostgreSQL 已可通过 DATABASE_URL 访问" return 0 fi prepare_postgres_volume stop_legacy_postgres_container if compose_postgres_running; then log "PostgreSQL Compose 服务已运行: ${COMPOSE_PROJECT_NAME}/postgres" else if postgres_port_reachable; then log "检测到本机 PostgreSQL 端口已可访问: ${POSTGRES_PORT},跳过 Compose 数据库启动" return 0 fi log "启动 PostgreSQL Compose 服务: ${COMPOSE_PROJECT_NAME}/postgres" docker compose up -d postgres >/dev/null fi for i in $(seq 1 40); do if docker compose exec -T postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" >/dev/null 2>&1; then log "PostgreSQL 已就绪" return 0 fi sleep 0.5 done echo "错误: PostgreSQL 启动超时" docker compose logs postgres 2>/dev/null || true exit 1 } cleanup() { trap - INT TERM EXIT echo "" log "正在停止前后端..." for pid in "${PIDS[@]:-}"; do if kill -0 "$pid" 2>/dev/null; then # 结束子进程树 pkill -P "$pid" 2>/dev/null || true kill "$pid" 2>/dev/null || true fi done free_port "$BACKEND_PORT" free_port "$FRONTEND_PORT" wait 2>/dev/null || true log "已全部停止" exit 0 } trap cleanup INT TERM # 检查依赖 if ! command -v go >/dev/null 2>&1; then echo "错误: 未找到 go,请先安装 Go" exit 1 fi if ! command -v npm >/dev/null 2>&1; then echo "错误: 未找到 npm,请先安装 Node.js" exit 1 fi # 首次安装前端依赖 if [[ ! -d "$ROOT/frontend/node_modules" ]]; then log "安装前端依赖..." (cd "$ROOT/frontend" && npm install) fi free_port "$BACKEND_PORT" free_port "$FRONTEND_PORT" ensure_postgres log "启动后端 http://localhost:${BACKEND_PORT}" ( cd "$ROOT/backend" export PORT="$BACKEND_PORT" go run ./cmd/server ) & PIDS+=($!) # 等后端健康检查就绪(最多约 15 秒) for i in $(seq 1 30); do if curl -sf "http://127.0.0.1:${BACKEND_PORT}/health" >/dev/null 2>&1; then log "后端已就绪" break fi if ! kill -0 "${PIDS[0]}" 2>/dev/null; then echo "错误: 后端启动失败" cleanup fi sleep 0.5 done log "启动前端 http://localhost:${FRONTEND_PORT}" ( cd "$ROOT/frontend" npm run dev -- --host --port "$FRONTEND_PORT" ) & PIDS+=($!) echo "" log "========================================" log " 游戏皮肤分销系统 已启动" log " 前端: http://localhost:${FRONTEND_PORT}" log " 后端: http://localhost:${BACKEND_PORT}" log " 账号: admin / admin123" log " 按 Ctrl+C 停止全部服务" log "========================================" echo "" # 任一子进程退出则整体清理 wait -n 2>/dev/null || wait cleanup