diff --git a/admin/Dockerfile b/admin/Dockerfile new file mode 100644 index 0000000..6df9130 --- /dev/null +++ b/admin/Dockerfile @@ -0,0 +1,45 @@ +# ==================== 第一阶段:编译 ==================== +FROM maven:3.8-openjdk-17 AS builder +WORKDIR /build + +# 阿里云 Maven 镜像加速 +COPY settings.xml /root/.m2/settings.xml + +# 先拷贝 pom 利用 Docker 缓存层(依赖不变时跳过下载) +COPY pom.xml . +COPY common/pom.xml common/pom.xml +COPY manager/pom.xml manager/pom.xml +COPY admin/pom.xml admin/pom.xml + +# 拷贝源码 +COPY . . + +# 构建(从父 pom 编译,admin 依赖 common 和 manager) +RUN --mount=type=cache,target=/root/.m2/repository \ + mvn clean package -pl admin -am -DskipTests + +# ==================== 第二阶段:运行 ==================== +FROM alibabadragonwell/dragonwell:17-anolis + +ENV TZ=Asia/Shanghai +ENV PROFILES_ACTIVE=prod +ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \ + -XX:+UseG1GC \ + -XX:+HeapDumpOnOutOfMemoryError \ + -XX:HeapDumpPath=/app/logs/heapdump.hprof \ + -Dfile.encoding=UTF-8 \ + -Duser.timezone=Asia/Shanghai" + +# 时区 + curl(健康检查用) +RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \ + && yum install -y curl && yum clean all + +RUN mkdir -p /app/logs + +WORKDIR /app + +EXPOSE 8081 + +COPY --from=builder /build/admin/target/admin.jar ./admin.jar + +CMD java $JAVA_OPTS -jar admin.jar diff --git a/admin/Jenkinsfile b/admin/Jenkinsfile new file mode 100644 index 0000000..dc2588e --- /dev/null +++ b/admin/Jenkinsfile @@ -0,0 +1,202 @@ +/** + * OfferPie Backend Admin 蓝绿部署流水线 + * + * 架构:Jenkins 本地编译 → scp 镜像到目标机 → SSH 远程蓝绿切换 + * 目标机目录:/opt/offerpie/admin/ + */ +pipeline { + agent any + + parameters { + choice(name: 'BRANCH', choices: ['master', 'pre', 'dev', 'test'], description: '选择要部署的分支') + } + + environment { + // 目标服务器配置 + DEPLOY_HOST = '8.148.237.97' + DEPLOY_PORT = '22' + DEPLOY_USER = 'root' + DEPLOY_PASS = 'Mzpy520@126.com' + + // 项目配置 + IMAGE_NAME = 'offerpie-backend-admin' + IMAGE_TAG = 'latest' + CONTAINER_PREFIX = 'offerpie-backend-admin' + REMOTE_DIR = '/opt/offerpie/admin' + HEALTH_URL = 'http://localhost:8081/admin/public/actuator/health' + + // SSH 命令前缀 + SSH_CMD = "sshpass -p '${DEPLOY_PASS}' ssh -o StrictHostKeyChecking=no -p ${DEPLOY_PORT} ${DEPLOY_USER}@${DEPLOY_HOST}" + SCP_CMD = "sshpass -p '${DEPLOY_PASS}' scp -o StrictHostKeyChecking=no -P ${DEPLOY_PORT}" + } + + stages { + stage('环境检查') { + steps { + sh 'sshpass -V' + } + } + + stage('拉取代码') { + steps { + echo "拉取 ${params.BRANCH} 分支代码" + git branch: "${params.BRANCH}", + credentialsId: 'gitea-fab089c1-b55d-4b58-9fad', + url: 'http://git.jianshixingqiu.com/offerpai/offerpai_backend.git' + } + } + + stage('本地编译') { + steps { + echo "开始构建镜像" + sh "docker build -f admin/Dockerfile -t ${IMAGE_NAME}:${IMAGE_TAG} ." + echo "导出镜像" + sh "docker save -o ${IMAGE_NAME}.tar ${IMAGE_NAME}:${IMAGE_TAG}" + } + } + + stage('文件传输') { + steps { + echo "传输文件到目标服务器" + sh "${SSH_CMD} 'mkdir -p ${REMOTE_DIR}'" + sh "${SCP_CMD} ${IMAGE_NAME}.tar ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/" + sh "${SCP_CMD} admin/nginx.conf ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/nginx.conf" + sh "${SCP_CMD} docker-compose.admin.yml ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/docker-compose.yml" + } + } + + stage('加载镜像') { + steps { + sh "${SSH_CMD} 'docker load < ${REMOTE_DIR}/${IMAGE_NAME}.tar'" + } + } + + stage('检测部署目标') { + steps { + script { + def blueRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-blue'", returnStdout: true).trim() + def greenRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-green'", returnStdout: true).trim() + + env.DEPLOY_TARGET = '' + + if (blueRunning && !greenRunning) { + env.DEPLOY_TARGET = 'green' + } + if (greenRunning && !blueRunning) { + env.DEPLOY_TARGET = 'blue' + } + if (!env.DEPLOY_TARGET) { + echo "当前环境未部署服务或状态异常,默认部署 blue" + env.DEPLOY_TARGET = 'blue' + } + + env.OTHER_TARGET = (env.DEPLOY_TARGET == 'blue') ? 'green' : 'blue' + echo "当前激活: ${env.OTHER_TARGET},即将部署: ${env.DEPLOY_TARGET}" + } + } + } + + stage('启动新版本') { + steps { + script { + def existingContainer = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'", returnStdout: true).trim() + if (existingContainer) { + sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'" + } + sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d ${env.DEPLOY_TARGET}'" + sh 'sleep 35' + } + } + } + + stage('检查Nginx') { + steps { + script { + def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim() + if (!nginxExists) { + echo "首次部署,初始化 Nginx 容器" + sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d nginx'" + sh 'sleep 3' + } else { + def nginxRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim() + if (!nginxRunning) { + echo "Nginx 容器已停止,重新启动" + sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'" + sh 'sleep 2' + } + echo "Nginx 容器已存在且运行中" + } + // docker cp nginx.conf 进容器(覆盖更新配置模板) + sh "${SSH_CMD} 'docker cp ${REMOTE_DIR}/nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf'" + } + } + } + + stage('健康检查') { + steps { + script { + def maxRetries = 3 + def retryCount = 0 + def healthy = false + + while (retryCount < maxRetries && !healthy) { + try { + sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'" + healthy = true + } catch (Exception e) { + retryCount++ + if (retryCount < maxRetries) { + echo "健康检查失败,5秒后重试 (${retryCount}/${maxRetries})" + sleep 5 + } + } + } + + if (!healthy) { + error "健康检查失败,部署中止" + } + echo "✅ ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} 健康检查通过" + } + } + } + + stage('切换流量') { + steps { + script { + sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx sed -i \"s/proxy_pass http:\\/\\/\\(blue\\|green\\):8081;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:8081;/\" /etc/nginx/nginx.conf'" + sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'" + echo "✅ 流量已切换到 ${env.DEPLOY_TARGET}" + } + } + } + + stage('删除旧版本') { + steps { + script { + def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim() + if (otherExists) { + sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'" + echo "旧版本 ${env.OTHER_TARGET} 已删除" + } + } + } + } + + stage('清理') { + steps { + sh "${SSH_CMD} 'rm -f ${REMOTE_DIR}/${IMAGE_NAME}.tar'" + sh "rm -f ${IMAGE_NAME}.tar" + } + } + } + + post { + success { + echo "✅ 蓝绿部署成功!当前运行: ${env.DEPLOY_TARGET}" + } + failure { + echo '❌ 部署失败,请检查日志' + sh "rm -f ${IMAGE_NAME}.tar || true" + } + } +} diff --git a/admin/Jenkinsfile.test b/admin/Jenkinsfile.test new file mode 100644 index 0000000..69cc0ef --- /dev/null +++ b/admin/Jenkinsfile.test @@ -0,0 +1,210 @@ +/** + * OfferPie Backend Admin 蓝绿部署流水线(测试环境) + * + * 架构:Jenkins 本地编译 → scp 镜像到目标机 → SSH 远程蓝绿切换 + * 目标机目录:/opt/offerpie/admin/ + * + * 与生产差异: + * - 部署目标为测试服务器 + * - 默认分支 test + * - 镜像 tag = test(与生产 latest 隔离,避免同一 Jenkins 并发构建串包) + * - tar 文件名带 tag(双保险) + * - 运行时 PROFILES_ACTIVE=test(在 docker-compose.test.admin.yml 中设置) + */ +pipeline { + agent any + + parameters { + choice(name: 'BRANCH', choices: ['test', 'dev', 'pre', 'master'], description: '选择要部署的分支') + } + + environment { + // 目标服务器配置(测试) + DEPLOY_HOST = '8.163.131.234' + DEPLOY_PORT = '22' + DEPLOY_USER = 'root' + DEPLOY_PASS = 'Mzpy520@126.com' + + // 项目配置 + IMAGE_NAME = 'offerpie-backend-admin' + IMAGE_TAG = 'test' + IMAGE_TAR = 'offerpie-backend-admin-test.tar' + CONTAINER_PREFIX = 'offerpie-backend-admin' + REMOTE_DIR = '/opt/offerpie/admin' + HEALTH_URL = 'http://localhost:8081/admin/public/actuator/health' + + // SSH 命令前缀 + SSH_CMD = "sshpass -p '${DEPLOY_PASS}' ssh -o StrictHostKeyChecking=no -p ${DEPLOY_PORT} ${DEPLOY_USER}@${DEPLOY_HOST}" + SCP_CMD = "sshpass -p '${DEPLOY_PASS}' scp -o StrictHostKeyChecking=no -P ${DEPLOY_PORT}" + } + + stages { + stage('环境检查') { + steps { + sh 'sshpass -V' + } + } + + stage('拉取代码') { + steps { + echo "拉取 ${params.BRANCH} 分支代码" + git branch: "${params.BRANCH}", + credentialsId: 'gitea-fab089c1-b55d-4b58-9fad', + url: 'http://git.jianshixingqiu.com/offerpai/offerpai_backend.git' + } + } + + stage('本地编译') { + steps { + echo "开始构建镜像" + sh "docker build -f admin/Dockerfile -t ${IMAGE_NAME}:${IMAGE_TAG} ." + echo "导出镜像" + sh "docker save -o ${IMAGE_TAR} ${IMAGE_NAME}:${IMAGE_TAG}" + } + } + + stage('文件传输') { + steps { + echo "传输文件到目标服务器" + sh "${SSH_CMD} 'mkdir -p ${REMOTE_DIR}'" + sh "${SCP_CMD} ${IMAGE_TAR} ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/" + sh "${SCP_CMD} admin/nginx.conf ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/nginx.conf" + sh "${SCP_CMD} docker-compose.test.admin.yml ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/docker-compose.yml" + } + } + + stage('加载镜像') { + steps { + sh "${SSH_CMD} 'docker load < ${REMOTE_DIR}/${IMAGE_TAR}'" + } + } + + stage('检测部署目标') { + steps { + script { + def blueRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-blue'", returnStdout: true).trim() + def greenRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-green'", returnStdout: true).trim() + + env.DEPLOY_TARGET = '' + + if (blueRunning && !greenRunning) { + env.DEPLOY_TARGET = 'green' + } + if (greenRunning && !blueRunning) { + env.DEPLOY_TARGET = 'blue' + } + if (!env.DEPLOY_TARGET) { + echo "当前环境未部署服务或状态异常,默认部署 blue" + env.DEPLOY_TARGET = 'blue' + } + + env.OTHER_TARGET = (env.DEPLOY_TARGET == 'blue') ? 'green' : 'blue' + echo "当前激活: ${env.OTHER_TARGET},即将部署: ${env.DEPLOY_TARGET}" + } + } + } + + stage('启动新版本') { + steps { + script { + def existingContainer = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'", returnStdout: true).trim() + if (existingContainer) { + sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'" + } + sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d ${env.DEPLOY_TARGET}'" + sh 'sleep 70' + } + } + } + + stage('检查Nginx') { + steps { + script { + def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim() + if (!nginxExists) { + echo "首次部署,初始化 Nginx 容器" + sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d nginx'" + sh 'sleep 3' + } else { + def nginxRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim() + if (!nginxRunning) { + echo "Nginx 容器已停止,重新启动" + sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'" + sh 'sleep 2' + } + echo "Nginx 容器已存在且运行中" + } + // docker cp nginx.conf 进容器(覆盖更新配置模板) + sh "${SSH_CMD} 'docker cp ${REMOTE_DIR}/nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf'" + } + } + } + + stage('健康检查') { + steps { + script { + def maxRetries = 3 + def retryCount = 0 + def healthy = false + + while (retryCount < maxRetries && !healthy) { + try { + sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'" + healthy = true + } catch (Exception e) { + retryCount++ + if (retryCount < maxRetries) { + echo "健康检查失败,5秒后重试 (${retryCount}/${maxRetries})" + sleep 5 + } + } + } + + if (!healthy) { + error "健康检查失败,部署中止" + } + echo "✅ ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} 健康检查通过" + } + } + } + + stage('切换流量') { + steps { + script { + sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx sed -i \"s/proxy_pass http:\\/\\/\\(blue\\|green\\):8081;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:8081;/\" /etc/nginx/nginx.conf'" + sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'" + echo "✅ 流量已切换到 ${env.DEPLOY_TARGET}" + } + } + } + + stage('删除旧版本') { + steps { + script { + def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim() + if (otherExists) { + sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'" + echo "旧版本 ${env.OTHER_TARGET} 已删除" + } + } + } + } + + stage('清理') { + steps { + sh "${SSH_CMD} 'rm -f ${REMOTE_DIR}/${IMAGE_TAR}'" + sh "rm -f ${IMAGE_TAR}" + } + } + } + + post { + success { + echo "✅ 蓝绿部署成功!当前运行: ${env.DEPLOY_TARGET}" + } + failure { + echo '❌ 部署失败,请检查日志' + sh "rm -f ${IMAGE_TAR} || true" + } + } +} diff --git a/admin/nginx.conf b/admin/nginx.conf new file mode 100644 index 0000000..3c7c958 --- /dev/null +++ b/admin/nginx.conf @@ -0,0 +1,54 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + + client_max_body_size 20m; + + server { + listen 80; + server_name _; + + # Nginx 自身健康检查 + location /health { + access_log off; + return 200 'ok'; + add_header Content-Type text/plain; + } + + # 默认代理到 blue,部署时通过 sed 切换 + location / { + proxy_pass http://blue:8081; + 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 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + } + } +} diff --git a/docker-compose.admin.yml b/docker-compose.admin.yml new file mode 100644 index 0000000..f12d2ae --- /dev/null +++ b/docker-compose.admin.yml @@ -0,0 +1,65 @@ +services: + nginx: + image: nginx:alpine + container_name: offerpie-backend-admin-nginx + restart: unless-stopped + ports: + - "10062:80" + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + deploy: + resources: + limits: + memory: 256M + cpus: '1' + + blue: + image: offerpie-backend-admin:latest + container_name: offerpie-backend-admin-blue + restart: unless-stopped + expose: + - "8081" + environment: + - APP_VERSION=blue + - PROFILES_ACTIVE=prod + - TZ=Asia/Shanghai + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/admin/public/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + volumes: + - /logs/offerpie-backend-admin:/app/logs + deploy: + resources: + limits: + memory: 1G + cpus: '1' + + green: + image: offerpie-backend-admin:latest + container_name: offerpie-backend-admin-green + restart: unless-stopped + expose: + - "8081" + environment: + - APP_VERSION=green + - PROFILES_ACTIVE=prod + - TZ=Asia/Shanghai + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/admin/public/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + volumes: + - /logs/offerpie-backend-admin:/app/logs + deploy: + resources: + limits: + memory: 1G + cpus: '1' diff --git a/docker-compose.test.admin.yml b/docker-compose.test.admin.yml new file mode 100644 index 0000000..5d82126 --- /dev/null +++ b/docker-compose.test.admin.yml @@ -0,0 +1,65 @@ +services: + nginx: + image: nginx:alpine + container_name: offerpie-backend-admin-nginx + restart: unless-stopped + ports: + - "10062:80" + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + deploy: + resources: + limits: + memory: 256M + cpus: '1' + + blue: + image: offerpie-backend-admin:test + container_name: offerpie-backend-admin-blue + restart: unless-stopped + expose: + - "8081" + environment: + - APP_VERSION=blue + - PROFILES_ACTIVE=test + - TZ=Asia/Shanghai + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/admin/public/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + volumes: + - /logs/offerpie-backend-admin:/app/logs + deploy: + resources: + limits: + memory: 1G + cpus: '1' + + green: + image: offerpie-backend-admin:test + container_name: offerpie-backend-admin-green + restart: unless-stopped + expose: + - "8081" + environment: + - APP_VERSION=green + - PROFILES_ACTIVE=test + - TZ=Asia/Shanghai + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/admin/public/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + volumes: + - /logs/offerpie-backend-admin:/app/logs + deploy: + resources: + limits: + memory: 1G + cpus: '1'