Files
YouduWiki/deploy.sh
2026-05-21 22:39:26 +08:00

838 lines
28 KiB
Bash
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.
#!/usr/bin/env bash
# ============================================
# YouduWiki 一键部署脚本
# 用法: ./deploy.sh [选项]
#
# 选项:
# --registry URL 从镜像仓库拉取 (跳过源码构建, 2分钟部署)
# --tag TAG 指定镜像标签 (配合 --registry, 默认 latest)
# --build 强制重新构建所有镜像
# --skip-build 跳过镜像构建 (使用已有镜像)
# --pull 拉取最新的基础镜像
# --clean 停止并清理所有容器和数据 (危险!)
# --stop 停止所有服务
# --restart 重启所有服务
# --logs 查看所有服务日志
# --status 查看服务运行状态
# --help 显示帮助信息
#
# 示例:
# ./deploy.sh # 源码构建部署
# ./deploy.sh --registry registry.cn-hangzhou.aliyuncs.com/my-ns # 镜像部署
# ============================================
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
ENV_FILE=".env"
COMPOSE_FILE="docker-compose.yml"
COMPOSE_FILE_REGISTRY="docker-compose.registry.yml"
USE_REGISTRY=false
REGISTRY_URL=""
IMAGE_TAG="${TAG:-latest}"
ADMIN_PORT="${ADMIN_PORT:-2443}"
APP_PORT="${APP_PORT:-3010}"
API_PORT="${API_PORT:-8000}"
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
# 终端宽度
TERM_WIDTH=${COLUMNS:-80}
((TERM_WIDTH < 60)) && TERM_WIDTH=60
# ============================================
# 阶段定义 (用于进度追踪)
# ============================================
# 总共几个阶段 (不含子步骤):
# 1. 环境检查
# 2. 配置环境变量
# 3. 构建 api 镜像
# 4. 构建 consumer 镜像
# 5. 构建 admin 镜像
# 6. 构建 app 镜像
# 7. 启动基础设施 + 等待就绪
# 8. 启动业务服务 + 等待就绪
TOTAL_STAGES=8
CURRENT_STAGE=0
# 首次构建预估耗时 (秒)
# api: ~3min, consumer: ~2min, admin: ~10min (pnpm install), app: ~8min
# 基础设施: ~30s, 业务服务: ~30s
EST_FIRST_BUILD=$((180 + 120 + 600 + 480 + 30 + 30))
# 缓存构建预估耗时 (秒)
EST_CACHED_BUILD=$((30 + 20 + 60 + 50 + 30 + 30))
OVERALL_START=0
STAGE_START=0
IS_FIRST_BUILD=true
# ============================================
# 进度条绘制
# ============================================
# 清空当前行
clear_line() { printf "\r\033[K"; }
# 格式化时间
format_time() {
local seconds=$1
if [ "$seconds" -lt 60 ]; then
printf "%ds" "$seconds"
elif [ "$seconds" -lt 3600 ]; then
printf "%dm%ds" $((seconds / 60)) $((seconds % 60))
else
printf "%dh%dm" $((seconds / 3600)) $(((seconds % 3600) / 60))
fi
}
# 绘制进度条
# 参数: $1=当前值 $2=最大值 $3=前缀文本 $4=后缀文本
draw_progress_bar() {
local current=$1
local total=${2:-100}
local prefix="${3:-}"
local suffix="${4:-}"
(( total < 1 )) && total=1
(( current > total )) && current=$total
local pct=$((current * 100 / total))
# 计算可用宽度
local bar_width=$((TERM_WIDTH - 45))
((bar_width < 10)) && bar_width=10
local filled=$((pct * bar_width / 100))
local empty=$((bar_width - filled))
# 颜色随进度变化
local bar_color="${GREEN}"
if [ "$pct" -lt 30 ]; then bar_color="${RED}"; fi
if [ "$pct" -ge 30 ] && [ "$pct" -lt 70 ]; then bar_color="${YELLOW}"; fi
# 已填充的块
local filled_bar=""
local i
for ((i = 0; i < filled; i++)); do filled_bar+="█"; done
for ((i = 0; i < empty; i++)); do filled_bar+="░"; done
printf "\r\033[K ${DIM}%s${NC} ${bar_color}%s${NC} ${BOLD}%3d%%${NC} ${DIM}%s${NC}" \
"$prefix" "$filled_bar" "$pct" "$suffix"
}
# ============================================
# 旋转动画 (用于不确定进度的步骤)
# ============================================
SPINNER_CHARS="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
SPINNER_PID=""
SPINNER_ACTIVE=false
start_spinner() {
local message="$1"
SPINNER_ACTIVE=true
(
local i=0
local elapsed=0
while $SPINNER_ACTIVE; do
local c="${SPINNER_CHARS:$((i % 10)):1}"
local elapsed_str
elapsed_str=$(format_time $elapsed)
# 估算剩余时间
local suffix="已运行 ${elapsed_str}"
if [ -n "${STAGE_EST_SEC:-}" ] && [ "$STAGE_EST_SEC" -gt 0 ]; then
local remaining=$((STAGE_EST_SEC - elapsed))
((remaining < 0)) && remaining=0
suffix="已运行 ${elapsed_str} | 预计剩余 $(format_time $remaining)"
fi
printf "\r\033[K ${CYAN}%s${NC} ${DIM}%s${NC} %s" "$c" "$message" "$suffix"
sleep 0.1
elapsed=$((elapsed + 1))
i=$((i + 1))
done
) &
SPINNER_PID=$!
}
stop_spinner() {
SPINNER_ACTIVE=false
[ -n "$SPINNER_PID" ] && wait "$SPINNER_PID" 2>/dev/null
SPINNER_PID=""
}
# ============================================
# 阶段计数器
# ============================================
next_stage() {
CURRENT_STAGE=$((CURRENT_STAGE + 1))
STAGE_START=$(date +%s)
}
# 打印阶段标题
stage_header() {
local stage_name="$1"
local est_sec="${2:-0}"
STAGE_EST_SEC=$est_sec
local elapsed_total=0
if [ "$OVERALL_START" -gt 0 ]; then
elapsed_total=$(($(date +%s) - OVERALL_START))
fi
echo ""
clear_line
printf " ${BOLD}${BLUE}[%d/%d]${NC} ${BOLD}%s${NC}" "$CURRENT_STAGE" "$TOTAL_STAGES" "$stage_name"
if [ "$est_sec" -gt 0 ]; then
printf " ${DIM}(预计 %s)${NC}" "$(format_time $est_sec)"
fi
if [ "$elapsed_total" -gt 0 ]; then
printf " ${DIM}总耗时: %s${NC}" "$(format_time $elapsed_total)"
fi
echo ""
}
# ============================================
# 带进度的等待函数
# ============================================
# 等待某个条件成立,显示进度条
# $1: 等待描述 $2: 最大等待秒数 $3: 检查命令
wait_with_progress() {
local desc="$1"
local max_wait="${2:-60}"
local check_cmd="$3"
local waited=0
while [ $waited -lt $max_wait ]; do
if eval "$check_cmd" &>/dev/null; then
draw_progress_bar "$max_wait" "$max_wait" "$desc" "就绪 ✓"
echo ""
return 0
fi
draw_progress_bar "$waited" "$max_wait" "$desc" "$(format_time $waited)"
sleep 2
waited=$((waited + 2))
done
draw_progress_bar "$max_wait" "$max_wait" "$desc" "超时 ✗"
echo ""
return 1
}
# ============================================
# 构建单个镜像 (带旋转动画)
# ============================================
build_single_image() {
local service="$1"
local label="$2"
local est_sec="${3:-120}"
next_stage
stage_header "$label" "$est_sec"
# Docker 默认输出自带进度条 [1/6] [2/6] 等,直接显示到终端
docker compose -f "$COMPOSE_FILE" build "$service" 2>&1
local exit_code=$?
local elapsed=$(( $(date +%s) - STAGE_START ))
if [ $exit_code -eq 0 ]; then
printf " ${GREEN}${NC} %s ${DIM}(耗时 %s)${NC}\n" "$label 构建成功" "$(format_time $elapsed)"
else
printf " ${RED}${NC} %s ${DIM}(耗时 %s)${NC}\n" "$label 构建失败" "$(format_time $elapsed)"
exit 1
fi
}
# ============================================
# 检查构建缓存状态
# ============================================
check_build_cache() {
# 检查是否已有构建缓存 (通过 docker layer 检查)
if docker image inspect youdu-wiki-api:latest &>/dev/null && \
docker image inspect youdu-wiki-admin:latest &>/dev/null; then
IS_FIRST_BUILD=false
fi
}
# ============================================
# 日志函数
# ============================================
log_info() { echo -e " ${GREEN}${NC} $1"; }
log_warn() { echo -e " ${YELLOW}${NC} $1"; }
log_error() { echo -e " ${RED}${NC} $1"; }
banner() {
clear_line
echo -e "${BLUE}"
echo " ╔══════════════════════════════════════╗"
echo " ║ YouduWiki 部署工具 ║"
echo " ║ v3.85.0 (功能全解锁版) ║"
echo " ╚══════════════════════════════════════╝"
echo -e "${NC}"
}
# ============================================
# 1. 环境检查
# ============================================
check_prerequisites() {
next_stage
stage_header "环境检查" 3
local checks=()
if command -v docker &>/dev/null; then
checks+=("Docker: $(docker --version 2>/dev/null | cut -d' ' -f3 | tr -d ',')")
else
log_error "未找到 Docker请先安装 Docker Engine 20.x+"
echo " Ubuntu: sudo apt-get install docker-ce"
exit 1
fi
if docker compose version &>/dev/null; then
checks+=("Compose: v$(docker compose version --short 2>/dev/null)")
elif command -v docker-compose &>/dev/null; then
checks+=("Compose: $(docker-compose --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+')")
else
log_error "未找到 Docker Compose"
exit 1
fi
if [ -f "$COMPOSE_FILE" ]; then
checks+=("配置文件: ${COMPOSE_FILE}")
else
log_error "未找到 ${COMPOSE_FILE}"
exit 1
fi
local available_gb
available_gb=$(df -BG . 2>/dev/null | awk 'NR==2 {print $4}' | sed 's/G//')
if [ -n "$available_gb" ]; then
if [ "${available_gb:-0}" -lt 10 ]; then
checks+=("磁盘空间: ${available_gb}GB (偏小, 建议≥10GB)")
else
checks+=("磁盘空间: ${available_gb}GB")
fi
fi
local mem_mb
mem_mb=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}')
if [ -n "$mem_mb" ]; then
checks+=("内存: $((mem_mb / 1024))GB")
fi
for check in "${checks[@]}"; do
log_info "$check"
done
clear_line
printf " ${GREEN}${NC} 环境检查通过 ${DIM}(耗时 %s)${NC}\n" "$(format_time $(($(date +%s) - STAGE_START)))"
}
# ============================================
# 2. 环境变量
# ============================================
setup_env() {
next_stage
stage_header "配置环境变量" 2
if [ -f "$ENV_FILE" ]; then
set -a; source "$ENV_FILE"; set +a
log_info "已加载 ${ENV_FILE}"
return
fi
local pg_pass; pg_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64 | tr -d '\n')
local redis_pass; redis_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64 | tr -d '\n')
local nats_pass; nats_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64 | tr -d '\n')
local s3_pass; s3_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64 | tr -d '\n')
local jwt_secret; jwt_secret=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64 | tr -d '\n')
cat > "$ENV_FILE" << EOF
# YouduWiki 环境变量 - $(date '+%Y-%m-%d %H:%M:%S')
POSTGRES_PASSWORD=${pg_pass}
REDIS_PASSWORD=${redis_pass}
NATS_USER=youdu-wiki
NATS_PASSWORD=${nats_pass}
S3_ACCESS_KEY=s3youdu-wiki
S3_SECRET_KEY=${s3_pass}
JWT_SECRET=${jwt_secret}
ADMIN_PASSWORD=admin123
LOG_LEVEL=0
ADMIN_PORT=2443
APP_PORT=3010
API_PORT=8000
EOF
chmod 600 "$ENV_FILE"
set -a; source "$ENV_FILE"; set +a
log_info "已生成随机密码并保存至 ${ENV_FILE}"
}
# ============================================
# 3-6. 构建镜像
# ============================================
build_images() {
# 检查是否有缓存 → 决定预估时间
check_build_cache
local est
if [ "$IS_FIRST_BUILD" = true ]; then
est=$((EST_FIRST_BUILD - 30 - 30)) # 仅构建部分
else
est=$((EST_CACHED_BUILD - 30 - 30))
fi
echo ""
clear_line
if [ "$IS_FIRST_BUILD" = true ]; then
printf " ${YELLOW}首次构建${NC} ${DIM}需要下载依赖包, 预计 %s${NC}\n" "$(format_time $est)"
else
printf " ${GREEN}检测到已有镜像${NC} ${DIM}增量构建, 预计 %s${NC}\n" "$(format_time $est)"
fi
# api
build_single_image "api" "构建后端 API 镜像 (Go 编译)" 180
# consumer
build_single_image "consumer" "构建后端 Consumer 镜像 (Go 编译)" 120
# admin (最慢: pnpm install 几百个包)
build_single_image "admin" "构建管理后台镜像 (React + pnpm)" 600
# app
build_single_image "app" "构建 Wiki 前端镜像 (Next.js + pnpm)" 480
echo ""
clear_line
printf " ${GREEN}${NC} 全部镜像构建完成\n"
# 显示镜像列表
echo ""
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" 2>/dev/null \
| grep -E "youdu-wiki|REPOSITORY" \
| while IFS= read -r line; do echo " $line"; done
}
# ============================================
# 拉取基础镜像 (带并行进度)
# ============================================
pull_base_images() {
next_stage
stage_header "拉取基础镜像" 60
local pulls=()
docker pull postgres:16-alpine & pulls+=($!)
docker pull redis:7-alpine & pulls+=($!)
docker pull nats:2-alpine & pulls+=($!)
docker pull minio/minio:latest & pulls+=($!)
start_spinner "并行拉取 postgres, redis, nats, minio ..."
for pid in "${pulls[@]}"; do wait "$pid" 2>/dev/null; done
stop_spinner
clear_line
printf " ${GREEN}${NC} 基础镜像拉取完成 ${DIM}(耗时 %s)${NC}\n" \
"$(format_time $(($(date +%s) - STAGE_START)))"
}
# ============================================
# Registry 模式: 拉取预构建镜像
# ============================================
pull_registry_images() {
next_stage
stage_header "从镜像仓库拉取预构建镜像" 120
COMPOSE_FILE="$COMPOSE_FILE_REGISTRY"
echo -e " ${DIM}仓库: ${REGISTRY_URL}${NC}"
echo -e " ${DIM}标签: ${IMAGE_TAG}${NC}"
echo ""
# 并行拉取
local pulls=()
local images=(
"youdu-wiki-api"
"youdu-wiki-consumer"
"youdu-wiki-admin"
"youdu-wiki-app"
)
for img in "${images[@]}"; do
local full_img="${REGISTRY_URL}/${img}:${IMAGE_TAG}"
echo -e " ${DIM}拉取 ${full_img}${NC}"
docker pull "$full_img" &
pulls+=($!)
done
# 等待全部拉取完成
start_spinner "并行拉取 4 个镜像 ..."
for pid in "${pulls[@]}"; do
wait "$pid" 2>/dev/null
done
stop_spinner
local elapsed=$(($(date +%s) - STAGE_START))
clear_line
printf " ${GREEN}${NC} 镜像拉取完成 ${DIM}(耗时 %s)${NC}\n" "$(format_time $elapsed)"
echo ""
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" \
| grep -E "youdu-wiki|REPOSITORY" \
| while IFS= read -r line; do echo " $line"; done
}
# ============================================
# 本地镜像文件加载
# ============================================
load_local_images() {
local dir="$1"
local tag="${2:-latest}"
next_stage
stage_header "从本地文件加载镜像" 60
if [ ! -d "$dir" ]; then
log_error "目录不存在: $dir"
exit 1
fi
echo -e " ${DIM}目录: ${dir}${NC}"
echo -e " ${DIM}标签: ${tag}${NC}"
echo ""
local images=(
"youdu-wiki-api"
"youdu-wiki-consumer"
"youdu-wiki-admin"
"youdu-wiki-app"
)
for img in "${images[@]}"; do
local tar_file="${dir}/${img}-${tag}.tar"
local gz_file="${tar_file}.gz"
if [ -f "$gz_file" ]; then
echo -e " ${YELLOW}加载 (gzip):${NC} ${gz_file}"
gunzip -c "$gz_file" | docker load
elif [ -f "$tar_file" ]; then
echo -e " ${YELLOW}加载:${NC} ${tar_file}"
docker load -i "$tar_file"
else
log_error "未找到: ${tar_file}${gz_file}"
echo " 请确认文件名格式: ${img}-${tag}.tar[.gz]"
exit 1
fi
echo -e " ${GREEN}${NC} ${img} 加载完成"
echo ""
done
local elapsed=$(($(date +%s) - STAGE_START))
clear_line
printf " ${GREEN}${NC} 镜像加载完成 ${DIM}(耗时 %s)${NC}\n" "$(format_time $elapsed)"
echo ""
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" \
| grep -E "youdu-wiki|REPOSITORY" \
| while IFS= read -r line; do echo " $line"; done
}
# ============================================
# 7. 启动基础设施
# ============================================
start_infra() {
next_stage
stage_header "启动基础设施 (PostgreSQL, Redis, NATS, MinIO)" 60
local compose_cmd="docker compose -f $COMPOSE_FILE"
if [ "$USE_REGISTRY" = true ]; then
compose_cmd="REGISTRY=${REGISTRY_URL} TAG=${IMAGE_TAG} docker compose -f $COMPOSE_FILE"
fi
start_spinner "启动容器 ..."
eval "$compose_cmd up -d postgres redis nats minio" >/dev/null 2>&1
stop_spinner
log_info "容器已创建"
echo ""
if wait_with_progress "PostgreSQL 启动" 60 \
"docker compose -f $COMPOSE_FILE ps postgres 2>/dev/null | grep -q healthy"; then
log_info "PostgreSQL 就绪"
else
log_error "PostgreSQL 启动超时"; echo ""; echo " 排查: docker compose -f $COMPOSE_FILE logs postgres"; exit 1
fi
if wait_with_progress "Redis 启动 " 30 \
"docker compose -f $COMPOSE_FILE exec -T redis redis-cli -a ${REDIS_PASSWORD:-ChangeMe123!} ping 2>/dev/null | grep -q PONG"; then
log_info "Redis 就绪"
else
log_warn "Redis 健康检查未通过,继续..."
fi
local elapsed=$(( $(date +%s) - STAGE_START ))
clear_line
printf " ${GREEN}${NC} 基础设施就绪 ${DIM}(耗时 %s)${NC}\n" "$(format_time $elapsed)"
}
# ============================================
# 8. 启动业务服务
# ============================================
start_app() {
next_stage
stage_header "启动业务服务 (API, Consumer, Admin, App)" 60
local compose_cmd="docker compose -f $COMPOSE_FILE"
if [ "$USE_REGISTRY" = true ]; then
compose_cmd="REGISTRY=${REGISTRY_URL} TAG=${IMAGE_TAG} docker compose -f $COMPOSE_FILE"
fi
start_spinner "启动容器 ..."
eval "$compose_cmd up -d api consumer admin app" >/dev/null 2>&1
stop_spinner
log_info "容器已创建"
echo ""
if wait_with_progress "API 服务启动 (含数据库迁移)" 45 \
"curl -sf http://localhost:${API_PORT}/api/v1/health 2>/dev/null"; then
log_info "API 服务就绪"
else
log_error "API 服务启动超时"; echo ""; echo " 排查: docker compose -f $COMPOSE_FILE logs api | tail -30"; exit 1
fi
local elapsed=$(( $(date +%s) - STAGE_START ))
clear_line
printf " ${GREEN}${NC} 业务服务就绪 ${DIM}(耗时 %s)${NC}\n" "$(format_time $elapsed)"
}
# ============================================
# 部署完成信息
# ============================================
show_info() {
local total_elapsed
total_elapsed=$(($(date +%s) - OVERALL_START))
local server_ip
if command -v hostname &>/dev/null; then
server_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
fi
[ -z "$server_ip" ] && server_ip="服务器IP"
echo ""
echo -e " ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e " ${BOLD}${GREEN} ✓ YouduWiki 部署成功!${NC} ${DIM}总耗时: $(format_time $total_elapsed)${NC}"
echo -e " ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e " ${BOLD}访问地址:${NC}"
echo -e " ┌─────────────────────────────────────────────────┐"
printf " │ 管理后台: %-37s │\n" "http://${server_ip}:${ADMIN_PORT}"
printf " │ Wiki 前端: %-37s │\n" "http://${server_ip}:${APP_PORT}"
printf " │ API 服务: %-37s │\n" "http://${server_ip}:${API_PORT}"
printf " │ MinIO 控制台: %-35s │\n" "http://${server_ip}:9001"
echo -e " └─────────────────────────────────────────────────┘"
echo ""
echo -e " ${BOLD}登录凭据:${NC}"
echo -e " ┌─────────────────────────────────────────────────┐"
printf " │ 用户名: %-38s │\n" "admin"
printf " │ 密码: %-38s │\n" "${ADMIN_PASSWORD:-admin123}"
echo -e "${YELLOW}(首次登录后请在管理后台修改密码)${NC}"
echo -e " └─────────────────────────────────────────────────┘"
echo ""
echo -e " ${BOLD}${YELLOW}⚡ 下一步:${NC}"
echo -e " 1. 访问管理后台 → 登录"
echo -e " 2. 配置 AI 大模型 (设置 → 模型管理)"
echo -e " 3. 创建知识库 → 上传文档 → 等待学习"
echo -e " 4. 访问 Wiki 前端体验 AI 问答"
echo ""
echo -e " ${BOLD}常用命令:${NC}"
echo -e " ./deploy.sh --status 查看运行状态"
echo -e " ./deploy.sh --logs 查看实时日志"
echo -e " ./deploy.sh --restart 重启全部服务"
echo -e " ./deploy.sh --stop 停止全部服务"
echo ""
}
# ============================================
# 辅助命令
# ============================================
stop_services() {
echo -e "\n ${YELLOW}停止所有服务...${NC}"
docker compose -f "$COMPOSE_FILE" down 2>/dev/null
echo -e " ${GREEN}${NC} 已停止\n"
}
restart_services() {
echo -e "\n ${YELLOW}重启所有服务...${NC}"
docker compose -f "$COMPOSE_FILE" restart 2>/dev/null
echo -e " ${GREEN}${NC} 已重启\n"
}
show_logs() {
echo -e "\n ${DIM}实时日志 (Ctrl+C 退出)${NC}\n"
docker compose -f "$COMPOSE_FILE" logs -f --tail=50
}
show_status() {
echo -e "\n ${BOLD}服务运行状态${NC}\n"
docker compose -f "$COMPOSE_FILE" ps 2>/dev/null
echo ""
if curl -sf http://localhost:${API_PORT}/api/v1/health &>/dev/null; then
echo -e " API 健康检查: ${GREEN}正常${NC}"
else
echo -e " API 健康检查: ${RED}异常${NC}"
fi
echo ""
echo " 容器资源占用:"
docker stats --no-stream \
--format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" \
$(docker compose -f "$COMPOSE_FILE" ps -q) 2>/dev/null || true
}
clean_all() {
echo -e "\n ${RED}${BOLD}⚠ 危险操作: 将删除所有容器、数据卷和镜像${NC}"
echo -e " ${RED}数据库中的所有数据将永久丢失!${NC}\n"
read -rp " 输入 DELETE 确认: " confirm
if [ "$confirm" != "DELETE" ]; then
echo -e " ${GREEN}${NC} 已取消\n"; exit 0
fi
echo -e " ${YELLOW}清理中...${NC}"
docker compose -f "$COMPOSE_FILE" down -v --remove-orphans 2>/dev/null
docker images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null \
| grep "youdu-wiki" \
| xargs -r docker rmi 2>/dev/null || true
echo -e " ${GREEN}${NC} 清理完成\n"
}
# ============================================
# 主流程
# ============================================
main() {
local DO_BUILD=true
local DO_PULL=false
local LOAD_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--load) LOAD_DIR="${2:-mirror}"; DO_BUILD=false; [ $# -gt 1 ] && [[ "$2" != --* ]] && shift; shift ;;
--registry) USE_REGISTRY=true; REGISTRY_URL="$2"; DO_BUILD=false; COMPOSE_FILE="$COMPOSE_FILE_REGISTRY"; shift 2 ;;
--tag) IMAGE_TAG="$2"; shift 2 ;;
--build) DO_BUILD=true; shift ;;
--skip-build) DO_BUILD=false; shift ;;
--pull) DO_PULL=true; shift ;;
--clean) banner; clean_all; exit 0 ;;
--stop) banner; stop_services; exit 0 ;;
--restart) banner; restart_services; show_info; exit 0 ;;
--logs) show_logs; exit 0 ;;
--status) banner; show_status; exit 0 ;;
--help|-h)
banner
echo " 用法: $0 [选项]"
echo ""
echo " 选项:"
echo " --load [DIR] 从本地目录加载镜像 tar (默认 ./mirror, 约1分钟)"
echo " --registry URL 从远程镜像仓库拉取 (约2分钟)"
echo " --tag TAG 指定镜像标签 (配合 --load/--registry)"
echo " --build 源码构建所有镜像 (首次 20-30min)"
echo " --skip-build 跳过镜像构建/加载步骤"
echo " --pull 拉取最新的基础镜像"
echo " --clean 停止并清理所有容器和数据 (危险!)"
echo " --stop 停止所有服务"
echo " --restart 重启所有服务"
echo " --logs 查看所有服务日志"
echo " --status 查看服务运行状态"
echo " --help 显示此帮助信息"
echo ""
echo " 三种部署方式 (由快到慢):"
echo " $0 --load ./images # 本地镜像文件 (最快)"
echo " $0 --registry registry.cn-hangzhou.aliyuncs.com/my-ns # 远程拉取"
echo " $0 # 源码构建 (慢)"
exit 0
;;
*) log_error "未知选项: $1"; echo " 使用 --help 查看帮助"; exit 1 ;;
esac
done
banner
# 检测已有运行中的服务
if docker compose -f "$COMPOSE_FILE" ps --services --filter "status=running" 2>/dev/null | grep -q .; then
echo -e " ${YELLOW}⚠ 检测到已有服务在运行${NC}"
read -rp " 是否重新部署? [y/N]: " redeploy
if [ "$redeploy" != "y" ] && [ "$redeploy" != "Y" ]; then
echo -e " ${GREEN}${NC} 已取消\n"; exit 0
fi
docker compose -f "$COMPOSE_FILE" down 2>/dev/null
fi
OVERALL_START=$(date +%s)
# 根据模式调整阶段数
if [ -n "$LOAD_DIR" ] || [ "$USE_REGISTRY" = true ]; then
TOTAL_STAGES=5
elif [ "$DO_BUILD" = false ]; then
TOTAL_STAGES=4
fi
# ─── 执行部署阶段 ───
check_prerequisites
setup_env
if [ "$DO_PULL" = true ]; then
pull_base_images
fi
if [ -n "$LOAD_DIR" ]; then
load_local_images "$LOAD_DIR" "$IMAGE_TAG"
elif [ "$USE_REGISTRY" = true ]; then
pull_registry_images
elif [ "$DO_BUILD" = true ]; then
build_images
else
next_stage
stage_header "跳过镜像步骤" 1
local missing=()
for img in youdu-wiki-api youdu-wiki-consumer youdu-wiki-admin youdu-wiki-app; do
docker image inspect "${img}:${IMAGE_TAG}" &>/dev/null || missing+=("$img")
done
if [ ${#missing[@]} -gt 0 ]; then
log_error "缺少镜像: ${missing[*]}"
echo " 请先构建: ./build-push.sh --output ./images"
exit 1
fi
log_info "所有镜像已存在"
fi
start_infra
start_app
show_info
}
main "$@"