iconsulting/deploy.sh

1396 lines
40 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
#===============================================================================
# iConsulting 部署管理脚本
#
# 用法: ./deploy.sh <command> [service] [options]
#
# 命令:
# build - 编译构建
# start - 启动服务
# stop - 停止服务
# restart - 重启服务
# status - 查看状态
# logs - 查看日志
# clean - 清理构建产物
# deploy - 完整部署(构建+启动)
# db - 数据库操作
# help - 显示帮助
#
# 服务:
# all - 所有服务
# web-client - 用户前端
# admin-client - 管理后台前端
# conversation - 对话服务
# user - 用户服务
# payment - 支付服务
# knowledge - 知识库服务
# evolution - 进化服务
# kong - API网关
# postgres - PostgreSQL数据库
# redis - Redis缓存
# neo4j - Neo4j图数据库
# nginx - Nginx静态服务
#
#===============================================================================
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# 项目根目录
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$PROJECT_ROOT"
# 配置
COMPOSE_FILE="docker-compose.yml"
ENV_FILE=".env"
# Docker Compose 命令 (默认值,会在 check_environment 中更新)
DOCKER_COMPOSE="docker-compose"
# 域名配置
DOMAIN="${DOMAIN:-iconsulting.szaiai.com}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@szaiai.com}"
# 服务端口配置
declare -A SERVICE_PORTS=(
["conversation"]=3004
["user"]=3001
["payment"]=3002
["knowledge"]=3003
["evolution"]=3005
["kong"]=8000
["postgres"]=5432
["redis"]=6379
["neo4j"]=7474
["nginx"]=8080
)
# 服务目录映射
declare -A SERVICE_DIRS=(
["conversation"]="packages/services/conversation-service"
["user"]="packages/services/user-service"
["payment"]="packages/services/payment-service"
["knowledge"]="packages/services/knowledge-service"
["evolution"]="packages/services/evolution-service"
["web-client"]="packages/web-client"
["admin-client"]="packages/admin-client"
["shared"]="packages/shared"
)
# Docker服务名映射
declare -A DOCKER_SERVICES=(
["conversation"]="conversation-service"
["user"]="user-service"
["payment"]="payment-service"
["knowledge"]="knowledge-service"
["evolution"]="evolution-service"
["web-client"]="web-client"
["admin-client"]="admin-client"
["kong"]="kong"
["postgres"]="postgres"
["redis"]="redis"
["neo4j"]="neo4j"
["nginx"]="nginx"
)
#===============================================================================
# 工具函数
#===============================================================================
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "${PURPLE}[STEP]${NC} $1"
}
# 检查命令是否存在
check_command() {
if ! command -v "$1" &> /dev/null; then
log_error "$1 未安装,请先安装"
exit 1
fi
}
# 检查环境 (只需要 Docker)
check_environment() {
log_step "检查运行环境..."
check_command "docker"
# 检查 docker-compose 或 docker compose
if command -v docker-compose &> /dev/null; then
DOCKER_COMPOSE="docker-compose"
elif docker compose version &> /dev/null 2>&1; then
DOCKER_COMPOSE="docker compose"
else
log_error "docker-compose 或 docker compose 未安装"
exit 1
fi
log_success "环境检查通过 (使用 $DOCKER_COMPOSE)"
}
# Builder 镜像名称
BUILDER_IMAGE="iconsulting-builder:latest"
# 构建 Builder 镜像
build_builder_image() {
log_step "检查/构建 Builder 镜像..."
# 检查镜像是否存在
if docker images | grep -q "iconsulting-builder"; then
log_info "Builder 镜像已存在"
return 0
fi
log_info "创建 Builder 镜像..."
# 创建临时 Dockerfile
cat > "$PROJECT_ROOT/.builder.Dockerfile" << 'DOCKERFILE'
FROM node:20-alpine
# 安装 pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# 设置工作目录
WORKDIR /app
# 设置 pnpm store 目录
ENV PNPM_HOME=/root/.local/share/pnpm
ENV PATH=$PNPM_HOME:$PATH
CMD ["sh"]
DOCKERFILE
docker build -t "$BUILDER_IMAGE" -f "$PROJECT_ROOT/.builder.Dockerfile" "$PROJECT_ROOT"
rm -f "$PROJECT_ROOT/.builder.Dockerfile"
log_success "Builder 镜像创建完成"
}
# 加载环境变量
load_env() {
if [ -f "$ENV_FILE" ]; then
export $(grep -v '^#' "$ENV_FILE" | xargs)
fi
}
# 等待服务就绪
wait_for_service() {
local host=$1
local port=$2
local service=$3
local max_attempts=${4:-30}
local attempt=1
log_info "等待 $service ($host:$port) 就绪..."
while [ $attempt -le $max_attempts ]; do
if nc -z "$host" "$port" 2>/dev/null; then
log_success "$service 已就绪"
return 0
fi
echo -n "."
sleep 2
attempt=$((attempt + 1))
done
echo ""
log_error "$service 启动超时"
return 1
}
#===============================================================================
# 构建函数 (使用 Docker 容器构建,无需在主机安装 Node.js)
#===============================================================================
# 在容器中执行命令
run_in_builder() {
docker run --rm \
-v "$PROJECT_ROOT:/app" \
-v "iconsulting-pnpm-store:/root/.local/share/pnpm/store" \
-w /app \
"$BUILDER_IMAGE" \
sh -c "$1"
}
# 安装依赖 (在容器中)
install_deps() {
log_step "安装项目依赖 (在 Docker 容器中)..."
build_builder_image
run_in_builder "pnpm install --frozen-lockfile || pnpm install"
log_success "依赖安装完成"
}
# 构建共享包 (在容器中)
build_shared() {
log_step "构建 shared 包..."
run_in_builder "cd packages/shared && pnpm run build"
log_success "shared 构建完成"
}
# 构建单个后端服务 (在容器中)
build_backend_service() {
local service=$1
local dir="${SERVICE_DIRS[$service]}"
if [ -z "$dir" ]; then
log_error "未知服务: $service"
return 1
fi
log_step "构建 $service..."
run_in_builder "cd $dir && rm -rf dist && pnpm run build"
log_success "$service 构建完成"
}
# 构建单个前端 (在容器中)
build_frontend() {
local service=$1
local dir="${SERVICE_DIRS[$service]}"
if [ -z "$dir" ]; then
log_error "未知服务: $service"
return 1
fi
log_step "构建 $service..."
run_in_builder "cd $dir && rm -rf dist && pnpm run build"
log_success "$service 构建完成"
}
# 构建所有后端服务
build_all_backend() {
build_shared
for service in conversation user payment knowledge evolution; do
build_backend_service "$service"
done
}
# 构建所有前端
build_all_frontend() {
for service in web-client admin-client; do
build_frontend "$service"
done
}
# 构建所有
build_all() {
log_info "开始构建所有服务 (使用 Docker 容器)..."
build_builder_image
install_deps
build_all_backend
build_all_frontend
log_success "所有服务构建完成"
}
# 构建入口
do_build() {
local target=${1:-all}
# 确保 builder 镜像存在
build_builder_image
# 如果不是构建全部,先安装依赖
if [ "$target" != "all" ]; then
install_deps
fi
case $target in
all)
build_all
;;
shared)
build_shared
;;
backend)
build_shared
build_all_backend
;;
frontend)
build_all_frontend
;;
web-client|admin-client)
build_frontend "$target"
;;
conversation|user|payment|knowledge|evolution)
build_shared
build_backend_service "$target"
;;
*)
log_error "未知构建目标: $target"
exit 1
;;
esac
}
#===============================================================================
# Docker 操作函数
#===============================================================================
# 构建 Docker 镜像
build_docker_images() {
local service=${1:-}
log_step "构建 Docker 镜像..."
if [ -n "$service" ] && [ "$service" != "all" ]; then
local docker_service="${DOCKER_SERVICES[$service]}"
if [ -n "$docker_service" ]; then
$DOCKER_COMPOSE build "$docker_service"
else
log_error "未知服务: $service"
return 1
fi
else
$DOCKER_COMPOSE build
fi
log_success "Docker 镜像构建完成"
}
# 启动基础设施
start_infrastructure() {
log_step "启动基础设施服务..."
$DOCKER_COMPOSE up -d postgres redis neo4j
# 等待数据库就绪
wait_for_service localhost 5432 "PostgreSQL"
wait_for_service localhost 6379 "Redis"
wait_for_service localhost 7474 "Neo4j"
log_success "基础设施启动完成"
}
# 启动 Kong 网关
start_kong() {
log_step "启动 Kong API 网关..."
$DOCKER_COMPOSE up -d kong-database
sleep 5
# Kong 数据库迁移
$DOCKER_COMPOSE run --rm kong kong migrations bootstrap || true
$DOCKER_COMPOSE up -d kong
wait_for_service localhost 8000 "Kong"
log_success "Kong 启动完成"
}
# 启动后端服务 (非 Docker 模式 - 已弃用,统一使用 Docker)
# 注意: 本函数已不再使用,保留仅作参考
start_backend_service_local() {
log_warning "本地模式已弃用,将使用 Docker 模式启动服务"
start_backend_service_docker "$1"
}
# 启动后端服务 (Docker 模式)
start_backend_service_docker() {
local service=$1
local docker_service="${DOCKER_SERVICES[$service]}"
log_step "启动 $service (Docker)..."
$DOCKER_COMPOSE up -d "$docker_service"
local port="${SERVICE_PORTS[$service]}"
wait_for_service localhost "$port" "$service"
}
# 启动所有后端服务
start_all_backend() {
local mode=${1:-docker}
for service in user payment knowledge conversation evolution; do
if [ "$mode" = "docker" ]; then
start_backend_service_docker "$service"
else
start_backend_service_local "$service"
fi
done
}
# 启动 Nginx (静态文件服务)
start_nginx() {
log_step "启动 iConsulting Nginx..."
$DOCKER_COMPOSE up -d nginx
wait_for_service localhost 8080 "Nginx"
log_success "iConsulting Nginx 启动完成 (端口 8080)"
# 自动配置系统nginx反向代理
setup_system_nginx_proxy
}
# 自动配置系统nginx反向代理 (傻瓜式)
setup_system_nginx_proxy() {
log_step "配置系统 Nginx 反向代理..."
# 检查系统nginx是否存在
if ! command -v nginx &> /dev/null; then
log_warning "系统未安装 nginx跳过反向代理配置"
log_info "您可以通过 http://服务器IP:8080 直接访问"
return 0
fi
# 检查nginx配置目录
local nginx_available="/etc/nginx/sites-available"
local nginx_enabled="/etc/nginx/sites-enabled"
local nginx_conf_d="/etc/nginx/conf.d"
# 生成配置文件内容
local proxy_conf="# iConsulting 反向代理配置 (自动生成)
# 生成时间: $(date)
server {
listen 80;
listen [::]:80;
server_name $DOMAIN;
# Let's Encrypt 验证
location /.well-known/acme-challenge/ {
root /var/www/html;
}
# 反向代理到 iConsulting Docker Nginx
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \"upgrade\";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
"
# 尝试写入配置 (需要sudo权限)
if [ -d "$nginx_available" ]; then
# Debian/Ubuntu 风格
echo "$proxy_conf" | sudo tee "$nginx_available/iconsulting.conf" > /dev/null 2>&1
if [ $? -eq 0 ]; then
sudo ln -sf "$nginx_available/iconsulting.conf" "$nginx_enabled/iconsulting.conf" 2>/dev/null
log_success "配置已写入 $nginx_available/iconsulting.conf"
else
log_warning "无法写入nginx配置请手动配置或使用sudo运行"
return 1
fi
elif [ -d "$nginx_conf_d" ]; then
# CentOS/RHEL 风格
echo "$proxy_conf" | sudo tee "$nginx_conf_d/iconsulting.conf" > /dev/null 2>&1
if [ $? -eq 0 ]; then
log_success "配置已写入 $nginx_conf_d/iconsulting.conf"
else
log_warning "无法写入nginx配置请手动配置或使用sudo运行"
return 1
fi
else
log_warning "未找到nginx配置目录请手动配置"
return 1
fi
# 测试nginx配置
log_info "测试 nginx 配置..."
if sudo nginx -t 2>/dev/null; then
log_success "nginx 配置测试通过"
# 重载nginx
log_info "重载 nginx..."
sudo systemctl reload nginx 2>/dev/null || sudo nginx -s reload 2>/dev/null
log_success "系统 nginx 已重载"
echo ""
log_success "反向代理配置完成!"
echo -e "${CYAN}现在可以通过以下地址访问:${NC}"
echo " http://$DOMAIN"
echo ""
echo -e "${YELLOW}如需配置 HTTPS请执行:${NC}"
echo " sudo certbot --nginx -d $DOMAIN"
else
log_error "nginx 配置测试失败,请检查配置"
return 1
fi
}
# 启动所有服务
start_all() {
local mode=${1:-docker}
log_info "开始启动所有服务 (模式: $mode)..."
# 创建必要目录
mkdir -p "$PROJECT_ROOT/logs"
mkdir -p "$PROJECT_ROOT/pids"
start_infrastructure
start_kong
start_all_backend "$mode"
start_nginx
log_success "所有服务启动完成"
do_status
}
# 启动入口
do_start() {
local target=${1:-all}
local mode=${2:-docker}
load_env
case $target in
all)
start_all "$mode"
;;
infra|infrastructure)
start_infrastructure
;;
kong)
start_kong
;;
nginx)
start_nginx
;;
postgres|redis|neo4j)
$DOCKER_COMPOSE up -d "$target"
;;
conversation|user|payment|knowledge|evolution)
if [ "$mode" = "docker" ]; then
start_backend_service_docker "$target"
else
start_backend_service_local "$target"
fi
;;
backend)
start_all_backend "$mode"
;;
*)
log_error "未知启动目标: $target"
exit 1
;;
esac
}
#===============================================================================
# 停止函数
#===============================================================================
# 停止单个服务 (本地模式)
stop_service_local() {
local service=$1
log_step "停止 $service..."
if command -v pm2 &> /dev/null; then
pm2 stop "iconsulting-$service" 2>/dev/null || true
pm2 delete "iconsulting-$service" 2>/dev/null || true
else
local pid_file="$PROJECT_ROOT/pids/$service.pid"
if [ -f "$pid_file" ]; then
kill $(cat "$pid_file") 2>/dev/null || true
rm -f "$pid_file"
fi
fi
log_success "$service 已停止"
}
# 停止单个服务 (Docker 模式)
stop_service_docker() {
local service=$1
local docker_service="${DOCKER_SERVICES[$service]}"
if [ -n "$docker_service" ]; then
log_step "停止 $service..."
$DOCKER_COMPOSE stop "$docker_service"
log_success "$service 已停止"
fi
}
# 停止所有服务
stop_all() {
local mode=${1:-docker}
log_info "停止所有服务..."
if [ "$mode" = "docker" ]; then
$DOCKER_COMPOSE down
else
for service in conversation user payment knowledge evolution; do
stop_service_local "$service"
done
$DOCKER_COMPOSE down
fi
log_success "所有服务已停止"
}
# 停止入口
do_stop() {
local target=${1:-all}
local mode=${2:-docker}
case $target in
all)
stop_all "$mode"
;;
infra|infrastructure)
$DOCKER_COMPOSE stop postgres redis neo4j
;;
conversation|user|payment|knowledge|evolution)
if [ "$mode" = "docker" ]; then
stop_service_docker "$target"
else
stop_service_local "$target"
fi
;;
kong|postgres|redis|neo4j|nginx)
$DOCKER_COMPOSE stop "$target"
;;
*)
log_error "未知停止目标: $target"
exit 1
;;
esac
}
#===============================================================================
# 重启函数
#===============================================================================
do_restart() {
local target=${1:-all}
local mode=${2:-docker}
log_info "重启 $target..."
do_stop "$target" "$mode"
sleep 2
do_start "$target" "$mode"
}
#===============================================================================
# 状态查看
#===============================================================================
do_status() {
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} iConsulting 服务状态 ${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
# Docker 服务状态
echo -e "${PURPLE}Docker 容器状态:${NC}"
$DOCKER_COMPOSE ps
echo ""
# 端口检查
echo -e "${PURPLE}服务端口检查:${NC}"
printf "%-20s %-10s %-10s\n" "服务" "端口" "状态"
echo "----------------------------------------"
for service in "${!SERVICE_PORTS[@]}"; do
local port="${SERVICE_PORTS[$service]}"
if nc -z localhost "$port" 2>/dev/null; then
printf "%-20s %-10s ${GREEN}%-10s${NC}\n" "$service" "$port" "运行中"
else
printf "%-20s %-10s ${RED}%-10s${NC}\n" "$service" "$port" "未运行"
fi
done
echo ""
# PM2 状态 (如果使用)
if command -v pm2 &> /dev/null; then
echo -e "${PURPLE}PM2 进程状态:${NC}"
pm2 list 2>/dev/null | grep iconsulting || echo "无 PM2 管理的服务"
echo ""
fi
}
#===============================================================================
# 日志查看
#===============================================================================
do_logs() {
local service=${1:-all}
local lines=${2:-100}
if [ "$service" = "all" ]; then
$DOCKER_COMPOSE logs -f --tail="$lines"
else
local docker_service="${DOCKER_SERVICES[$service]}"
if [ -n "$docker_service" ]; then
$DOCKER_COMPOSE logs -f --tail="$lines" "$docker_service"
else
# 本地日志
local log_file="$PROJECT_ROOT/logs/$service.log"
if [ -f "$log_file" ]; then
tail -f -n "$lines" "$log_file"
else
log_error "日志文件不存在: $log_file"
fi
fi
fi
}
#===============================================================================
# 清理函数
#===============================================================================
do_clean() {
local target=${1:-build}
case $target in
build)
log_step "清理构建产物..."
for dir in "${SERVICE_DIRS[@]}"; do
rm -rf "$PROJECT_ROOT/$dir/dist"
done
log_success "构建产物已清理"
;;
deps)
log_step "清理依赖..."
rm -rf node_modules
for dir in "${SERVICE_DIRS[@]}"; do
rm -rf "$PROJECT_ROOT/$dir/node_modules"
done
log_success "依赖已清理"
;;
docker)
log_step "清理 Docker 资源..."
$DOCKER_COMPOSE down -v --rmi local
docker system prune -f
log_success "Docker 资源已清理"
;;
logs)
log_step "清理日志..."
rm -rf "$PROJECT_ROOT/logs/*"
log_success "日志已清理"
;;
all)
do_clean build
do_clean deps
do_clean docker
do_clean logs
;;
*)
log_error "未知清理目标: $target (可选: build, deps, docker, logs, all)"
exit 1
;;
esac
}
#===============================================================================
# 完整部署
#===============================================================================
do_deploy() {
local mode=${1:-docker}
log_info "开始完整部署 (模式: $mode)..."
check_environment
# 构建
do_build all
# 如果是 Docker 模式,构建镜像
if [ "$mode" = "docker" ]; then
build_docker_images
fi
# 启动
do_start all "$mode"
# 配置 Kong 路由
setup_kong_routes
log_success "部署完成!"
echo ""
echo -e "${CYAN}访问地址:${NC}"
echo " 用户前端: https://$DOMAIN"
echo " 管理后台: https://$DOMAIN/admin"
echo " API 网关: https://$DOMAIN/api"
echo " Kong 管理: http://localhost:8001 (仅本地)"
echo ""
echo -e "${YELLOW}提示: 如需配置 SSL 证书,请执行:${NC}"
echo " ./deploy.sh ssl obtain"
echo ""
}
# 完整部署 (含 SSL)
do_deploy_full() {
log_info "开始完整部署 (含 SSL 证书)..."
# 基础部署
do_deploy docker
# 安装并获取 SSL 证书
do_ssl obtain
# 配置自动续期
do_ssl auto-renew
log_success "完整部署完成 (含 SSL)"
echo ""
echo -e "${CYAN}访问地址:${NC}"
echo " 用户前端: https://$DOMAIN"
echo " 管理后台: https://$DOMAIN/admin"
echo ""
}
#===============================================================================
# SSL 证书管理 (Let's Encrypt)
#===============================================================================
# 安装 Certbot
install_certbot() {
log_step "检查/安装 Certbot..."
if command -v certbot &> /dev/null; then
log_success "Certbot 已安装"
return 0
fi
# 检测操作系统
if [ -f /etc/debian_version ]; then
# Debian/Ubuntu
apt-get update
apt-get install -y certbot python3-certbot-nginx
elif [ -f /etc/redhat-release ]; then
# CentOS/RHEL
yum install -y epel-release
yum install -y certbot python3-certbot-nginx
elif [ -f /etc/alpine-release ]; then
# Alpine
apk add certbot certbot-nginx
else
log_error "无法识别的操作系统,请手动安装 certbot"
exit 1
fi
log_success "Certbot 安装完成"
}
# 申请 SSL 证书
obtain_ssl_cert() {
local domain=${1:-$DOMAIN}
local email=${2:-$ADMIN_EMAIL}
log_step "申请 SSL 证书: $domain..."
# 创建证书目录
mkdir -p "$PROJECT_ROOT/nginx/ssl"
# 停止 Nginx (释放 80 端口)
$DOCKER_COMPOSE stop nginx 2>/dev/null || true
# 使用 standalone 模式申请证书
certbot certonly \
--standalone \
--non-interactive \
--agree-tos \
--email "$email" \
-d "$domain" \
--cert-path "$PROJECT_ROOT/nginx/ssl/cert.pem" \
--key-path "$PROJECT_ROOT/nginx/ssl/privkey.pem" \
--fullchain-path "$PROJECT_ROOT/nginx/ssl/fullchain.pem"
# 复制证书到项目目录
if [ -d "/etc/letsencrypt/live/$domain" ]; then
cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$PROJECT_ROOT/nginx/ssl/"
cp "/etc/letsencrypt/live/$domain/privkey.pem" "$PROJECT_ROOT/nginx/ssl/"
chmod 644 "$PROJECT_ROOT/nginx/ssl/fullchain.pem"
chmod 600 "$PROJECT_ROOT/nginx/ssl/privkey.pem"
log_success "SSL 证书已获取并复制到 nginx/ssl/"
else
log_error "证书申请失败"
return 1
fi
# 重启 Nginx
$DOCKER_COMPOSE up -d nginx
log_success "SSL 证书配置完成"
}
# 续期 SSL 证书
renew_ssl_cert() {
log_step "续期 SSL 证书..."
certbot renew --quiet
# 复制新证书
if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then
cp "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "$PROJECT_ROOT/nginx/ssl/"
cp "/etc/letsencrypt/live/$DOMAIN/privkey.pem" "$PROJECT_ROOT/nginx/ssl/"
# 重载 Nginx
$DOCKER_COMPOSE exec nginx nginx -s reload
log_success "SSL 证书续期完成"
fi
}
# 配置证书自动续期
setup_ssl_auto_renew() {
log_step "配置证书自动续期..."
# 创建续期脚本
cat > /etc/cron.d/certbot-renew << EOF
# 每天凌晨 3 点检查证书续期
0 3 * * * root certbot renew --quiet && cp /etc/letsencrypt/live/$DOMAIN/*.pem $PROJECT_ROOT/nginx/ssl/ && docker-compose -f $PROJECT_ROOT/docker-compose.yml exec -T nginx nginx -s reload
EOF
log_success "自动续期已配置 (每天 3:00 检查)"
}
# SSL 操作入口
do_ssl() {
local action=${1:-status}
case $action in
install)
install_certbot
;;
obtain|get)
install_certbot
obtain_ssl_cert "$2" "$3"
;;
renew)
renew_ssl_cert
;;
auto-renew)
setup_ssl_auto_renew
;;
status)
echo -e "${PURPLE}SSL 证书状态:${NC}"
if [ -f "$PROJECT_ROOT/nginx/ssl/fullchain.pem" ]; then
echo "证书文件: 存在"
openssl x509 -in "$PROJECT_ROOT/nginx/ssl/fullchain.pem" -noout -dates 2>/dev/null || echo "无法读取证书信息"
else
echo "证书文件: 不存在"
fi
if command -v certbot &> /dev/null; then
echo ""
echo "Certbot 证书列表:"
certbot certificates 2>/dev/null || echo "无证书"
fi
;;
*)
log_error "未知 SSL 操作: $action (可选: install, obtain, renew, auto-renew, status)"
exit 1
;;
esac
}
#===============================================================================
# Kong API Gateway 配置
#===============================================================================
# 配置 Kong 路由
setup_kong_routes() {
local kong_admin="${KONG_ADMIN_URL:-http://localhost:8001}"
log_step "配置 Kong API Gateway 路由..."
# 等待 Kong 就绪
local max_attempts=30
local attempt=1
while [ $attempt -le $max_attempts ]; do
if curl -s "$kong_admin" > /dev/null 2>&1; then
break
fi
echo -n "."
sleep 2
attempt=$((attempt + 1))
done
if [ $attempt -gt $max_attempts ]; then
log_error "Kong Admin API 不可用"
return 1
fi
echo ""
log_info "Kong Admin API 就绪"
# 创建服务和路由
log_info "创建微服务..."
# User Service
curl -s -X POST "$kong_admin/services" \
-d "name=user-service" \
-d "url=http://user-service:3001" > /dev/null 2>&1 || true
curl -s -X POST "$kong_admin/services/user-service/routes" \
-d "name=user-route" \
-d "paths[]=/v1/users" \
-d "paths[]=/v1/auth" \
-d "strip_path=false" > /dev/null 2>&1 || true
# Payment Service
curl -s -X POST "$kong_admin/services" \
-d "name=payment-service" \
-d "url=http://payment-service:3002" > /dev/null 2>&1 || true
curl -s -X POST "$kong_admin/services/payment-service/routes" \
-d "name=payment-route" \
-d "paths[]=/v1/payments" \
-d "paths[]=/v1/balance" \
-d "strip_path=false" > /dev/null 2>&1 || true
# Knowledge Service
curl -s -X POST "$kong_admin/services" \
-d "name=knowledge-service" \
-d "url=http://knowledge-service:3003" > /dev/null 2>&1 || true
curl -s -X POST "$kong_admin/services/knowledge-service/routes" \
-d "name=knowledge-route" \
-d "paths[]=/v1/knowledge" \
-d "strip_path=false" > /dev/null 2>&1 || true
# Conversation Service
curl -s -X POST "$kong_admin/services" \
-d "name=conversation-service" \
-d "url=http://conversation-service:3004" > /dev/null 2>&1 || true
curl -s -X POST "$kong_admin/services/conversation-service/routes" \
-d "name=conversation-route" \
-d "paths[]=/v1/conversations" \
-d "strip_path=false" > /dev/null 2>&1 || true
# Evolution Service (Admin)
curl -s -X POST "$kong_admin/services" \
-d "name=evolution-service" \
-d "url=http://evolution-service:3005" > /dev/null 2>&1 || true
curl -s -X POST "$kong_admin/services/evolution-service/routes" \
-d "name=evolution-route" \
-d "paths[]=/v1/evolution" \
-d "paths[]=/v1/memory" \
-d "paths[]=/v1/admin" \
-d "strip_path=false" > /dev/null 2>&1 || true
log_info "配置全局插件..."
# Rate Limiting
curl -s -X POST "$kong_admin/plugins" \
-d "name=rate-limiting" \
-d "config.minute=100" \
-d "config.policy=local" > /dev/null 2>&1 || true
# CORS
curl -s -X POST "$kong_admin/plugins" \
-d "name=cors" \
-d "config.origins=https://$DOMAIN,http://localhost" \
-d "config.methods=GET,POST,PUT,DELETE,OPTIONS,PATCH" \
-d "config.headers=Accept,Authorization,Content-Type,X-User-Id,X-Request-Id" \
-d "config.credentials=true" \
-d "config.max_age=3600" > /dev/null 2>&1 || true
# Request ID
curl -s -X POST "$kong_admin/plugins" \
-d "name=correlation-id" \
-d "config.header_name=X-Request-Id" \
-d "config.generator=uuid" > /dev/null 2>&1 || true
log_success "Kong 路由配置完成"
# 显示配置结果
echo ""
echo -e "${PURPLE}已配置的服务:${NC}"
curl -s "$kong_admin/services" | grep -o '"name":"[^"]*"' | sed 's/"name":"//g;s/"//g' | while read name; do
echo " - $name"
done
echo ""
echo -e "${PURPLE}已配置的路由:${NC}"
curl -s "$kong_admin/routes" | grep -o '"name":"[^"]*"' | sed 's/"name":"//g;s/"//g' | while read name; do
echo " - $name"
done
}
# Kong 操作入口
do_kong() {
local action=${1:-status}
case $action in
setup|init)
setup_kong_routes
;;
status)
local kong_admin="${KONG_ADMIN_URL:-http://localhost:8001}"
echo -e "${PURPLE}Kong 状态:${NC}"
curl -s "$kong_admin" > /dev/null 2>&1 && echo "Kong Admin API: 运行中" || echo "Kong Admin API: 未运行"
curl -s "http://localhost:8000" > /dev/null 2>&1 && echo "Kong Proxy: 运行中" || echo "Kong Proxy: 未运行"
echo ""
echo "服务数量: $(curl -s "$kong_admin/services" 2>/dev/null | grep -o '"total":[0-9]*' | cut -d: -f2 || echo 0)"
echo "路由数量: $(curl -s "$kong_admin/routes" 2>/dev/null | grep -o '"total":[0-9]*' | cut -d: -f2 || echo 0)"
;;
services)
curl -s "http://localhost:8001/services" | python3 -m json.tool 2>/dev/null || curl -s "http://localhost:8001/services"
;;
routes)
curl -s "http://localhost:8001/routes" | python3 -m json.tool 2>/dev/null || curl -s "http://localhost:8001/routes"
;;
plugins)
curl -s "http://localhost:8001/plugins" | python3 -m json.tool 2>/dev/null || curl -s "http://localhost:8001/plugins"
;;
*)
log_error "未知 Kong 操作: $action (可选: setup, status, services, routes, plugins)"
exit 1
;;
esac
}
#===============================================================================
# 数据库操作
#===============================================================================
do_db() {
local action=${1:-status}
case $action in
migrate)
log_step "执行数据库迁移 (在 Docker 容器中)..."
build_builder_image
# 在容器中执行迁移命令
for service in user payment knowledge conversation evolution; do
local dir="${SERVICE_DIRS[$service]}"
run_in_builder "cd $dir && pnpm run migration:run 2>/dev/null" || log_warning "$service 无迁移或迁移失败"
done
log_success "数据库迁移完成"
;;
seed)
log_step "初始化种子数据..."
# 添加种子数据脚本
log_success "种子数据初始化完成"
;;
backup)
local backup_dir="$PROJECT_ROOT/backups/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$backup_dir"
log_step "备份数据库..."
$DOCKER_COMPOSE exec -T postgres pg_dump -U postgres iconsulting > "$backup_dir/postgres.sql"
log_success "数据库备份到: $backup_dir"
;;
restore)
local backup_file=$2
if [ -z "$backup_file" ]; then
log_error "请指定备份文件: ./deploy.sh db restore <backup_file>"
exit 1
fi
log_step "恢复数据库..."
$DOCKER_COMPOSE exec -T postgres psql -U postgres iconsulting < "$backup_file"
log_success "数据库恢复完成"
;;
reset)
log_warning "这将删除所有数据!"
read -p "确认继续? (y/N) " confirm
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
$DOCKER_COMPOSE down -v
$DOCKER_COMPOSE up -d postgres redis neo4j
wait_for_service localhost 5432 "PostgreSQL"
do_db migrate
log_success "数据库已重置"
fi
;;
status)
echo -e "${PURPLE}数据库状态:${NC}"
$DOCKER_COMPOSE exec postgres psql -U postgres -c "SELECT version();" 2>/dev/null || echo "PostgreSQL 未运行"
$DOCKER_COMPOSE exec redis redis-cli ping 2>/dev/null || echo "Redis 未运行"
curl -s http://localhost:7474 > /dev/null && echo "Neo4j 运行中" || echo "Neo4j 未运行"
;;
*)
log_error "未知数据库操作: $action (可选: migrate, seed, backup, restore, reset, status)"
exit 1
;;
esac
}
#===============================================================================
# 帮助信息
#===============================================================================
show_help() {
cat << EOF
╔═══════════════════════════════════════════════════════════════════════════════╗
║ iConsulting 部署管理脚本 ║
║ 域名: $DOMAIN ║
╚═══════════════════════════════════════════════════════════════════════════════╝
用法: ./deploy.sh <command> [target] [options]
命令:
build [target] 编译构建
target: all, shared, backend, frontend,
conversation, user, payment, knowledge, evolution,
web-client, admin-client
start [target] [mode] 启动服务
target: all, infra, kong, nginx, backend,
conversation, user, payment, knowledge, evolution,
postgres, redis, neo4j
mode: docker (默认), local
stop [target] [mode] 停止服务
(target 同上)
restart [target] [mode] 重启服务
(target 同上)
status 查看所有服务状态
logs [service] [lines] 查看日志
service: 服务名或 all
lines: 显示行数 (默认 100)
clean [target] 清理
target: build, deps, docker, logs, all
deploy [mode] 完整部署 (构建 + 启动)
mode: docker (默认), local
deploy-full 完整部署 (含 SSL 证书自动申请)
ssl <action> SSL 证书管理 (Let's Encrypt)
action: install - 安装 certbot
obtain - 申请证书
renew - 续期证书
auto-renew - 配置自动续期
status - 查看证书状态
kong <action> Kong API Gateway 管理
action: setup - 配置路由和插件
status - 查看状态
services - 查看服务列表
routes - 查看路由列表
plugins - 查看插件列表
db <action> 数据库操作
action: migrate, seed, backup, restore, reset, status
help 显示此帮助信息
示例:
./deploy.sh deploy # 完整部署
./deploy.sh deploy-full # 完整部署 (含 SSL)
./deploy.sh ssl obtain # 申请 SSL 证书
./deploy.sh kong setup # 配置 Kong 路由
./deploy.sh build conversation # 只构建对话服务
./deploy.sh start backend local # 本地模式启动所有后端
./deploy.sh restart user docker # 重启用户服务 (Docker)
./deploy.sh logs conversation 200 # 查看对话服务最近200行日志
./deploy.sh clean all # 清理所有构建产物和依赖
./deploy.sh db backup # 备份数据库
./deploy.sh db migrate # 执行数据库迁移
环境变量:
DOMAIN 域名 (默认: iconsulting.szaiai.com)
ADMIN_EMAIL 管理员邮箱 (用于 SSL 证书)
系统要求:
- Docker (必须)
- Docker Compose 或 docker compose (必须)
- 无需安装 Node.js / pnpm (构建在 Docker 容器中进行)
EOF
}
#===============================================================================
# 主入口
#===============================================================================
main() {
local command=${1:-help}
shift || true
# 对于 help 命令不需要检查环境
if [ "$command" != "help" ] && [ "$command" != "--help" ] && [ "$command" != "-h" ]; then
check_environment
fi
case $command in
build)
do_build "$@"
;;
start)
do_start "$@"
;;
stop)
do_stop "$@"
;;
restart)
do_restart "$@"
;;
status)
do_status
;;
logs)
do_logs "$@"
;;
clean)
do_clean "$@"
;;
deploy)
do_deploy "$@"
;;
deploy-full)
do_deploy_full
;;
ssl)
do_ssl "$@"
;;
kong)
do_kong "$@"
;;
db)
do_db "$@"
;;
help|--help|-h)
show_help
;;
*)
log_error "未知命令: $command"
show_help
exit 1
;;
esac
}
# 执行主函数
main "$@"