245 lines
7.0 KiB
Bash
245 lines
7.0 KiB
Bash
|
|
#!/bin/bash
|
|||
|
|
#
|
|||
|
|
# Cosmo 开发环境一键启动脚本
|
|||
|
|
#
|
|||
|
|
# 先检查运行环境(Python / Node / 依赖 / 配置 / 数据库 / Redis / 端口),
|
|||
|
|
# 检查通过后启动:
|
|||
|
|
# - 后端 FastAPI http://localhost:8000 (API 前缀 /api)
|
|||
|
|
# - 前端 Vite http://localhost:5173
|
|||
|
|
#
|
|||
|
|
# 用法:
|
|||
|
|
# ./scripts/run.sh # 检查环境并启动前后端(Ctrl+C 停止)
|
|||
|
|
# ./scripts/run.sh --check # 只检查环境,不启动服务
|
|||
|
|
|
|||
|
|
set -euo pipefail
|
|||
|
|
|
|||
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||
|
|
BACKEND_DIR="$PROJECT_ROOT/backend"
|
|||
|
|
FRONTEND_DIR="$PROJECT_ROOT/frontend"
|
|||
|
|
|
|||
|
|
BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}"
|
|||
|
|
BACKEND_PORT="${BACKEND_PORT:-8000}"
|
|||
|
|
FRONTEND_PORT="${PORT:-5173}"
|
|||
|
|
|
|||
|
|
RED='\033[0;31m'
|
|||
|
|
GREEN='\033[0;32m'
|
|||
|
|
YELLOW='\033[1;33m'
|
|||
|
|
BLUE='\033[0;34m'
|
|||
|
|
NC='\033[0m'
|
|||
|
|
|
|||
|
|
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|||
|
|
ok() { echo -e "${GREEN}[ OK ]${NC} $1"; }
|
|||
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|||
|
|
fail() { echo -e "${RED}[FAIL]${NC} $1"; exit 1; }
|
|||
|
|
|
|||
|
|
CHECK_ONLY=0
|
|||
|
|
if [ "${1:-}" = "--check" ] || [ "${1:-}" = "-c" ]; then
|
|||
|
|
CHECK_ONLY=1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
print_header() {
|
|||
|
|
echo "================================================================="
|
|||
|
|
echo " Cosmo 开发环境启动脚本"
|
|||
|
|
echo " 项目目录: $PROJECT_ROOT"
|
|||
|
|
echo "================================================================="
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------- 后端环境
|
|||
|
|
|
|||
|
|
PYTHON=""
|
|||
|
|
|
|||
|
|
check_backend_env() {
|
|||
|
|
info "检查后端环境..."
|
|||
|
|
|
|||
|
|
if [ -x "$BACKEND_DIR/venv/bin/python" ]; then
|
|||
|
|
PYTHON="$BACKEND_DIR/venv/bin/python"
|
|||
|
|
else
|
|||
|
|
PYTHON="$(command -v python3 || true)"
|
|||
|
|
[ -n "$PYTHON" ] || fail "未找到 python3,请先安装 Python 3.11+"
|
|||
|
|
info "未找到 backend/venv,正在创建虚拟环境..."
|
|||
|
|
"$PYTHON" -m venv "$BACKEND_DIR/venv" || fail "创建虚拟环境失败"
|
|||
|
|
PYTHON="$BACKEND_DIR/venv/bin/python"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
"$PYTHON" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)' \
|
|||
|
|
|| fail "Python 版本过低(需 3.11+): $("$PYTHON" --version 2>&1)"
|
|||
|
|
ok "Python: $("$PYTHON" --version 2>&1) ($PYTHON)"
|
|||
|
|
|
|||
|
|
if ! "$PYTHON" -c 'import fastapi, uvicorn, sqlalchemy, asyncpg' &> /dev/null; then
|
|||
|
|
info "后端依赖不完整,正在安装 requirements.txt..."
|
|||
|
|
"$PYTHON" -m pip install -r "$BACKEND_DIR/requirements.txt" || fail "依赖安装失败"
|
|||
|
|
fi
|
|||
|
|
ok "后端依赖已就绪"
|
|||
|
|
|
|||
|
|
if [ ! -f "$BACKEND_DIR/.env" ]; then
|
|||
|
|
if [ -f "$BACKEND_DIR/.env.example" ]; then
|
|||
|
|
cp "$BACKEND_DIR/.env.example" "$BACKEND_DIR/.env"
|
|||
|
|
warn "已从 .env.example 生成 backend/.env,请核对数据库等配置"
|
|||
|
|
else
|
|||
|
|
fail "缺少 backend/.env,且未找到 backend/.env.example"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
ok "配置文件: backend/.env"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------- 前端环境
|
|||
|
|
|
|||
|
|
check_frontend_env() {
|
|||
|
|
info "检查前端环境..."
|
|||
|
|
|
|||
|
|
command -v node &> /dev/null || fail "未找到 node,请先安装 Node.js 20+"
|
|||
|
|
local node_major
|
|||
|
|
node_major="$(node -p 'process.versions.node.split(".")[0]')"
|
|||
|
|
if [ "$node_major" -lt 20 ]; then
|
|||
|
|
fail "Node.js 版本过低(需 20+): $(node --version)"
|
|||
|
|
fi
|
|||
|
|
ok "Node.js: $(node --version)"
|
|||
|
|
|
|||
|
|
command -v yarn &> /dev/null || fail "未找到 yarn,请先安装 Yarn"
|
|||
|
|
ok "Yarn: $(yarn --version)"
|
|||
|
|
|
|||
|
|
if [ ! -d "$FRONTEND_DIR/node_modules" ]; then
|
|||
|
|
info "未找到 frontend/node_modules,正在安装前端依赖..."
|
|||
|
|
(cd "$FRONTEND_DIR" && yarn install --ignore-engines) || fail "前端依赖安装失败"
|
|||
|
|
fi
|
|||
|
|
ok "前端依赖已就绪"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ------------------------------------------------------- 数据库 / Redis / 端口
|
|||
|
|
|
|||
|
|
check_services() {
|
|||
|
|
info "检查数据库与 Redis 连接..."
|
|||
|
|
|
|||
|
|
local output status
|
|||
|
|
set +e
|
|||
|
|
output="$(cd "$BACKEND_DIR" && "$PYTHON" - <<'PY' 2>&1
|
|||
|
|
import socket
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
from app.config import settings
|
|||
|
|
|
|||
|
|
|
|||
|
|
def reachable(host: str, port: int, timeout: float = 3.0) -> bool:
|
|||
|
|
try:
|
|||
|
|
with socket.create_connection((host, port), timeout=timeout):
|
|||
|
|
return True
|
|||
|
|
except OSError:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
database_ok = reachable(settings.database_host, settings.database_port)
|
|||
|
|
redis_ok = reachable(settings.redis_host, settings.redis_port)
|
|||
|
|
|
|||
|
|
print(f"database|{settings.database_host}:{settings.database_port}|{'ok' if database_ok else 'fail'}")
|
|||
|
|
print(f"redis|{settings.redis_host}:{settings.redis_port}|{'ok' if redis_ok else 'fail'}")
|
|||
|
|
|
|||
|
|
sys.exit(0 if database_ok else 1)
|
|||
|
|
PY
|
|||
|
|
)"
|
|||
|
|
status=$?
|
|||
|
|
set -e
|
|||
|
|
|
|||
|
|
echo "$output" | sed -u 's/^/ /'
|
|||
|
|
|
|||
|
|
[ "$status" -eq 0 ] || fail "无法连接 PostgreSQL,请先启动数据库(如 docker compose up -d postgres redis)"
|
|||
|
|
ok "PostgreSQL 连接正常"
|
|||
|
|
|
|||
|
|
if echo "$output" | grep -q '^redis|.*|fail$'; then
|
|||
|
|
warn "Redis 未连接,后端将降级为内存缓存"
|
|||
|
|
else
|
|||
|
|
ok "Redis 连接正常"
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
port_in_use() {
|
|||
|
|
"$PYTHON" - "$1" <<'PY'
|
|||
|
|
import socket
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
with socket.socket() as sock:
|
|||
|
|
sock.settimeout(1.0)
|
|||
|
|
sys.exit(0 if sock.connect_ex(("127.0.0.1", int(sys.argv[1]))) == 0 else 1)
|
|||
|
|
PY
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------- 服务启动
|
|||
|
|
|
|||
|
|
PIDS=()
|
|||
|
|
|
|||
|
|
start_backend() {
|
|||
|
|
info "启动后端: http://localhost:$BACKEND_PORT (API 文档 /api/docs)"
|
|||
|
|
(cd "$BACKEND_DIR" && exec "$PYTHON" -m uvicorn app.main:app \
|
|||
|
|
--host "$BACKEND_HOST" --port "$BACKEND_PORT" --reload) \
|
|||
|
|
> >(sed -u 's/^/[后端] /') 2>&1 &
|
|||
|
|
PIDS+=("$!")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
start_frontend() {
|
|||
|
|
info "启动前端: http://localhost:$FRONTEND_PORT"
|
|||
|
|
(cd "$FRONTEND_DIR" && PORT="$FRONTEND_PORT" exec yarn dev) \
|
|||
|
|
> >(sed -u 's/^/[前端] /') 2>&1 &
|
|||
|
|
PIDS+=("$!")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cleanup() {
|
|||
|
|
trap - INT TERM
|
|||
|
|
echo
|
|||
|
|
info "正在停止服务..."
|
|||
|
|
if [ "${#PIDS[@]}" -gt 0 ]; then
|
|||
|
|
for pid in "${PIDS[@]}"; do
|
|||
|
|
kill "$pid" 2> /dev/null || true
|
|||
|
|
done
|
|||
|
|
fi
|
|||
|
|
wait 2> /dev/null || true
|
|||
|
|
ok "服务已停止"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main() {
|
|||
|
|
print_header
|
|||
|
|
|
|||
|
|
check_backend_env
|
|||
|
|
check_frontend_env
|
|||
|
|
check_services
|
|||
|
|
|
|||
|
|
info "检查端口占用..."
|
|||
|
|
local backend_free=1 frontend_free=1
|
|||
|
|
if port_in_use "$BACKEND_PORT"; then
|
|||
|
|
warn "端口 $BACKEND_PORT 已被占用,跳过启动后端"
|
|||
|
|
backend_free=0
|
|||
|
|
fi
|
|||
|
|
if port_in_use "$FRONTEND_PORT"; then
|
|||
|
|
warn "端口 $FRONTEND_PORT 已被占用,跳过启动前端"
|
|||
|
|
frontend_free=0
|
|||
|
|
fi
|
|||
|
|
if [ "$backend_free" = 1 ] && [ "$frontend_free" = 1 ]; then
|
|||
|
|
ok "端口 $BACKEND_PORT / $FRONTEND_PORT 均可用"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if [ "$CHECK_ONLY" = 1 ]; then
|
|||
|
|
echo
|
|||
|
|
ok "环境检查完成"
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo
|
|||
|
|
if [ "$backend_free" = 1 ]; then
|
|||
|
|
start_backend
|
|||
|
|
fi
|
|||
|
|
if [ "$frontend_free" = 1 ]; then
|
|||
|
|
start_frontend
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if [ "${#PIDS[@]}" -eq 0 ]; then
|
|||
|
|
warn "前后端均已在运行,未启动新服务"
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo
|
|||
|
|
ok "启动完成,按 Ctrl+C 停止服务"
|
|||
|
|
trap cleanup INT TERM
|
|||
|
|
wait
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main "$@"
|