From 6e012584acef46af14b68e0642238d0eeaa02ecd Mon Sep 17 00:00:00 2001 From: ailuckly Date: Tue, 31 Mar 2026 21:33:33 +0800 Subject: [PATCH] chore: streamline startup and staging deployment --- .env.example | 71 +++ .github/workflows/cd-production.yml | 8 +- .github/workflows/cd-staging.yml | 588 +++--------------- .github/workflows/emergency-rollback.yml | 8 +- .github/workflows/release.yml | 6 +- .gitignore | 3 +- docker-compose.prod.yml | 11 +- docker-compose.test.yml | 30 +- docker-compose.yml | 106 +++- ...00\345\217\221\347\216\257\345\242\203.md" | 57 ++ docs/GitHub-Staging-Secrets.md | 68 ++ ...50\347\275\262\346\226\207\346\241\243.md" | 3 + vocata-admin/.dockerignore | 16 + vocata-admin/Dockerfile | 40 +- vocata-server/.dockerignore | 4 +- vocata-server/Dockerfile | 56 +- vocata-server/Dockerfile.ci | 37 +- .../com/vocata/file/config/QiniuConfig.java | 15 +- .../vocata/file/config/QiniuProperties.java | 12 +- .../file/service/impl/FileServiceImpl.java | 22 +- vocata-web/.dockerignore | 16 + vocata-web/Dockerfile | 34 +- 22 files changed, 521 insertions(+), 690 deletions(-) create mode 100644 .env.example create mode 100644 "docs/Docker\345\274\200\345\217\221\347\216\257\345\242\203.md" create mode 100644 docs/GitHub-Staging-Secrets.md create mode 100644 vocata-admin/.dockerignore create mode 100644 vocata-web/.dockerignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7b67f7f --- /dev/null +++ b/.env.example @@ -0,0 +1,71 @@ +# Docker Compose development defaults +COMPOSE_PROJECT_NAME=vocata-dev + +# Core service ports +POSTGRES_PORT=5432 +REDIS_EXPOSE_PORT=6379 +SERVER_PORT=9009 +WEB_PORT=3000 +ADMIN_PORT=3001 +PGADMIN_PORT=5050 +MAILHOG_SMTP_PORT=1025 +MAILHOG_WEB_PORT=8025 + +# Backend profile +SPRING_PROFILES_ACTIVE=local + +# Database +POSTGRES_DB=vocata_db +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres123 +DB_HOST=postgres +DB_PORT=5432 +DB_NAME=vocata_db +DB_USERNAME=postgres +DB_PASSWORD=postgres123 + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DATABASE=0 + +# Frontend build-time API target +VITE_APP_URL=http://localhost:9009 +WEB_BUILD_MODE=production +ADMIN_BUILD_MODE=production + +# Optional local mail +MAIL_USERNAME= +MAIL_PASSWORD= + +# Xunfei TTS (used when AI_TTS_PROVIDER=xunfei) +XUNFEI_TTS_APP_ID= +XUNFEI_TTS_API_KEY= +XUNFEI_TTS_SECRET_KEY= + +# Third-party object storage / AI +# Current backend startup still expects these entries to exist for a complete run. +QINIU_ACCESS_KEY= +QINIU_SECRET_KEY= +QINIU_BUCKET=voice-mp3 +QINIU_DOMAIN=http://voice-mp3.s3.cn-north-1.qiniucs.com +QINIU_REGION=huabei +QINIU_KEY_PREFIX=Vocata +QINIU_AI_API_KEY= +QINIU_AI_BASE_URL=https://openai.qiniu.com/v1 +QINIU_AI_MODEL=x-ai/grok-4-fast +QINIU_AI_TIMEOUT=60 + +GEMINI_API_KEY= +GEMINI_MODEL=gemini-2.5-flash-lite + +OPENAI_API_KEY= +OPENAI_MODEL=gpt-3.5-turbo + +SILICONFLOW_API_KEY= +SILICONFLOW_AI_MODEL=Qwen/Qwen3-8B + +# Optional admin tools +PGADMIN_DEFAULT_EMAIL=admin@vocata.com +PGADMIN_DEFAULT_PASSWORD=admin123 diff --git a/.github/workflows/cd-production.yml b/.github/workflows/cd-production.yml index 73df012..6dc4880 100644 --- a/.github/workflows/cd-production.yml +++ b/.github/workflows/cd-production.yml @@ -466,7 +466,7 @@ jobs: case $service in "vocata-server") for i in {1..6}; do - if curl -f -m 10 "http://localhost:9009/api/actuator/health" > /dev/null 2>&1; then + if curl -f -m 10 "http://localhost:9009/api/health" > /dev/null 2>&1; then echo "✓ 新的后端服务健康检查通过" break fi @@ -504,7 +504,7 @@ jobs: for service in "${SERVICES_TO_UPDATE[@]}"; do case $service in "vocata-server") - if ! curl -f -m 30 "http://localhost:9009/api/actuator/health" > /dev/null 2>&1; then + if ! curl -f -m 30 "http://localhost:9009/api/health" > /dev/null 2>&1; then echo "✗ 后端服务最终健康检查失败" HEALTH_FAILED=true else @@ -649,7 +649,7 @@ jobs: # API健康检查 (假设使用域名) echo "验证后端API..." - if curl -f -m 60 "https://api.vocata.com/api/actuator/health" > /dev/null 2>&1; then + if curl -f -m 60 "https://api.vocata.com/api/health" > /dev/null 2>&1; then echo "✅ 后端API验证通过" >> $GITHUB_STEP_SUMMARY else echo "❌ 后端API验证失败" >> $GITHUB_STEP_SUMMARY @@ -683,4 +683,4 @@ jobs: echo "⚠️ **生产环境验证部分失败**" >> $GITHUB_STEP_SUMMARY echo "请立即检查相关服务状态。" >> $GITHUB_STEP_SUMMARY exit 1 - fi \ No newline at end of file + fi diff --git a/.github/workflows/cd-staging.yml b/.github/workflows/cd-staging.yml index 0042655..2cbcc3e 100644 --- a/.github/workflows/cd-staging.yml +++ b/.github/workflows/cd-staging.yml @@ -2,547 +2,125 @@ name: Deploy to Staging on: push: - branches: [ develop ] + branches: [develop] workflow_dispatch: - inputs: - force_rebuild: - description: '强制重建所有镜像' - required: false - default: false - type: boolean - skip_health_check: - description: '跳过健康检查(快速部署)' - required: false - default: false - type: boolean permissions: contents: read - packages: write - actions: read -env: - REGISTRY: ghcr.io - JAVA_VERSION: '17' - NODE_VERSION: '20' +concurrency: + group: staging-deploy + cancel-in-progress: true jobs: - # 检测变更并决定构建策略 - detect-changes: - runs-on: ubuntu-latest - outputs: - server-changed: ${{ steps.changes.outputs.server }} - web-changed: ${{ steps.changes.outputs.web }} - admin-changed: ${{ steps.changes.outputs.admin }} - image-tag: ${{ steps.tag.outputs.image-tag }} - steps: - - name: 检出代码 - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: 检测文件变更 - uses: dorny/paths-filter@v2 - id: changes - with: - filters: | - server: - - 'vocata-server/**' - - 'pom.xml' - - '.github/workflows/**' - web: - - 'vocata-web/**' - - '.github/workflows/**' - admin: - - 'vocata-admin/**' - - '.github/workflows/**' - - - name: 生成镜像标签 - id: tag - run: | - IMAGE_TAG="staging-$(date +'%Y%m%d-%H%M%S')-${GITHUB_SHA:0:8}" - echo "image-tag=$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "生成镜像标签: $IMAGE_TAG" - - - name: 强制重建检查 - run: | - if [[ "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - echo "server-changed=true" >> $GITHUB_OUTPUT - echo "web-changed=true" >> $GITHUB_OUTPUT - echo "admin-changed=true" >> $GITHUB_OUTPUT - echo "强制重建所有组件" - fi - - # 构建后端镜像 - build-server: - runs-on: ubuntu-latest - needs: detect-changes - if: needs.detect-changes.outputs.server-changed == 'true' || github.event.inputs.force_rebuild == 'true' - outputs: - image: ${{ steps.meta.outputs.tags }} - steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 设置 JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4 - with: - java-version: ${{ env.JAVA_VERSION }} - distribution: 'temurin' - - - name: 缓存 Maven 依赖 - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - - - name: 构建后端应用 - working-directory: ./vocata-server - run: mvn clean package -DskipTests -Dspring.profiles.active=test - - - name: 设置 Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: 登录 GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: 提取镜像元数据 - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/veardk/vocata-server - tags: | - type=raw,value=${{ needs.detect-changes.outputs.image-tag }} - type=raw,value=staging-latest - - - name: 构建并推送镜像 - uses: docker/build-push-action@v5 - with: - context: . - file: ./vocata-server/Dockerfile.ci - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=vocata-server-staging - cache-to: type=gha,mode=max,scope=vocata-server-staging - - # 构建前端客户端镜像 - build-web: - runs-on: ubuntu-latest - needs: detect-changes - if: needs.detect-changes.outputs.web-changed == 'true' || github.event.inputs.force_rebuild == 'true' - outputs: - image: ${{ steps.meta.outputs.tags }} - steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 设置 Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: vocata-web/package-lock.json - - - name: 准备前端环境配置 - working-directory: ./vocata-web - run: | - # 显示替换前的配置 - echo "=== 替换前的 .env.test 文件 ===" - cat .env.test - - # 替换测试环境配置中的占位符 - sed -i "s/{{STAGING_HOST}}/${{ secrets.STAGING_HOST }}/g" .env.test - - # 显示替换后的配置 - echo "=== 替换后的 .env.test 文件 ===" - cat .env.test - echo "前端环境配置已更新" - - - name: 构建前端应用 - working-directory: ./vocata-web - run: | - npm ci - npm run build:test - - - name: 设置 Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: 登录 GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: 提取镜像元数据 - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/veardk/vocata-web - tags: | - type=raw,value=${{ needs.detect-changes.outputs.image-tag }} - type=raw,value=staging-latest - - - name: 构建并推送镜像 - uses: docker/build-push-action@v5 - with: - context: ./vocata-web - file: ./vocata-web/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=vocata-web-staging - cache-to: type=gha,mode=max,scope=vocata-web-staging - - # 构建管理后台镜像 - build-admin: - runs-on: ubuntu-latest - needs: detect-changes - if: needs.detect-changes.outputs.admin-changed == 'true' || github.event.inputs.force_rebuild == 'true' - outputs: - image: ${{ steps.meta.outputs.tags }} - steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 设置 Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: vocata-admin/package-lock.json - - - name: 准备管理后台环境配置 - working-directory: ./vocata-admin - run: | - # 显示替换前的配置 - echo "=== 替换前的 .env.test 文件 ===" - cat .env.test - - # 替换测试环境配置中的占位符 - sed -i "s/{{STAGING_HOST}}/${{ secrets.STAGING_HOST }}/g" .env.test - - # 显示替换后的配置 - echo "=== 替换后的 .env.test 文件 ===" - cat .env.test - echo "管理后台环境配置已更新" - - - name: 构建管理后台应用 - working-directory: ./vocata-admin - run: | - npm ci - npm run build:test - - - name: 设置 Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: 登录 GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: 提取镜像元数据 - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/veardk/vocata-admin - tags: | - type=raw,value=${{ needs.detect-changes.outputs.image-tag }} - type=raw,value=staging-latest - - - name: 构建并推送镜像 - uses: docker/build-push-action@v5 - with: - context: ./vocata-admin - file: ./vocata-admin/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=vocata-admin-staging - cache-to: type=gha,mode=max,scope=vocata-admin-staging - - # 部署到测试服务器 deploy-staging: runs-on: ubuntu-latest - needs: [detect-changes, build-server, build-web, build-admin] - if: always() && (needs.build-server.result == 'success' || needs.build-server.result == 'skipped') && - (needs.build-web.result == 'success' || needs.build-web.result == 'skipped') && - (needs.build-admin.result == 'success' || needs.build-admin.result == 'skipped') environment: name: staging steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 准备部署配置 - run: | - mkdir -p deploy/staging - - # 生成 .env 文件 - cat > deploy/staging/.env << EOF - # VocaTa 测试环境配置 - COMPOSE_PROJECT_NAME=vocata-staging - - # 镜像配置 - 使用staging-latest作为后备 - SERVER_IMAGE="ghcr.io/veardk/vocata-server:staging-latest" - WEB_IMAGE="ghcr.io/veardk/vocata-web:staging-latest" - ADMIN_IMAGE="ghcr.io/veardk/vocata-admin:staging-latest" - - # 应用配置 - SERVER_PORT=9009 - WEB_PORT=3000 - ADMIN_PORT=3001 - SPRING_PROFILES_ACTIVE=test - - # 数据库配置 - DB_HOST="${{ secrets.DB_HOST }}" - DB_PORT="${{ secrets.DB_PORT }}" - DB_NAME="${{ secrets.DB_NAME }}" - DB_USERNAME="${{ secrets.DB_USERNAME }}" - DB_PASSWORD="${{ secrets.DB_PASSWORD }}" - - # Redis配置 - REDIS_HOST="${{ secrets.REDIS_HOST }}" - REDIS_PORT="${{ secrets.REDIS_PORT }}" - REDIS_PASSWORD="${{ secrets.REDIS_PASSWORD }}" - REDIS_DATABASE="${{ secrets.REDIS_DATABASE }}" - - # 七牛云配置 - QINIU_ACCESS_KEY="${{ secrets.QINIU_ACCESS_KEY }}" - QINIU_SECRET_KEY="${{ secrets.QINIU_SECRET_KEY }}" - QINIU_BUCKET="${{ secrets.QINIU_BUCKET }}" - QINIU_DOMAIN="${{ secrets.QINIU_DOMAIN }}" - QINIU_REGION="${{ secrets.QINIU_REGION }}" - - # 邮箱配置 - EMAIL_USER_NAME="${{ secrets.EMAIL_USER_NAME }}" - EMAIL_USER_PASSWORD="${{ secrets.EMAIL_USER_PASSWORD }}" - - # 部署信息 - DEPLOY_TAG="${{ needs.detect-changes.outputs.image-tag }}" - DEPLOY_TIME="$(date '+%Y-%m-%d %H:%M:%S')" - DEPLOY_COMMIT="${{ github.sha }}" - SKIP_HEALTH_CHECK="${{ github.event.inputs.skip_health_check || 'false' }}" - EOF - - # 复制 docker-compose 配置 - cp docker-compose.test.yml deploy/staging/docker-compose.yml - - # 生成部署脚本 - cat > deploy/staging/deploy.sh << 'EOF' - #!/bin/bash - set -e - - echo "=== VocaTa 测试环境部署开始 ===" - echo "部署标签: $DEPLOY_TAG" - echo "提交版本: $DEPLOY_COMMIT" - - # 加载环境变量 - source .env - - # 登录 Docker Registry - echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "veardk" --password-stdin - - # 确定需要更新的服务 - SERVICES_TO_UPDATE=() - - if [[ "${{ needs.detect-changes.outputs.server-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - SERVICES_TO_UPDATE+=("vocata-server") - echo "✓ 将更新后端服务: $SERVER_IMAGE" - fi - - if [[ "${{ needs.detect-changes.outputs.web-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - SERVICES_TO_UPDATE+=("vocata-web") - echo "✓ 将更新前端客户端: $WEB_IMAGE" - fi - - if [[ "${{ needs.detect-changes.outputs.admin-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - SERVICES_TO_UPDATE+=("vocata-admin") - echo "✓ 将更新管理后台: $ADMIN_IMAGE" - fi - - if [ ${#SERVICES_TO_UPDATE[@]} -eq 0 ]; then - echo "⚠ 没有服务需要更新" - exit 0 - fi - - # 拉取最新镜像 - echo "📥 拉取镜像..." - for service in "${SERVICES_TO_UPDATE[@]}"; do - case $service in - "vocata-server") - echo "拉取后端镜像: $SERVER_IMAGE" - docker pull "$SERVER_IMAGE" - ;; - "vocata-web") - echo "拉取前端镜像: $WEB_IMAGE" - docker pull "$WEB_IMAGE" - ;; - "vocata-admin") - echo "拉取管理后台镜像: $ADMIN_IMAGE" - docker pull "$ADMIN_IMAGE" - ;; - esac - done - - # 滚动更新服务 - echo "🔄 更新服务..." - for service in "${SERVICES_TO_UPDATE[@]}"; do - echo "更新 $service..." - docker-compose up -d --no-deps $service - sleep 5 - done - - # 健康检查 - if [[ "$SKIP_HEALTH_CHECK" != "true" ]]; then - echo "🏥 健康检查..." - - health_check() { - local service=$1 - local port=$2 - local path=${3:-"/"} - local max_attempts=6 # 减少从12次到6次 - local attempt=1 - - while [ $attempt -le $max_attempts ]; do - if curl -f -m 5 "http://localhost:$port$path" > /dev/null 2>&1; then # 超时从10s减少到5s - echo "✓ $service 健康检查通过" - return 0 - else - echo "⏳ 等待 $service 启动... ($attempt/$max_attempts)" - sleep 5 # 间隔从10s减少到5s - ((attempt++)) - fi - done + - name: 部署到服务器 + uses: appleboy/ssh-action@v1.0.3 + env: + GIT_REF: ${{ github.ref_name }} + GIT_SHA: ${{ github.sha }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ github.token }} + with: + host: ${{ secrets.STAGING_HOST }} + username: ${{ secrets.STAGING_USER }} + key: ${{ secrets.STAGING_SSH_KEY }} + port: 22 + script_stop: true + envs: GIT_REF,GIT_SHA,GITHUB_REPOSITORY,GITHUB_TOKEN + script: | + set -e - echo "✗ $service 健康检查失败" - docker-compose logs --tail=20 $service # 减少日志行数从50到20 - return 1 - } + APP_DIR=/home/deploy/deploy/vocata + REPO_DIR=$APP_DIR/repo + ENV_FILE=$APP_DIR/.env + LEGACY_ENV_FILE=$APP_DIR/data/vocata-staging.env + REPO_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - # 执行健康检查 - HEALTH_FAILED=false + mkdir -p "$APP_DIR" - for service in "${SERVICES_TO_UPDATE[@]}"; do - case $service in - "vocata-server") - echo "⏭️ 跳过后端服务健康检查(服务已启动)" - ;; - "vocata-web") - if ! health_check "vocata-web" 3000 "/health"; then - HEALTH_FAILED=true - fi - ;; - "vocata-admin") - if ! health_check "vocata-admin" 3001 "/health"; then - HEALTH_FAILED=true - fi - ;; - esac - done + if [ ! -f "$ENV_FILE" ] && [ -f "$LEGACY_ENV_FILE" ]; then + cp "$LEGACY_ENV_FILE" "$ENV_FILE" + fi - if [[ "$HEALTH_FAILED" == "true" ]]; then - echo "❌ 部署失败: 健康检查未通过" - echo "正在回滚..." - docker-compose restart "${SERVICES_TO_UPDATE[@]}" + if [ ! -f "$ENV_FILE" ]; then + echo "缺少部署环境文件: $ENV_FILE" + echo "请先在服务器上准备好 .env,再重新执行工作流。" exit 1 fi - else - echo "⚡ 跳过健康检查(快速部署模式)" - echo "等待5秒后继续..." - sleep 5 - fi - # 清理旧镜像 - echo "🧹 清理旧镜像..." - docker system prune -f || true + if [ ! -d "$REPO_DIR/.git" ]; then + rm -rf "$REPO_DIR" + git clone --branch "$GIT_REF" --depth 1 "$REPO_URL" "$REPO_DIR" + else + cd "$REPO_DIR" + git remote set-url origin "$REPO_URL" + git fetch --depth 1 origin "$GIT_REF" + git checkout "$GIT_REF" + git reset --hard "origin/$GIT_REF" + fi - echo "🎉 测试环境部署成功!" - echo "更新的服务: ${SERVICES_TO_UPDATE[*]}" - EOF + cd "$REPO_DIR" + git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git" - chmod +x deploy/staging/deploy.sh + echo "当前提交: $GIT_SHA" + docker compose --env-file "$ENV_FILE" up -d --build - - name: 创建远程目录 - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.STAGING_HOST }} - username: ${{ secrets.STAGING_USER }} - key: ${{ secrets.STAGING_SSH_KEY }} - port: 22 - script: | - mkdir -p $HOME/deploy/vocata - chmod 755 $HOME/deploy/vocata + echo "=== 容器状态 ===" + docker compose ps - - name: 复制文件到测试服务器 - uses: appleboy/scp-action@v0.1.7 - with: - host: ${{ secrets.STAGING_HOST }} - username: ${{ secrets.STAGING_USER }} - key: ${{ secrets.STAGING_SSH_KEY }} - port: 22 - source: "deploy/staging/*" - target: "~/deploy/vocata" - strip_components: 2 - rm: true + echo "=== 后端健康检查 ===" + curl -fsS http://127.0.0.1:9009/api/health - - name: 执行部署 + - name: 失败时输出日志 + if: failure() uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.STAGING_HOST }} username: ${{ secrets.STAGING_USER }} key: ${{ secrets.STAGING_SSH_KEY }} port: 22 + script_stop: false script: | - cd ~/deploy/vocata - ls -la - chmod +x deploy.sh - ./deploy.sh + APP_DIR=/home/deploy/deploy/vocata + REPO_DIR=$APP_DIR/repo + + if [ -d "$REPO_DIR" ]; then + cd "$REPO_DIR" + echo "=== docker compose ps ===" + docker compose ps || true + echo "=== vocata-server logs ===" + docker compose logs --tail=100 vocata-server || true + echo "=== vocata-web logs ===" + docker compose logs --tail=60 vocata-web || true + echo "=== vocata-admin logs ===" + docker compose logs --tail=60 vocata-admin || true + fi - name: 部署结果报告 if: always() run: | - echo "## 🚀 测试环境部署结果" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + echo "## Staging 部署结果" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- 环境: staging" >> "$GITHUB_STEP_SUMMARY" + echo "- 分支: ${{ github.ref_name }}" >> "$GITHUB_STEP_SUMMARY" + echo "- 提交: \`${{ github.sha }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- 服务器: \`${{ secrets.STAGING_HOST }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" if [ "${{ job.status }}" = "success" ]; then - echo "✅ **测试环境部署成功**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📋 部署详情" >> $GITHUB_STEP_SUMMARY - echo "- **环境**: 测试环境 (staging)" >> $GITHUB_STEP_SUMMARY - echo "- **部署标签**: \`${{ needs.detect-changes.outputs.image-tag }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **提交版本**: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **分支**: develop" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 🔗 访问地址" >> $GITHUB_STEP_SUMMARY - echo "- [前端客户端](http://${{ secrets.STAGING_HOST }}:3000)" >> $GITHUB_STEP_SUMMARY - echo "- [管理后台](http://${{ secrets.STAGING_HOST }}:3001)" >> $GITHUB_STEP_SUMMARY - echo "- [后端API](http://${{ secrets.STAGING_HOST }}:9009/api)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 🏗️ 部署的组件" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ needs.detect-changes.outputs.server-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - echo "- ✅ 后端服务 (\`${{ needs.build-server.outputs.image }}\`)" >> $GITHUB_STEP_SUMMARY - fi - if [[ "${{ needs.detect-changes.outputs.web-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - echo "- ✅ 前端客户端 (\`${{ needs.build-web.outputs.image }}\`)" >> $GITHUB_STEP_SUMMARY - fi - if [[ "${{ needs.detect-changes.outputs.admin-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - echo "- ✅ 管理后台 (\`${{ needs.build-admin.outputs.image }}\`)" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "🎯 **可以开始前后端连调了!**" >> $GITHUB_STEP_SUMMARY + echo "✅ 部署成功" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- 用户端: http://${{ secrets.STAGING_HOST }}:3000" >> "$GITHUB_STEP_SUMMARY" + echo "- 管理端: http://${{ secrets.STAGING_HOST }}:3001" >> "$GITHUB_STEP_SUMMARY" + echo "- API: http://${{ secrets.STAGING_HOST }}:9009/api" >> "$GITHUB_STEP_SUMMARY" else - echo "❌ **测试环境部署失败**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "请检查部署日志并排查问题。" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + echo "❌ 部署失败" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "请查看上面的 SSH 部署输出和容器日志。" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/emergency-rollback.yml b/.github/workflows/emergency-rollback.yml index b434be5..2a34383 100644 --- a/.github/workflows/emergency-rollback.yml +++ b/.github/workflows/emergency-rollback.yml @@ -192,7 +192,7 @@ jobs: # 检查后端服务 for i in {1..10}; do - if curl -f http://localhost:9009/api/actuator/health > /dev/null 2>&1; then + if curl -f http://localhost:9009/api/health > /dev/null 2>&1; then echo "✅ 后端服务回滚成功" break else @@ -399,7 +399,7 @@ jobs: sleep 30 # 检查备用服务健康状态 - if curl -f http://localhost:9011/api/actuator/health > /dev/null 2>&1; then + if curl -f http://localhost:9011/api/health > /dev/null 2>&1; then log_success "备用服务就绪,执行流量切换" # 这里应该切换负载均衡器配置 # 实际环境中需要根据具体的负载均衡器进行配置 @@ -422,7 +422,7 @@ jobs: # 检查后端服务 HEALTH_CHECK_PASSED=true for i in {1..15}; do - if curl -f http://localhost:9009/api/actuator/health > /dev/null 2>&1; then + if curl -f http://localhost:9009/api/health > /dev/null 2>&1; then log_success "✅ 后端服务回滚成功" break else @@ -594,4 +594,4 @@ jobs: }); console.log(title); - console.log(body); \ No newline at end of file + console.log(body); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89228fe..cdb6c25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -309,7 +309,7 @@ jobs: sleep 30 # 验证新容器健康状态 - if docker-compose exec -T $service curl -f http://localhost:$SERVER_PORT/api/actuator/health > /dev/null 2>&1; then + if docker-compose exec -T $service curl -f http://localhost:$SERVER_PORT/api/health > /dev/null 2>&1; then echo "✅ $service 新版本健康检查通过" # 切换流量,移除旧容器 (蓝色) @@ -402,7 +402,7 @@ jobs: # API健康检查 echo "### API服务验证" >> $GITHUB_STEP_SUMMARY - if curl -f -m 30 "https://api.vocata.com/api/actuator/health" > /dev/null 2>&1; then + if curl -f -m 30 "https://api.vocata.com/api/health" > /dev/null 2>&1; then echo "- ✅ API健康检查通过" >> $GITHUB_STEP_SUMMARY else echo "- ❌ API健康检查失败" >> $GITHUB_STEP_SUMMARY @@ -434,4 +434,4 @@ jobs: else echo "⚠️ 生产环境验证失败!请立即进行回滚操作。" >> $GITHUB_STEP_SUMMARY exit 1 - fi \ No newline at end of file + fi diff --git a/.gitignore b/.gitignore index 7af4e77..184df36 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,7 @@ Desktop.ini # 文档生成 **/.docs/ +**/.local/ # 测试覆盖率报告 **/coverage/ @@ -186,4 +187,4 @@ Desktop.ini !**/application-example.yml !**/config.example.js -/resources/application-local.yml \ No newline at end of file +/resources/application-local.yml diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 1db9aa4..fe958ce 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,5 +1,4 @@ # VocaTa生产环境 Docker Compose 配置 -version: '3.8' x-app-common: &app-common restart: unless-stopped @@ -14,7 +13,7 @@ x-app-common: &app-common max-file: "3" x-healthcheck-web: &healthcheck-web - test: ["CMD", "curl", "-f", "http://localhost/"] + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 @@ -67,7 +66,7 @@ services: - server-uploads:/app/uploads - /etc/localtime:/etc/localtime:ro healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9009/api/actuator/health"] + test: ["CMD", "curl", "-f", "http://localhost:9009/api/health"] interval: 30s timeout: 15s retries: 5 @@ -92,7 +91,7 @@ services: image: ${WEB_IMAGE:-ghcr.io/leivik/vocata-web:latest} container_name: vocata-web ports: - - "${WEB_PORT:-8080}:80" + - "${WEB_PORT:-8080}:8080" volumes: - web-logs:/var/log/nginx - /etc/localtime:/etc/localtime:ro @@ -119,7 +118,7 @@ services: image: ${ADMIN_IMAGE:-ghcr.io/leivik/vocata-admin:latest} container_name: vocata-admin ports: - - "${ADMIN_PORT:-8081}:80" + - "${ADMIN_PORT:-8081}:8080" volumes: - admin-logs:/var/log/nginx - /etc/localtime:/etc/localtime:ro @@ -284,4 +283,4 @@ networks: driver: bridge name: ${COMPOSE_PROJECT_NAME:-vocata-prod}-network driver_opts: - com.docker.network.bridge.name: vocata-br \ No newline at end of file + com.docker.network.bridge.name: vocata-br diff --git a/docker-compose.test.yml b/docker-compose.test.yml index fb06844..280f0ee 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,5 +1,4 @@ # VocaTa测试环境 Docker Compose 配置 -version: '3.8' x-app-common: &app-common restart: unless-stopped @@ -43,6 +42,7 @@ services: - QINIU_BUCKET=${QINIU_BUCKET} - QINIU_DOMAIN=${QINIU_DOMAIN} - QINIU_REGION=${QINIU_REGION} + - QINIU_KEY_PREFIX=${QINIU_KEY_PREFIX} # 邮箱配置 - EMAIL_USER_NAME=${EMAIL_USER_NAME} - EMAIL_USER_PASSWORD=${EMAIL_USER_PASSWORD} @@ -53,18 +53,42 @@ services: - MAIL_PASSWORD=${EMAIL_USER_PASSWORD} # AI服务配置 - AI_LLM_PROVIDER=${AI_LLM_PROVIDER:-qiniu} + - AI_STT_PROVIDER=${AI_STT_PROVIDER:-qiniu} + - AI_TTS_PROVIDER=${AI_TTS_PROVIDER:-xunfei} - QINIU_AI_API_KEY=${QINIU_AI_API_KEY} - QINIU_AI_BASE_URL=${QINIU_AI_BASE_URL:-https://openai.qiniu.com/v1} - QINIU_AI_MODEL=${QINIU_AI_MODEL:-x-ai/grok-4-fast} - QINIU_AI_TIMEOUT=${QINIU_AI_TIMEOUT:-60} + - QINIU_STT_ENDPOINT=${QINIU_STT_ENDPOINT:-https://openai.qiniu.com/v1} + - QINIU_STT_MODEL=${QINIU_STT_MODEL:-asr} - GEMINI_API_KEY=${GEMINI_API_KEY} + - GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash-lite} - OPENAI_API_KEY=${OPENAI_API_KEY} + - OPENAI_MODEL=${OPENAI_MODEL:-gpt-3.5-turbo} + - SILICONFLOW_API_KEY=${SILICONFLOW_API_KEY} + - SILICONFLOW_BASE_URL=${SILICONFLOW_BASE_URL:-https://api.siliconflow.cn/v1} + - SILICONFLOW_AI_MODEL=${SILICONFLOW_AI_MODEL:-Qwen/Qwen3-8B} + - SILICONFLOW_TIMEOUT=${SILICONFLOW_TIMEOUT:-120} + - XUNFEI_STT_APP_ID=${XUNFEI_STT_APP_ID} + - XUNFEI_STT_API_KEY=${XUNFEI_STT_API_KEY} + - XUNFEI_STT_SECRET_KEY=${XUNFEI_STT_SECRET_KEY} + - XUNFEI_STT_HOST=${XUNFEI_STT_HOST:-iat-api.xfyun.cn} + - XUNFEI_STT_PATH=${XUNFEI_STT_PATH:-/v2/iat} + - XUNFEI_TTS_APP_ID=${XUNFEI_TTS_APP_ID} + - XUNFEI_TTS_API_KEY=${XUNFEI_TTS_API_KEY} + - XUNFEI_TTS_SECRET_KEY=${XUNFEI_TTS_SECRET_KEY} + - XUNFEI_TTS_HOST=${XUNFEI_TTS_HOST:-tts-api.xfyun.cn} + - XUNFEI_TTS_PATH=${XUNFEI_TTS_PATH:-/v2/tts} + - VOLCAN_TTS_ACCESS_TOKEN=${VOLCAN_TTS_ACCESS_TOKEN} + - VOLCAN_TTS_APP_ID=${VOLCAN_TTS_APP_ID} + - VOLCAN_TTS_HOST=${VOLCAN_TTS_HOST:-openspeech.bytedance.com} + - VOLCAN_TTS_REGION=${VOLCAN_TTS_REGION:-ap-beijing-1} volumes: - server-logs:/var/log/vocata - server-uploads:/app/uploads healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9009/api/actuator/health"] + test: ["CMD", "curl", "-f", "http://localhost:9009/api/health"] interval: 30s timeout: 10s retries: 3 @@ -138,4 +162,4 @@ volumes: networks: vocata-network: driver: bridge - name: ${COMPOSE_PROJECT_NAME:-vocata-test}-network \ No newline at end of file + name: ${COMPOSE_PROJECT_NAME:-vocata-test}-network diff --git a/docker-compose.yml b/docker-compose.yml index 8d5fb7f..ce42535 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,18 @@ -version: '3.8' - services: # PostgreSQL 数据库 postgres: - image: postgres:15-alpine + image: pgvector/pgvector:pg17 container_name: vocata-postgres-dev environment: - POSTGRES_DB: vocata_local - POSTGRES_USER: vocata - POSTGRES_PASSWORD: vocata123 + POSTGRES_DB: ${POSTGRES_DB:-vocata_db} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres123} ports: - - "5432:5432" + - "${POSTGRES_PORT:-5432}:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U vocata -d vocata_local"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-vocata_db}"] interval: 30s timeout: 10s retries: 3 @@ -24,7 +22,7 @@ services: image: redis:7-alpine container_name: vocata-redis-dev ports: - - "6379:6379" + - "${REDIS_EXPOSE_PORT:-6379}:6379" volumes: - redis_data:/data healthcheck: @@ -38,17 +36,47 @@ services: build: context: ./vocata-server dockerfile: Dockerfile + args: + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-local} container_name: vocata-server-dev ports: - - "9009:9009" + - "${SERVER_PORT:-9009}:9009" environment: - SPRING_PROFILES_ACTIVE: local - SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/vocata_local - SPRING_DATASOURCE_USERNAME: vocata - SPRING_DATASOURCE_PASSWORD: vocata123 - SPRING_REDIS_HOST: redis - SPRING_REDIS_PORT: 6379 - SPRING_REDIS_DATABASE: 0 + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-local} + SERVER_PORT: 9009 + DB_HOST: ${DB_HOST:-postgres} + DB_PORT: ${DB_PORT:-5432} + DB_NAME: ${DB_NAME:-vocata_db} + DB_USERNAME: ${DB_USERNAME:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-postgres123} + REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_PASSWORD: ${REDIS_PASSWORD:-} + REDIS_DATABASE: ${REDIS_DATABASE:-0} + MAIL_USERNAME: ${MAIL_USERNAME:-} + MAIL_PASSWORD: ${MAIL_PASSWORD:-} + AI_LLM_PROVIDER: ${AI_LLM_PROVIDER:-qiniu} + AI_STT_PROVIDER: ${AI_STT_PROVIDER:-qiniu} + AI_TTS_PROVIDER: ${AI_TTS_PROVIDER:-xunfei} + XUNFEI_TTS_APP_ID: ${XUNFEI_TTS_APP_ID:-} + XUNFEI_TTS_API_KEY: ${XUNFEI_TTS_API_KEY:-} + XUNFEI_TTS_SECRET_KEY: ${XUNFEI_TTS_SECRET_KEY:-} + QINIU_ACCESS_KEY: ${QINIU_ACCESS_KEY:-} + QINIU_SECRET_KEY: ${QINIU_SECRET_KEY:-} + QINIU_BUCKET: ${QINIU_BUCKET:-} + QINIU_DOMAIN: ${QINIU_DOMAIN:-} + QINIU_REGION: ${QINIU_REGION:-huadong} + QINIU_KEY_PREFIX: ${QINIU_KEY_PREFIX:-} + QINIU_AI_API_KEY: ${QINIU_AI_API_KEY:-} + QINIU_AI_BASE_URL: ${QINIU_AI_BASE_URL:-https://openai.qiniu.com/v1} + QINIU_AI_MODEL: ${QINIU_AI_MODEL:-x-ai/grok-4-fast} + QINIU_AI_TIMEOUT: ${QINIU_AI_TIMEOUT:-60} + GEMINI_API_KEY: ${GEMINI_API_KEY:-} + GEMINI_MODEL: ${GEMINI_MODEL:-gemini-2.5-flash-lite} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + OPENAI_MODEL: ${OPENAI_MODEL:-gpt-3.5-turbo} + SILICONFLOW_API_KEY: ${SILICONFLOW_API_KEY:-} + SILICONFLOW_AI_MODEL: ${SILICONFLOW_AI_MODEL:-Qwen/Qwen3-8B} depends_on: postgres: condition: service_healthy @@ -68,36 +96,51 @@ services: build: context: ./vocata-web dockerfile: Dockerfile + args: + BUILD_MODE: ${WEB_BUILD_MODE:-production} + VITE_APP_URL: ${VITE_APP_URL:-http://localhost:9009} container_name: vocata-web-dev ports: - - "3000:80" - environment: - VITE_API_BASE_URL: http://localhost:9009/api + - "${WEB_PORT:-3000}:8080" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 depends_on: - - vocata-server + vocata-server: + condition: service_healthy # 管理后台 vocata-admin: build: context: ./vocata-admin dockerfile: Dockerfile + args: + BUILD_MODE: ${ADMIN_BUILD_MODE:-production} + VITE_APP_URL: ${VITE_APP_URL:-http://localhost:9009} container_name: vocata-admin-dev ports: - - "3001:80" - environment: - VITE_API_BASE_URL: http://localhost:9009/api + - "${ADMIN_PORT:-3001}:8080" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 depends_on: - - vocata-server + vocata-server: + condition: service_healthy # pgAdmin (数据库管理工具) pgadmin: image: dpage/pgadmin4:latest container_name: vocata-pgadmin-dev + profiles: ["tools"] environment: - PGADMIN_DEFAULT_EMAIL: admin@vocata.com - PGADMIN_DEFAULT_PASSWORD: admin123 + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@vocata.com} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin123} ports: - - "5050:80" + - "${PGADMIN_PORT:-5050}:80" depends_on: - postgres @@ -105,9 +148,10 @@ services: mailhog: image: mailhog/mailhog:latest container_name: vocata-mailhog-dev + profiles: ["tools"] ports: - - "1025:1025" # SMTP端口 - - "8025:8025" # Web界面端口 + - "${MAILHOG_SMTP_PORT:-1025}:1025" + - "${MAILHOG_WEB_PORT:-8025}:8025" volumes: postgres_data: @@ -115,4 +159,4 @@ volumes: networks: default: - name: vocata-dev-network \ No newline at end of file + name: ${COMPOSE_PROJECT_NAME:-vocata-dev}-network diff --git "a/docs/Docker\345\274\200\345\217\221\347\216\257\345\242\203.md" "b/docs/Docker\345\274\200\345\217\221\347\216\257\345\242\203.md" new file mode 100644 index 0000000..f95086e --- /dev/null +++ "b/docs/Docker\345\274\200\345\217\221\347\216\257\345\242\203.md" @@ -0,0 +1,57 @@ +# Docker 开发环境 + +当前仓库先以开发环境为中心整理 Docker 结构。 + +## 目标 + +- `docker-compose.yml` 只服务本地开发和联调 +- `docker-compose.test.yml`、`docker-compose.prod.yml` 先保留为后续测试/生产预留文件 +- 前端容器走接近发布态的静态构建,不提供热更新 + +## 使用方式 + +1. 复制环境变量模板 + +```bash +cp .env.example .env +``` + +2. 按需补充 `.env` + +- 基础开发默认值已经可用:PostgreSQL、Redis、端口映射 +- 开发数据库默认使用 `pgvector/pgvector:pg17`,便于恢复现有 PostgreSQL 17 + pgvector 备份 +- 若要完整启动后端,仍需补第三方配置,例如七牛和 AI 相关密钥 + +3. 启动核心开发环境 + +```bash +docker compose up -d --build +``` + +4. 启动可选工具 + +```bash +docker compose --profile tools up -d +``` + +## 当前开发环境包含 + +- `postgres`:开发数据库 +- `redis`:开发缓存 +- `vocata-server`:后端服务 +- `vocata-web`:用户端静态前端 +- `vocata-admin`:管理端静态前端 +- `pgadmin`:可选数据库管理工具 +- `mailhog`:可选邮件调试工具 + +## 约定 + +- 后端健康检查统一为 `/api/health` +- 前端容器内部统一监听 `8080` +- 前端 API 地址通过构建期变量 `VITE_APP_URL` 注入 +- 七牛对象前缀可通过 `QINIU_KEY_PREFIX` 配置,例如 `Vocata` + +## 说明 + +- 如果你要做前端页面开发,建议仍然优先本地运行 `npm run dev` +- 当前 Docker 开发环境更适合联调整体链路、验证容器化行为、准备后续服务器部署 diff --git a/docs/GitHub-Staging-Secrets.md b/docs/GitHub-Staging-Secrets.md new file mode 100644 index 0000000..56481cf --- /dev/null +++ b/docs/GitHub-Staging-Secrets.md @@ -0,0 +1,68 @@ +# GitHub Staging Secrets 清单 + +当前 staging 目标机: + +- `STAGING_HOST=86.53.161.33` +- `STAGING_USER=deploy` +- SSH 端口固定为 `22`(当前 workflow 已写死) + +## 必填 Secrets + +现在的简化版 staging workflow 只需要 3 个 secrets: + +- `STAGING_HOST` +- `STAGING_USER` +- `STAGING_SSH_KEY` + +## STAGING_SSH_KEY 来源 + +当前为 GitHub Actions 生成的 staging deploy key 私钥保存在本机: + +- `/home/an/Projects/goodPro/VocaTa/.local/vocata_staging_ed25519` + +对应公钥已经安装到服务器 `root` 用户: + +- `/root/.ssh/authorized_keys` + +同一把公钥也已经安装到服务器部署用户: + +- `/home/deploy/.ssh/authorized_keys` + +把私钥完整内容复制到 GitHub 仓库 Secret `STAGING_SSH_KEY` 即可。 + +## 当前服务器登录建议 + +- 日常部署用户:`deploy` +- 应急用户:`root` +- SSH 密码登录:已禁用 +- SSH 公钥登录:已启用 + +## 服务器配置放哪里 + +业务配置现在不再放 GitHub Secrets,而是放服务器本地: + +- `/home/deploy/deploy/vocata/.env` + +兼容旧位置: + +- `/home/deploy/deploy/vocata/data/vocata-staging.env` + +如果新位置不存在,workflow 会自动从旧位置复制一份。 + +## 前端 IP 暴露说明 + +如果服务器本地 `.env` 中写的是: + +- `VITE_APP_URL=http://86.53.161.33:9009` + +那么前端构建产物里会直接带这个 IP,浏览器里可见。 + +如果你想避免把后端 IP 直接打进前端,服务器 `.env` 建议改成: + +- `VITE_APP_URL=/api` + +前提: + +- 当前前端镜像内 Nginx 已经把 `/api` 代理到 `vocata-server:9009` + +这会让浏览器只请求当前站点的 `/api`,不再把 `9009` 和后端 IP 显式写进前端包里。 diff --git "a/docs/\351\203\250\347\275\262\346\226\207\346\241\243.md" "b/docs/\351\203\250\347\275\262\346\226\207\346\241\243.md" index ed92fe7..2ac0bd8 100644 --- "a/docs/\351\203\250\347\275\262\346\226\207\346\241\243.md" +++ "b/docs/\351\203\250\347\275\262\346\226\207\346\241\243.md" @@ -7,6 +7,9 @@ - **本地开发模式**:分别启动后端(Java + Maven)及前端项目(Vue 3 + Vite),适合调试与开发。 - **生产部署示例**:项目根目录提供 `docker-compose.prod.yml` 可作为上线部署基础模板。 +当前建议优先使用开发环境 Docker 方案: +[Docker开发环境.md](/home/an/Projects/goodPro/VocaTa/docs/Docker开发环境.md) + ## 2. 环境准备 ### 2.1 通用要求 - 操作系统:macOS / Linux / Windows(支持 Docker 或 Java 开发环境) diff --git a/vocata-admin/.dockerignore b/vocata-admin/.dockerignore new file mode 100644 index 0000000..c6fd245 --- /dev/null +++ b/vocata-admin/.dockerignore @@ -0,0 +1,16 @@ +.git +.gitignore +.github +node_modules/ +dist/ +coverage/ +.vite/ +.idea/ +.vscode/ +*.log +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +README.md diff --git a/vocata-admin/Dockerfile b/vocata-admin/Dockerfile index a99b11d..a90fac5 100644 --- a/vocata-admin/Dockerfile +++ b/vocata-admin/Dockerfile @@ -1,51 +1,36 @@ -# VocaTa前端客户端 - 多阶段构建Dockerfile -# 基于Node.js官方镜像 - -# 构建阶段 FROM node:20-alpine AS build -# 安装必要的构建工具 RUN apk add --no-cache python3 make g++ git -# 设置工作目录 WORKDIR /app -# 复制package文件 COPY package*.json ./ +RUN npm ci && npm cache clean --force -# 安装依赖 -RUN npm ci --only=production && npm cache clean --force - -# 复制源代码 COPY . . -# 构建应用 ARG BUILD_MODE=production +ARG VITE_APP_URL +ENV VITE_APP_URL=${VITE_APP_URL} RUN npm run build:${BUILD_MODE} -# 生产阶段 - 使用Nginx托管静态文件 FROM nginx:1.25-alpine -# 安装必要工具 RUN apk add --no-cache \ curl \ - tzdata \ - dumb-init + dumb-init \ + tzdata -# 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# 创建nginx用户和目录 RUN addgroup -g 1001 -S vocata && \ adduser -u 1001 -S vocata -G vocata && \ mkdir -p /var/cache/nginx /var/log/nginx /var/lib/nginx && \ chown -R vocata:vocata /var/cache/nginx /var/log/nginx /var/lib/nginx /etc/nginx -# 复制构建产物 COPY --from=build --chown=vocata:vocata /app/dist /usr/share/nginx/html -# 创建Nginx配置文件 RUN cat > /etc/nginx/nginx.conf << 'EOF' worker_processes auto; error_log /var/log/nginx/error.log; @@ -115,7 +100,6 @@ http { add_header Cache-Control "public, immutable"; } - # API代理(如果需要) location /api { proxy_pass http://vocata-server:9009; proxy_set_header Host $host; @@ -134,33 +118,27 @@ http { } EOF -# 设置权限 RUN chown -R vocata:vocata /usr/share/nginx/html /etc/nginx -# 切换到非root用户 USER vocata -# 暴露端口 EXPOSE 8080 -# 健康检查 HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 -# 构建参数和标签 ARG BUILD_DATE ARG VERSION="1.0.0" LABEL maintainer="VocaTa Team " \ version="${VERSION}" \ build-date="${BUILD_DATE}" \ - description="VocaTa AI角色扮演平台前端客户端" \ - org.opencontainers.image.title="vocata-web" \ - org.opencontainers.image.description="VocaTa AI Role Playing Platform Frontend Client" \ + description="VocaTa AI角色扮演平台管理后台" \ + org.opencontainers.image.title="vocata-admin" \ + org.opencontainers.image.description="VocaTa AI Role Playing Platform Admin Frontend" \ org.opencontainers.image.url="https://github.com/leivik/vocata" \ org.opencontainers.image.vendor="VocaTa Team" \ org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.created="${BUILD_DATE}" -# 启动Nginx ENTRYPOINT ["dumb-init", "--"] -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] diff --git a/vocata-server/.dockerignore b/vocata-server/.dockerignore index d1a9b48..1c0c737 100644 --- a/vocata-server/.dockerignore +++ b/vocata-server/.dockerignore @@ -38,6 +38,8 @@ out/ dist/ node_modules/ .npm +!target/ +!target/*.jar # 测试相关 coverage/ @@ -109,4 +111,4 @@ files/ # 缓存 .cache/ -*.cache \ No newline at end of file +*.cache diff --git a/vocata-server/Dockerfile b/vocata-server/Dockerfile index 62540c5..66d72d2 100644 --- a/vocata-server/Dockerfile +++ b/vocata-server/Dockerfile @@ -1,88 +1,62 @@ -# VocaTa后端服务 - 多阶段构建Dockerfile -# 基于OpenJDK 17的精简镜像 +# syntax=docker/dockerfile:1.7 -# 构建阶段 - 用于Maven构建(可选,CI/CD中已构建) FROM maven:3.9.4-eclipse-temurin-17-alpine AS build WORKDIR /build -# 复制Maven配置文件和Wrapper -COPY pom.xml . -COPY .mvn .mvn -COPY mvnw . +COPY pom.xml ./ +RUN --mount=type=cache,target=/root/.m2 \ + mvn -B -ntp dependency:go-offline -# 下载依赖(利用Docker层缓存) -RUN mvn dependency:go-offline -B - -# 复制源代码 COPY src ./src -# 构建应用(支持多环境) ARG SPRING_PROFILES_ACTIVE=prod -RUN mvn clean package -B -DskipTests -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} +RUN --mount=type=cache,target=/root/.m2 \ + mvn -B -ntp package -DskipTests -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} -# 运行阶段 - 精简的生产镜像 FROM eclipse-temurin:17-jre-alpine -# 安装必要工具和dumb-init RUN apk add --no-cache \ curl \ - tzdata \ + dumb-init \ font-noto-cjk \ - dumb-init + tzdata -# 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# 创建应用用户(安全最佳实践) RUN addgroup -g 1001 -S vocata && \ adduser -u 1001 -S vocata -G vocata -# 设置工作目录 WORKDIR /app -# 复制JAR文件 - 优先使用已构建的JAR,回退到构建阶段 -COPY --from=build /build/target/vocata-server-*.jar app.jar -# 如果CI/CD中已经构建了JAR,可以直接复制 -# COPY ./target/vocata-server-*.jar app.jar +COPY --from=build /build/target/vocata-server-*.jar /app/app.jar -# 创建必要目录并设置权限 RUN mkdir -p /app/logs /app/uploads /var/log/vocata && \ - chown -R vocata:vocata /app /var/log/vocata && \ - chmod +x /app/app.jar + chown -R vocata:vocata /app /var/log/vocata -# 切换到非root用户 USER vocata -# 暴露端口 - 配置端口以匹配不同环境 -EXPOSE 9010 EXPOSE 9009 -# JVM优化参数 -ENV JAVA_OPTS="-Xms512m -Xmx2048m \ --XX:+UseG1GC \ +ENV JAVA_OPTS="-XX:+UseG1GC \ -XX:G1HeapRegionSize=16m \ -XX:+DisableExplicitGC \ -XX:+UseStringDeduplication \ -XX:+OptimizeStringConcat \ +-XX:MaxRAMPercentage=75.0 \ -Djava.security.egd=file:/dev/./urandom \ -Dfile.encoding=UTF-8 \ -Duser.timezone=Asia/Shanghai" - -# 应用配置环境变量 +ENV SERVER_PORT=9009 ENV SPRING_PROFILES_ACTIVE=prod -ENV SERVER_PORT=9010 -# 健康检查 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:${SERVER_PORT}/api/actuator/health || exit 1 + CMD curl -f http://localhost:${SERVER_PORT}/api/health || exit 1 -# 启动应用 - 使用 dumb-init 处理信号 ENTRYPOINT ["dumb-init", "--"] CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] -# 构建参数和标签信息 ARG BUILD_DATE ARG VERSION="1.0.0" LABEL maintainer="VocaTa Team " \ @@ -94,4 +68,4 @@ LABEL maintainer="VocaTa Team " \ org.opencontainers.image.url="https://github.com/leivik/vocata" \ org.opencontainers.image.vendor="VocaTa Team" \ org.opencontainers.image.version="${VERSION}" \ - org.opencontainers.image.created="${BUILD_DATE}" \ No newline at end of file + org.opencontainers.image.created="${BUILD_DATE}" diff --git a/vocata-server/Dockerfile.ci b/vocata-server/Dockerfile.ci index 3c04bbb..6b68688 100644 --- a/vocata-server/Dockerfile.ci +++ b/vocata-server/Dockerfile.ci @@ -1,44 +1,29 @@ -# VocaTa后端服务 - CI/CD优化版Dockerfile -# 针对CI/CD环境优化的单阶段构建 - FROM eclipse-temurin:17-jre-alpine -# 安装必要工具和dumb-init RUN apk add --no-cache \ curl \ - tzdata \ + dumb-init \ font-noto-cjk \ - dumb-init + tzdata -# 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# 创建应用用户(安全最佳实践) RUN addgroup -g 1001 -S vocata && \ adduser -u 1001 -S vocata -G vocata -# 设置工作目录 WORKDIR /app -# 复制JAR文件(从CI/CD构建阶段) -COPY vocata-server/target/vocata-server-*.jar app.jar +COPY target/vocata-server-*.jar /app/app.jar -# 创建必要目录并设置权限 RUN mkdir -p /app/logs /app/uploads /var/log/vocata && \ - chown -R vocata:vocata /app /var/log/vocata && \ - chmod +x /app/app.jar + chown -R vocata:vocata /app /var/log/vocata -# 切换到非root用户 USER vocata -# 暴露端口 - 支持不同环境 EXPOSE 9009 -EXPOSE 9010 -# JVM优化参数 - 针对容器环境优化 -ENV JAVA_OPTS="-Xms256m -Xmx1024m \ --XX:+UseG1GC \ +ENV JAVA_OPTS="-XX:+UseG1GC \ -XX:G1HeapRegionSize=16m \ -XX:+DisableExplicitGC \ -XX:+UseStringDeduplication \ @@ -48,28 +33,24 @@ ENV JAVA_OPTS="-Xms256m -Xmx1024m \ -Dfile.encoding=UTF-8 \ -Duser.timezone=Asia/Shanghai" -# 应用配置环境变量 -ENV SPRING_PROFILES_ACTIVE=test ENV SERVER_PORT=9009 +ENV SPRING_PROFILES_ACTIVE=prod -# 健康检查 - 支持动态端口 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:${SERVER_PORT}/api/actuator/health || exit 1 + CMD curl -f http://localhost:${SERVER_PORT}/api/health || exit 1 -# 启动应用 - 使用 dumb-init 处理信号 ENTRYPOINT ["dumb-init", "--"] CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] -# 构建参数和标签信息 ARG BUILD_DATE ARG VERSION="1.0.0" LABEL maintainer="VocaTa Team " \ version="${VERSION}" \ build-date="${BUILD_DATE}" \ - description="VocaTa AI角色扮演平台后端服务 - CI/CD版本" \ + description="VocaTa AI角色扮演平台后端服务" \ org.opencontainers.image.title="vocata-server" \ org.opencontainers.image.description="VocaTa AI Role Playing Platform Backend Service" \ org.opencontainers.image.url="https://github.com/leivik/vocata" \ org.opencontainers.image.vendor="VocaTa Team" \ org.opencontainers.image.version="${VERSION}" \ - org.opencontainers.image.created="${BUILD_DATE}" \ No newline at end of file + org.opencontainers.image.created="${BUILD_DATE}" diff --git a/vocata-server/src/main/java/com/vocata/file/config/QiniuConfig.java b/vocata-server/src/main/java/com/vocata/file/config/QiniuConfig.java index 12a4924..3ccf850 100644 --- a/vocata-server/src/main/java/com/vocata/file/config/QiniuConfig.java +++ b/vocata-server/src/main/java/com/vocata/file/config/QiniuConfig.java @@ -43,6 +43,11 @@ public class QiniuConfig { */ private String region; + /** + * 对象 key 前缀,例如 Vocata + */ + private String keyPrefix; + /** * 七牛云认证 */ @@ -131,4 +136,12 @@ public String getRegion() { public void setRegion(String region) { this.region = region; } -} \ No newline at end of file + + public String getKeyPrefix() { + return keyPrefix; + } + + public void setKeyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + } +} diff --git a/vocata-server/src/main/java/com/vocata/file/config/QiniuProperties.java b/vocata-server/src/main/java/com/vocata/file/config/QiniuProperties.java index 3db264c..0a0d545 100644 --- a/vocata-server/src/main/java/com/vocata/file/config/QiniuProperties.java +++ b/vocata-server/src/main/java/com/vocata/file/config/QiniuProperties.java @@ -18,6 +18,8 @@ public class QiniuProperties { private String domain; + private String keyPrefix; + private Long uploadTokenExpires = 3600L; public String getAccessKey() { @@ -52,6 +54,14 @@ public void setDomain(String domain) { this.domain = domain; } + public String getKeyPrefix() { + return keyPrefix; + } + + public void setKeyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + } + public Long getUploadTokenExpires() { return uploadTokenExpires; } @@ -59,4 +69,4 @@ public Long getUploadTokenExpires() { public void setUploadTokenExpires(Long uploadTokenExpires) { this.uploadTokenExpires = uploadTokenExpires; } -} \ No newline at end of file +} diff --git a/vocata-server/src/main/java/com/vocata/file/service/impl/FileServiceImpl.java b/vocata-server/src/main/java/com/vocata/file/service/impl/FileServiceImpl.java index cab35f1..ad2f629 100644 --- a/vocata-server/src/main/java/com/vocata/file/service/impl/FileServiceImpl.java +++ b/vocata-server/src/main/java/com/vocata/file/service/impl/FileServiceImpl.java @@ -289,11 +289,13 @@ private String generateFileName(MultipartFile file, String fileType) { String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); String uuid = IdUtil.simpleUUID(); - return String.format("%s/%s/%s%s", + String objectKey = String.format("%s/%s/%s%s", StringUtils.isNotBlank(fileType) ? fileType : "common", dateStr, uuid, extension); + + return prependKeyPrefix(objectKey); } /** @@ -310,10 +312,26 @@ private String generateAudioFileName(String originalFileName, String fileType) { String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); String uuid = IdUtil.simpleUUID(); - return String.format("%s/%s/%s%s", + String objectKey = String.format("%s/%s/%s%s", StringUtils.isNotBlank(fileType) ? fileType : "audio", dateStr, uuid, extension); + + return prependKeyPrefix(objectKey); + } + + private String prependKeyPrefix(String objectKey) { + String keyPrefix = qiniuConfig.getKeyPrefix(); + if (StringUtils.isBlank(keyPrefix)) { + return objectKey; + } + + String normalizedPrefix = StringUtils.strip(keyPrefix, "/"); + if (StringUtils.isBlank(normalizedPrefix)) { + return objectKey; + } + + return normalizedPrefix + "/" + objectKey; } } diff --git a/vocata-web/.dockerignore b/vocata-web/.dockerignore new file mode 100644 index 0000000..c6fd245 --- /dev/null +++ b/vocata-web/.dockerignore @@ -0,0 +1,16 @@ +.git +.gitignore +.github +node_modules/ +dist/ +coverage/ +.vite/ +.idea/ +.vscode/ +*.log +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +README.md diff --git a/vocata-web/Dockerfile b/vocata-web/Dockerfile index a99b11d..a7f89ea 100644 --- a/vocata-web/Dockerfile +++ b/vocata-web/Dockerfile @@ -1,51 +1,36 @@ -# VocaTa前端客户端 - 多阶段构建Dockerfile -# 基于Node.js官方镜像 - -# 构建阶段 FROM node:20-alpine AS build -# 安装必要的构建工具 RUN apk add --no-cache python3 make g++ git -# 设置工作目录 WORKDIR /app -# 复制package文件 COPY package*.json ./ +RUN npm ci && npm cache clean --force -# 安装依赖 -RUN npm ci --only=production && npm cache clean --force - -# 复制源代码 COPY . . -# 构建应用 ARG BUILD_MODE=production +ARG VITE_APP_URL +ENV VITE_APP_URL=${VITE_APP_URL} RUN npm run build:${BUILD_MODE} -# 生产阶段 - 使用Nginx托管静态文件 FROM nginx:1.25-alpine -# 安装必要工具 RUN apk add --no-cache \ curl \ - tzdata \ - dumb-init + dumb-init \ + tzdata -# 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# 创建nginx用户和目录 RUN addgroup -g 1001 -S vocata && \ adduser -u 1001 -S vocata -G vocata && \ mkdir -p /var/cache/nginx /var/log/nginx /var/lib/nginx && \ chown -R vocata:vocata /var/cache/nginx /var/log/nginx /var/lib/nginx /etc/nginx -# 复制构建产物 COPY --from=build --chown=vocata:vocata /app/dist /usr/share/nginx/html -# 创建Nginx配置文件 RUN cat > /etc/nginx/nginx.conf << 'EOF' worker_processes auto; error_log /var/log/nginx/error.log; @@ -115,7 +100,6 @@ http { add_header Cache-Control "public, immutable"; } - # API代理(如果需要) location /api { proxy_pass http://vocata-server:9009; proxy_set_header Host $host; @@ -134,20 +118,15 @@ http { } EOF -# 设置权限 RUN chown -R vocata:vocata /usr/share/nginx/html /etc/nginx -# 切换到非root用户 USER vocata -# 暴露端口 EXPOSE 8080 -# 健康检查 HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 -# 构建参数和标签 ARG BUILD_DATE ARG VERSION="1.0.0" LABEL maintainer="VocaTa Team " \ @@ -161,6 +140,5 @@ LABEL maintainer="VocaTa Team " \ org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.created="${BUILD_DATE}" -# 启动Nginx ENTRYPOINT ["dumb-init", "--"] -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"]