497 lines
14 KiB
Bash
Executable File
497 lines
14 KiB
Bash
Executable File
#!/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
|