#!/bin/bash set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' BACKEND_MAX_RETRIES="${BACKEND_MAX_RETRIES:-3}" BACKEND_HEALTH_CHECK_ATTEMPTS="${BACKEND_HEALTH_CHECK_ATTEMPTS:-10}" BACKEND_HEALTH_CHECK_INTERVAL="${BACKEND_HEALTH_CHECK_INTERVAL:-2}" FRONTEND_MAX_RETRIES="${FRONTEND_MAX_RETRIES:-3}" FRONTEND_HEALTH_CHECK_ATTEMPTS="${FRONTEND_HEALTH_CHECK_ATTEMPTS:-10}" FRONTEND_HEALTH_CHECK_INTERVAL="${FRONTEND_HEALTH_CHECK_INTERVAL:-2}" DEFAULT_BACKEND_PORT="${DEFAULT_BACKEND_PORT:-8000}" DEFAULT_FRONTEND_PORT="${DEFAULT_FRONTEND_PORT:-3000}" ensure_uv_backend_deps() { echo -e "${BLUE}📦 检查后端 uv 环境...${NC}" if ! command -v uv >/dev/null 2>&1; then echo -e "${RED}❌ 未找到 uv,请先安装 uv 并加入 PATH${NC}" exit 1 fi cd "$SCRIPT_DIR" if [ ! -x "$SCRIPT_DIR/.venv/bin/python" ]; then echo -e "${YELLOW}⚠️ 未检测到 .venv,正在执行 uv sync...${NC}" uv sync --group dev fi if [ ! -x "$SCRIPT_DIR/.venv/bin/python" ]; then echo -e "${RED}❌ uv 环境初始化失败,未找到 .venv/bin/python${NC}" exit 1 fi } ensure_frontend_deps() { echo -e "${BLUE}📦 检查前端依赖...${NC}" if ! command -v bun >/dev/null 2>&1; then echo -e "${RED}❌ 未找到 bun,请先安装或加载 bun 到 PATH${NC}" exit 1 fi cd "$SCRIPT_DIR/frontend" if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then echo -e "${YELLOW}⚠️ 前端依赖缺失,正在执行 bun install...${NC}" bun install fi if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then echo -e "${RED}❌ 前端依赖安装失败,未找到 vite${NC}" exit 1 fi } wait_for_http() { local url="$1" local attempts="$2" local interval="$3" local service_name="$4" local attempt=1 while [ "$attempt" -le "$attempts" ]; do if curl -s "$url" > /dev/null 2>&1; then return 0 fi echo -e "${YELLOW}⏳ 等待${service_name}就绪 (${attempt}/${attempts})...${NC}" sleep "$interval" attempt=$((attempt + 1)) done return 1 } start_backend_with_retry() { local backend_port="$1" local retry=1 while [ "$retry" -le "$BACKEND_MAX_RETRIES" ]; do pkill -f "uvicorn" 2>/dev/null || true cd "$SCRIPT_DIR/backend" PYTHONPATH="$SCRIPT_DIR/backend" nohup uv run --project "$SCRIPT_DIR" python -m uvicorn app.main:app --host 0.0.0.0 --port "$backend_port" --reload > /tmp/planet_backend.log 2>&1 & BACKEND_PID=$! if wait_for_http "http://localhost:${backend_port}/health" "$BACKEND_HEALTH_CHECK_ATTEMPTS" "$BACKEND_HEALTH_CHECK_INTERVAL" "后端"; then return 0 fi echo -e "${YELLOW}⚠️ 后端第 ${retry}/${BACKEND_MAX_RETRIES} 次启动未就绪,准备重试...${NC}" kill "$BACKEND_PID" 2>/dev/null || true retry=$((retry + 1)) done return 1 } start_frontend_with_retry() { local frontend_port="$1" local retry=1 while [ "$retry" -le "$FRONTEND_MAX_RETRIES" ]; do pkill -f "vite" 2>/dev/null || true pkill -f "bun run dev" 2>/dev/null || true cd "$SCRIPT_DIR/frontend" nohup bun run dev --port "$frontend_port" > /tmp/planet_frontend.log 2>&1 & FRONTEND_PID=$! if wait_for_http "http://localhost:${frontend_port}" "$FRONTEND_HEALTH_CHECK_ATTEMPTS" "$FRONTEND_HEALTH_CHECK_INTERVAL" "前端"; then return 0 fi echo -e "${YELLOW}⚠️ 前端第 ${retry}/${FRONTEND_MAX_RETRIES} 次启动未就绪,准备重试...${NC}" kill "$FRONTEND_PID" 2>/dev/null || true retry=$((retry + 1)) done return 1 } validate_port() { local port="$1" if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then echo -e "${RED}❌ 非法端口: ${port}${NC}" exit 1 fi } kill_port_if_requested() { local port="$1" local service_name="$2" echo -e "${YELLOW}🧹 检测 ${service_name} 端口 ${port} 占用...${NC}" if command -v fuser >/dev/null 2>&1 && fuser "${port}/tcp" >/dev/null 2>&1; then echo -e "${BLUE}🔌 发现端口 ${port} 占用,正在终止...${NC}" fuser -k "${port}/tcp" >/dev/null 2>&1 || true sleep 1 return 0 fi if command -v lsof >/dev/null 2>&1; then local pids pids="$(lsof -ti tcp:"${port}" 2>/dev/null || true)" if [ -n "$pids" ]; then echo -e "${BLUE}🔌 发现端口 ${port} 占用,正在终止...${NC}" kill $pids 2>/dev/null || true sleep 1 return 0 fi fi echo -e "${GREEN}✅ 端口 ${port} 未被占用${NC}" } parse_service_args() { BACKEND_PORT="$DEFAULT_BACKEND_PORT" FRONTEND_PORT="$DEFAULT_FRONTEND_PORT" BACKEND_PORT_REQUESTED=0 FRONTEND_PORT_REQUESTED=0 while [ "$#" -gt 0 ]; do case "$1" in -b|--backend-port) BACKEND_PORT_REQUESTED=1 if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then BACKEND_PORT="$2" shift 2 else shift 1 fi ;; -f|--frontend-port) FRONTEND_PORT_REQUESTED=1 if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then FRONTEND_PORT="$2" shift 2 else shift 1 fi ;; *) echo -e "${RED}❌ 未知参数: $1${NC}" exit 1 ;; esac done validate_port "$BACKEND_PORT" validate_port "$FRONTEND_PORT" } cleanup_exit_containers() { local exit_containers exit_containers="$(docker ps -a --filter status=exited -q 2>/dev/null || true)" if [ -n "$exit_containers" ]; then echo -e "${BLUE}🗑️ 清理残留 Exit 容器...${NC}" echo "$exit_containers" | xargs -r docker rm -f >/dev/null 2>&1 || true echo -e "${GREEN}✅ 残留容器已清理${NC}" fi } stop_backend_service() { pkill -f "uvicorn" 2>/dev/null || true } stop_frontend_service() { pkill -f "vite" 2>/dev/null || true pkill -f "bun run dev" 2>/dev/null || true } start_backend_service() { local backend_port="$1" local backend_port_requested="$2" echo -e "${BLUE}🗄️ 启动数据库...${NC}" docker start planet_postgres planet_redis 2>/dev/null || docker-compose up -d postgres redis sleep 3 if [ "$backend_port_requested" -eq 1 ]; then kill_port_if_requested "$backend_port" "后端" fi echo -e "${BLUE}🔧 启动后端...${NC}" ensure_uv_backend_deps if ! start_backend_with_retry "$backend_port"; then echo -e "${RED}❌ 后端启动失败,已重试 ${BACKEND_MAX_RETRIES} 次${NC}" tail -10 /tmp/planet_backend.log exit 1 fi } start_frontend_service() { local frontend_port="$1" local frontend_port_requested="$2" if [ "$frontend_port_requested" -eq 1 ]; then kill_port_if_requested "$frontend_port" "前端" fi echo -e "${BLUE}🌐 启动前端...${NC}" ensure_frontend_deps if ! start_frontend_with_retry "$frontend_port"; then echo -e "${RED}❌ 前端启动失败,已重试 ${FRONTEND_MAX_RETRIES} 次${NC}" tail -10 /tmp/planet_frontend.log exit 1 fi } create_user() { local username local password local password_confirm local email local generated_email local role="viewer" local is_admin ensure_uv_backend_deps echo -e "${BLUE}🗄️ 启动数据库...${NC}" docker start planet_postgres 2>/dev/null || docker-compose up -d postgres sleep 2 echo -e "${BLUE}👤 创建用户${NC}" read -r -p "用户名: " username if [ -z "$username" ]; then echo -e "${RED}❌ 用户名不能为空${NC}" exit 1 fi generated_email="${username}@planet.local" read -r -p "邮箱(留空自动使用 ${generated_email}): " email if [ -z "$email" ]; then email="$generated_email" fi read -r -s -p "密码: " password echo "" read -r -s -p "确认密码: " password_confirm echo "" if [ -z "$password" ]; then echo -e "${RED}❌ 密码不能为空${NC}" exit 1 fi if [ "${#password}" -lt 8 ]; then echo -e "${RED}❌ 密码长度不能少于 8 位${NC}" exit 1 fi if [ "$password" != "$password_confirm" ]; then echo -e "${RED}❌ 两次输入的密码不一致${NC}" exit 1 fi read -r -p "是否为管理员? (Y/N): " is_admin if [[ "$is_admin" =~ ^[Yy]$ ]]; then role="super_admin" fi cd "$SCRIPT_DIR/backend" PLANET_CREATEUSER_USERNAME="$username" \ PLANET_CREATEUSER_PASSWORD="$password" \ PLANET_CREATEUSER_EMAIL="$email" \ PLANET_CREATEUSER_ROLE="$role" \ PYTHONPATH="$SCRIPT_DIR/backend" \ "$SCRIPT_DIR/.venv/bin/python" - <<'PY' import asyncio import os from sqlalchemy import text from app.core.security import get_password_hash from app.db.session import async_session_factory from app.models.user import User async def main(): username = os.environ["PLANET_CREATEUSER_USERNAME"].strip() password = os.environ["PLANET_CREATEUSER_PASSWORD"] email = os.environ["PLANET_CREATEUSER_EMAIL"].strip() role = os.environ["PLANET_CREATEUSER_ROLE"].strip() async with async_session_factory() as session: result = await session.execute( text("SELECT id FROM users WHERE username = :username OR email = :email"), {"username": username, "email": email}, ) if result.fetchone(): raise SystemExit("用户名已存在") user = User( username=username, email=email, password_hash=get_password_hash(password), role=role, is_active=True, ) session.add(user) await session.commit() asyncio.run(main()) PY echo -e "${GREEN}✅ 用户创建成功${NC}" echo " 用户名: ${username}" echo " 角色: ${role}" echo " 邮箱: ${email}" } start() { parse_service_args "$@" cleanup_exit_containers echo -e "${BLUE}🚀 启动智能星球计划...${NC}" start_backend_service "$BACKEND_PORT" "$BACKEND_PORT_REQUESTED" start_frontend_service "$FRONTEND_PORT" "$FRONTEND_PORT_REQUESTED" echo "" echo -e "${GREEN}✅ 启动完成!${NC}" echo " 前端: http://localhost:${FRONTEND_PORT}" echo " 后端: http://localhost:${BACKEND_PORT}" } stop() { echo -e "${YELLOW}🛑 停止服务...${NC}" stop_backend_service stop_frontend_service docker stop planet_postgres planet_redis 2>/dev/null || true echo -e "${GREEN}✅ 已停止${NC}" } restart() { parse_service_args "$@" cleanup_exit_containers if [ "$BACKEND_PORT_REQUESTED" -eq 0 ] && [ "$FRONTEND_PORT_REQUESTED" -eq 0 ]; then stop sleep 1 start return 0 fi echo -e "${YELLOW}🔄 按需重启服务...${NC}" if [ "$BACKEND_PORT_REQUESTED" -eq 1 ]; then stop_backend_service sleep 1 start_backend_service "$BACKEND_PORT" 1 fi if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then stop_frontend_service sleep 1 start_frontend_service "$FRONTEND_PORT" 1 fi echo "" echo -e "${GREEN}✅ 重启完成!${NC}" if [ "$BACKEND_PORT_REQUESTED" -eq 1 ]; then echo " 后端: http://localhost:${BACKEND_PORT}" fi if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then echo " 前端: http://localhost:${FRONTEND_PORT}" fi } health() { echo "📊 容器状态:" docker ps --filter "name=planet_" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" echo "" echo "🔍 服务状态:" if curl -s http://localhost:8000/health > /dev/null 2>&1; then echo -e " 后端: ${GREEN}✅ 运行中${NC}" else echo -e " 后端: ${RED}❌ 未运行${NC}" fi if curl -s http://localhost:3000 > /dev/null 2>&1; then echo -e " 前端: ${GREEN}✅ 运行中${NC}" else echo -e " 前端: ${RED}❌ 未运行${NC}" fi } log() { case "$1" in -f|--frontend) echo "📝 前端日志 (Ctrl+C 退出):" tail -f /tmp/planet_frontend.log ;; -b|--backend) echo "📝 后端日志 (Ctrl+C 退出):" tail -f /tmp/planet_backend.log ;; *) echo "📝 最近日志:" echo "--- 后端 ---" tail -20 /tmp/planet_backend.log 2>/dev/null || echo "无日志" echo "--- 前端 ---" tail -20 /tmp/planet_frontend.log 2>/dev/null || echo "无日志" ;; esac } case "$1" in start) shift start "$@" ;; stop) stop ;; restart) shift restart "$@" ;; createuser) create_user ;; health) health ;; log) log "$2" ;; *) echo "用法: ./planet.sh {start|stop|restart|createuser|health|log}" echo "" echo "命令:" echo " start 启动服务,可选: -b <后端端口> -f <前端端口>" echo " stop 停止服务" echo " restart 重启服务,可选: -b [后端端口] -f [前端端口]" echo " createuser 交互创建用户" echo " health 检查健康状态" echo " log 查看日志" echo " log -f 查看前端日志" echo " log -b 查看后端日志" ;; esac