Files
planet/planet.sh
2026-03-27 11:13:01 +08:00

497 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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