245 lines
7.0 KiB
Bash
Executable File
245 lines
7.0 KiB
Bash
Executable File
#!/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 "$@"
|