#!/bin/bash # # SmartClean App 后台一键部署脚本(Docker 本地版) # # 用法: # ./deploy.sh # 构建并部署 complex + mall # ./deploy.sh complex # 仅构建部署 xiaoqu-complex # ./deploy.sh mall # 仅构建部署 xiaoqu-mall # ./deploy.sh rollback # 回滚到上一版本 # ./deploy.sh status # 查看容器状态 # ./deploy.sh logs [服务名] # 查看日志 # ./deploy.sh stop # 停止所有容器 # ./deploy.sh clean # 停止并清理(含数据卷) set -e DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(dirname "$(dirname "$DEPLOY_DIR")")" source "$DEPLOY_DIR/.env" # ===== 版本号 ===== GIT_HASH=$(cd "$ROOT_DIR" && git rev-parse --short HEAD) TIMESTAMP=$(date +%Y%m%d-%H%M%S) VERSION="${TIMESTAMP}-${GIT_HASH}" BRANCH=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD) COMMIT=$(cd "$ROOT_DIR" && git log -1 --format='%h %s') BACKUP_FILE="$DEPLOY_DIR/.last-version" CURRENT_FILE="$DEPLOY_DIR/.current-version" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_step() { echo -e "${CYAN}[STEP]${NC} $1"; } if docker compose version > /dev/null 2>&1; then DC="docker compose -f $DEPLOY_DIR/docker-compose.yml" else DC="docker-compose -f $DEPLOY_DIR/docker-compose.yml" fi # ===== 飞书通知 ===== notify_feishu() { local title="$1" content="$2" color="$3" [ -z "$FEISHU_WEBHOOK" ] && return curl -s -X POST "$FEISHU_WEBHOOK" \ -H "Content-Type: application/json" \ -d "{ \"msg_type\": \"interactive\", \"card\": { \"header\": {\"title\":{\"tag\":\"plain_text\",\"content\":\"$title\"},\"template\":\"$color\"}, \"elements\": [{\"tag\":\"markdown\",\"content\":\"$content\"}] } }" > /dev/null 2>&1 } # ===== 检查 Docker ===== check_docker() { if ! command -v docker &> /dev/null; then log_error "Docker 未安装" exit 1 fi if ! docker info > /dev/null 2>&1; then log_error "Docker 未启动" exit 1 fi } # ===== 备份当前版本号 ===== backup_version() { if [ -f "$CURRENT_FILE" ]; then cp "$CURRENT_FILE" "$BACKUP_FILE" log_info "已备份当前版本: $(cat "$BACKUP_FILE")" fi } # ===== 本地构建产物 ===== build_local() { local target="${1:-all}" local start_time=$(date +%s) # 切换 Java 8 export JAVA_HOME="$HOME/.sdkman/candidates/java/8.0.432-zulu" export PATH="$JAVA_HOME/bin:$PATH" cd "$ROOT_DIR/backend" # 先安装 intellectual-public(complex 依赖它) if [ "$target" = "all" ] || [ "$target" = "complex" ]; then log_info " 安装 xiaoqu-intellectual-public..." mvn clean install -pl xiaoqu-intellectual-public -DskipTests -q fi # 从 app-parent 用 docker profile 构建 cd "$ROOT_DIR/backend/xiaoqu-app-parent" case "$target" in complex) log_step "构建 xiaoqu-complex (-P docker)..." mvn clean package -pl ../xiaoqu-complex -am -P docker -DskipTests -q ;; mall) log_step "构建 xiaoqu-mall (-P docker)..." mvn clean package -pl ../xiaoqu-mall -am -P docker -DskipTests -q ;; all) log_step "构建全部 App 模块 (-P docker)..." mvn clean package -P docker -DskipTests -q ;; esac # 验证产物 case "$target" in complex|all) [ ! -f "$ROOT_DIR/backend/xiaoqu-complex/target/ROOT.war" ] && log_error "complex ROOT.war 不存在" && exit 1 log_info " complex: $(du -h "$ROOT_DIR/backend/xiaoqu-complex/target/ROOT.war" | cut -f1)" ;;& mall|all) [ ! -f "$ROOT_DIR/backend/xiaoqu-mall/target/ROOT.war" ] && log_error "mall ROOT.war 不存在" && exit 1 log_info " mall: $(du -h "$ROOT_DIR/backend/xiaoqu-mall/target/ROOT.war" | cut -f1)" ;; esac local elapsed=$(( $(date +%s) - start_time )) log_info "本地构建完成 (${elapsed}s)" } # ===== 打包 Docker 镜像 ===== build_images() { local target="${1:-all}" local start_time=$(date +%s) cd "$DEPLOY_DIR" case "$target" in complex) log_step "打包 Complex 镜像 ($VERSION)..." $DC build --no-cache complex docker tag smartclean-complex:latest smartclean-complex:$VERSION 2>/dev/null || true ;; mall) log_step "打包 Mall 镜像 ($VERSION)..." $DC build --no-cache mall docker tag smartclean-mall:latest smartclean-mall:$VERSION 2>/dev/null || true ;; all) log_step "打包全部镜像 ($VERSION)..." $DC build --no-cache docker tag smartclean-complex:latest smartclean-complex:$VERSION 2>/dev/null || true docker tag smartclean-mall:latest smartclean-mall:$VERSION 2>/dev/null || true ;; esac local elapsed=$(( $(date +%s) - start_time )) log_info "镜像打包完成 (${elapsed}s)" } # ===== 部署服务 ===== deploy_services() { local target="${1:-all}" cd "$DEPLOY_DIR" log_step "部署容器..." case "$target" in complex) $DC up -d --no-deps complex ;; mall) $DC up -d --no-deps mall ;; all) $DC down 2>/dev/null || true $DC up -d ;; esac log_info "容器已启动" } # ===== 健康检查 ===== healthcheck() { local target="${1:-all}" local max_retries=30 local interval=3 log_step "健康检查..." if [ "$target" = "all" ] || [ "$target" = "complex" ]; then log_info " 检查 Complex (http://localhost:$COMPLEX_PORT)..." for i in $(seq 1 $max_retries); do if curl -sf "http://localhost:$COMPLEX_PORT/" > /dev/null 2>&1; then log_info " Complex 健康 (第${i}次检查通过)" break fi if [ $i -eq $max_retries ]; then log_error " Complex 健康检查失败" return 1 fi printf "." sleep $interval done fi if [ "$target" = "all" ] || [ "$target" = "mall" ]; then log_info " 检查 Mall (http://localhost:$MALL_PORT)..." for i in $(seq 1 $max_retries); do if curl -sf "http://localhost:$MALL_PORT/" > /dev/null 2>&1; then log_info " Mall 健康 (第${i}次检查通过)" break fi if [ $i -eq $max_retries ]; then log_error " Mall 健康检查失败" return 1 fi printf "." sleep $interval done fi log_info "健康检查通过" return 0 } # ===== 回滚 ===== rollback() { if [ ! -f "$BACKUP_FILE" ]; then log_error "没有可回滚的版本" exit 1 fi local old_version=$(cat "$BACKUP_FILE") log_warn "回滚到版本: $old_version" local has_images=true docker image inspect smartclean-complex:$old_version > /dev/null 2>&1 || has_images=false docker image inspect smartclean-mall:$old_version > /dev/null 2>&1 || has_images=false if [ "$has_images" = false ]; then log_error "旧版本镜像不存在 ($old_version),无法回滚" exit 1 fi export VERSION="$old_version" cd "$DEPLOY_DIR" $DC down 2>/dev/null || true $DC up -d echo "$old_version" > "$CURRENT_FILE" log_info "回滚完成" notify_feishu "SmartClean App 已回滚 (Docker)" \ "**回滚版本:** $old_version" \ "yellow" } # ===== 清理旧镜像 ===== cleanup_images() { log_info "清理旧镜像(保留最近 5 个版本)..." for name in smartclean-complex smartclean-mall; do docker images "$name" --format "{{.Tag}}" | grep -v "latest" | sort -r | tail -n +6 | while read tag; do docker rmi "$name:$tag" 2>/dev/null && echo " 删除 $name:$tag" done done } # ===== 主流程 ===== TARGET="${1:-all}" DEPLOY_START=$(date +%s) case "$TARGET" in status) $DC ps exit 0 ;; logs) shift $DC logs -f $@ exit 0 ;; stop) log_info "停止所有容器..." $DC down log_info "已停止" exit 0 ;; clean) log_warn "停止所有容器并清理数据卷..." $DC down -v --rmi all log_info "已清理" exit 0 ;; rollback) check_docker rollback exit 0 ;; all|complex|mall) ;; *) echo "用法: $0 {all|complex|mall|rollback|status|logs|stop|clean}" exit 1 ;; esac echo "" log_info "======================================" log_info " SmartClean App 部署 (Docker)" log_info " 版本: $VERSION" log_info " 分支: $BRANCH" log_info " 提交: $COMMIT" log_info " 目标: $TARGET" log_info "======================================" echo "" # 1. 检查 Docker check_docker # 2. 备份当前版本号 backup_version # 3. 本地构建(Maven -P docker) build_local "$TARGET" # 4. 打包 Docker 镜像 build_images "$TARGET" # 5. 部署容器 deploy_services "$TARGET" # 6. 健康检查 if healthcheck "$TARGET"; then echo "$VERSION" > "$CURRENT_FILE" cleanup_images ELAPSED=$(( $(date +%s) - DEPLOY_START )) echo "" log_info "======================================" log_info " App 部署成功!" log_info " 版本: $VERSION" log_info " 耗时: ${ELAPSED}s" log_info " Complex: http://localhost:$COMPLEX_PORT" log_info " Mall: http://localhost:$MALL_PORT" log_info "======================================" notify_feishu "SmartClean App 部署成功 (Docker)" \ "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**目标:** $TARGET\\n**耗时:** ${ELAPSED}s\\n**Complex:** http://localhost:$COMPLEX_PORT\\n**Mall:** http://localhost:$MALL_PORT" \ "green" else log_error "健康检查失败,自动回滚..." rollback ELAPSED=$(( $(date +%s) - DEPLOY_START )) notify_feishu "SmartClean App 部署失败 (Docker,已回滚)" \ "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**耗时:** ${ELAPSED}s\\n**状态:** 健康检查失败,已自动回滚" \ "red" exit 1 fi