From 96358f8d5d1c2ce55e9d3fd4fddad33901d907c5 Mon Sep 17 00:00:00 2001 From: caoyuchun <335003032@qq.com> Date: Fri, 16 Jan 2026 11:43:45 +0800 Subject: [PATCH] cyc --- DATABASE_MIGRATION.md | 212 +++++++++++++++++++++++++++ Jenkinsfile | 216 +++++++--------------------- backend/Dockerfile | 13 +- backend/entrypoint.sh | 25 ++++ nginx/docker-compose.production.yml | 4 +- 5 files changed, 304 insertions(+), 166 deletions(-) create mode 100644 DATABASE_MIGRATION.md create mode 100644 backend/entrypoint.sh diff --git a/DATABASE_MIGRATION.md b/DATABASE_MIGRATION.md new file mode 100644 index 0000000..4f97f44 --- /dev/null +++ b/DATABASE_MIGRATION.md @@ -0,0 +1,212 @@ +# 数据库迁移和更新流程 + +## 概述 + +本项目已实现自动化的数据库迁移和初始化流程。每次容器启动时,会自动检查数据库状态并执行必要的迁移操作。 + +## 更新流程 + +### 场景一:只更新代码(无数据库变更) + +``` +1. 修改代码 +2. Git 提交: git commit -m "feat: 更新功能" +3. Git 推送: git push origin main +4. Jenkins 自动触发构建 +5. 部署到服务器 +6. 容器重启,应用新代码 +``` + +**数据库**: 无需操作,数据库文件保持不变 + +### 场景二:更新代码 + 数据库结构变更 + +``` +1. 修改代码和 schema.prisma +2. 创建迁移: cd backend && npm run prisma:migrate -- --name migration_name +3. Git 提交(包含迁移文件): git commit -m "feat: 添加新功能 + 数据库迁移" +4. Git 推送: git push origin main +5. Jenkins 自动触发构建 +6. 部署到服务器(包含迁移文件) +7. 容器启动时自动执行迁移 +``` + +## 数据库迁移机制 + +### 容器启动流程 + +当后端容器启动时,`entrypoint.sh` 脚本会自动执行以下步骤: + +1. **生成 Prisma Client** + ```bash + npx prisma generate + ``` + +2. **检查数据库状态** + - 如果数据库不存在 → 执行初始化 + - 执行 `prisma migrate deploy`(应用所有迁移) + - 执行 `prisma seed`(填充初始数据) + - 如果数据库已存在 → 只执行迁移 + - 执行 `prisma migrate deploy`(应用新的迁移) + +3. **启动应用** + ```bash + node dist/index.js + ``` + +### 迁移文件管理 + +- **迁移文件位置**: `backend/prisma/migrations/` +- **必须提交到 Git**: 所有迁移文件都需要版本控制 +- **自动应用**: 容器启动时自动应用所有未应用的迁移 + +### 开发环境 vs 生产环境 + +| 环境 | 命令 | 说明 | +|------|------|------| +| 开发 | `npm run prisma:migrate` | 创建新迁移文件并应用 | +| 生产 | `prisma migrate deploy` | 只应用已有迁移,不创建新迁移 | + +## 文件结构 + +``` +backend/ +├── Dockerfile # Docker 镜像构建配置 +├── entrypoint.sh # 容器启动脚本(自动迁移) +├── prisma/ +│ ├── schema.prisma # 数据库模型定义 +│ ├── seed.ts # 初始数据填充脚本 +│ └── migrations/ # 迁移文件目录 +│ ├── 20260110052756_init/ +│ │ └── migration.sql +│ └── migration_lock.toml +└── ... +``` + +## 部署配置 + +### Dockerfile 关键配置 + +```dockerfile +# 安装 tsx(用于执行 seed.ts) +RUN npm install -g tsx + +# 复制 entrypoint 脚本 +COPY backend/entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + +# 使用 entrypoint 启动 +CMD ["/app/entrypoint.sh"] +``` + +### Docker Compose 配置 + +```yaml +backend: + build: + context: /opt/nginx/html/ai/current # 包含 shared 目录 + dockerfile: backend/Dockerfile + volumes: + - /opt/nginx/html/ai/current/backend/prisma:/app/prisma # 数据库持久化 +``` + +## 常见操作 + +### 创建新的数据库迁移 + +```bash +# 1. 修改 schema.prisma +vim backend/prisma/schema.prisma + +# 2. 创建迁移 +cd backend +npm run prisma:migrate -- --name add_new_field + +# 3. 提交到 Git +git add backend/prisma/migrations/ +git commit -m "feat: 添加新字段" +git push +``` + +### 手动执行迁移(如果需要) + +```bash +# 在容器内执行 +docker exec -it ai-learning-backend sh +npx prisma migrate deploy +``` + +### 查看迁移状态 + +```bash +# 查看数据库中的迁移记录 +docker exec -it ai-learning-backend sh +npx prisma migrate status +``` + +### 回滚迁移(谨慎操作) + +Prisma 不直接支持回滚,需要: +1. 创建新的迁移来撤销更改 +2. 或手动修改数据库 + +## 注意事项 + +1. **迁移文件必须提交到 Git** + - 所有迁移文件都需要版本控制 + - 确保团队成员都能访问相同的迁移历史 + +2. **不要在生产环境使用 `prisma migrate dev`** + - 这会创建新的迁移文件 + - 生产环境应使用 `prisma migrate deploy` + +3. **数据库文件持久化** + - 数据库文件通过 Docker volume 挂载 + - 位置: `/opt/nginx/html/ai/current/backend/prisma/dev.db` + - 容器重启不会丢失数据 + +4. **首次部署** + - 容器启动时会自动创建数据库 + - 自动应用所有迁移 + - 自动填充初始数据(seed) + +5. **后续更新** + - 容器启动时自动检查并应用新迁移 + - 不会重复执行已应用的迁移 + - 不会重复执行 seed(seed 脚本会清理现有数据) + +## 故障排查 + +### 迁移失败 + +```bash +# 查看容器日志 +docker logs ai-learning-backend + +# 进入容器检查 +docker exec -it ai-learning-backend sh +npx prisma migrate status +``` + +### 数据库锁定 + +SQLite 数据库可能被锁定,检查是否有其他进程在使用数据库。 + +### Seed 执行失败 + +Seed 脚本失败不会阻止容器启动,但会输出警告信息。检查日志确认原因。 + +## 总结 + +✅ **自动化**: 容器启动时自动执行迁移 +✅ **安全**: 迁移文件版本控制 +✅ **持久化**: 数据库文件持久化存储 +✅ **灵活**: 支持开发和生产环境的不同流程 + +每次代码更新时,只需: +1. 创建迁移(如需要) +2. 提交到 Git +3. Jenkins 自动部署 +4. 容器启动时自动应用迁移 + +无需手动干预! diff --git a/Jenkinsfile b/Jenkinsfile index 7f0341d..12806d0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -21,94 +21,51 @@ pipeline { stage('Build') { steps { script { - // 先检查 Docker 命令是否可用 - def dockerAvailable = false + // 定义构建命令(公共部分) + def buildCommands = ''' + echo "Node 版本: $(node --version)" + echo "NPM 版本: $(npm --version)" + echo "安装依赖..." + npm install + npm run install:all + echo "构建前端..." + npm run build --workspace=frontend + echo "构建后端..." + cd backend && npm run build && npm run prisma:generate && cd .. + ''' + + // 尝试使用 Docker 构建,失败则回退到主机构建 + def useDocker = false try { def dockerCheck = sh( - script: 'command -v docker || docker --version', + script: 'command -v docker >/dev/null 2>&1 && docker --version', returnStatus: true ) if (dockerCheck == 0) { - dockerAvailable = true - echo "Docker 命令可用" - } else { - echo "Docker 命令不可用,将使用主机构建" + useDocker = true } } catch (Exception e) { - echo "检查 Docker 时出错: ${e.getMessage()}" - dockerAvailable = false + echo "Docker 不可用,使用主机构建" } - // 如果 Docker 可用,尝试使用 Docker 构建 - if (dockerAvailable) { + if (useDocker) { try { - def nodeImage = docker.image("node:18") - echo "使用 Docker 容器构建..." - nodeImage.inside('-v /var/run/docker.sock:/var/run/docker.sock') { - sh ''' - echo "在 Docker 容器中构建..." - echo "Node 版本:" - node --version - echo "NPM 版本:" - npm --version - - # 安装依赖 - echo "安装依赖..." - npm install - npm run install:all - - # 构建前端 - echo "构建前端..." - npm run build --workspace=frontend - - # 构建后端 - echo "构建后端..." - cd backend - npm run build - npm run prisma:generate - cd .. - ''' + docker.image("node:18").inside('-v /var/run/docker.sock:/var/run/docker.sock') { + sh buildCommands } } catch (Exception e) { - echo "Docker 构建失败,回退到主机构建..." - echo "错误信息: ${e.getMessage()}" - dockerAvailable = false + echo "Docker 构建失败,回退到主机构建: ${e.getMessage()}" + useDocker = false } } - // 如果 Docker 不可用或失败,使用主机构建 - if (!dockerAvailable) { - echo "使用主机构建..." + if (!useDocker) { sh ''' - echo "在主机上构建..." - - # 检查 Node.js if ! command -v node &> /dev/null; then echo "错误: Node.js 未安装,请安装 Node.js 18+" exit 1 fi - - echo "Node 版本:" - node --version - echo "NPM 版本:" - npm --version - - # 安装依赖 - echo "安装依赖..." - npm install - npm run install:all - - # 构建前端 - echo "构建前端..." - npm run build --workspace=frontend - - # 构建后端 - echo "构建后端..." - cd backend - npm run build - npm run prisma:generate - cd .. - ''' + ''' + buildCommands } } } @@ -124,24 +81,18 @@ pipeline { # 复制前端构建产物 cp -r frontend/dist deploy-package/frontend-dist - # 复制后端构建产物和必要文件 + # 复制后端文件 mkdir -p deploy-package/backend - cp -r backend/dist deploy-package/backend/dist - cp -r backend/prisma deploy-package/backend/prisma - cp backend/package.json deploy-package/backend/ + cp -r backend/{dist,prisma} deploy-package/backend/ + cp backend/{package.json,Dockerfile,entrypoint.sh} deploy-package/backend/ 2>/dev/null || true cp backend/package-lock.json deploy-package/backend/ 2>/dev/null || true + chmod +x deploy-package/backend/entrypoint.sh 2>/dev/null || true - # 复制 Docker 相关文件 + # 复制 shared 包和 Docker 配置 + [ -d shared ] && cp -r shared deploy-package/ mkdir -p deploy-package/docker - cp nginx/docker-compose.production.yml deploy-package/docker/docker-compose.yml - cp nginx/nginx.conf.docker deploy-package/docker/nginx.conf.docker - if [ -f backend/Dockerfile ]; then - cp backend/Dockerfile deploy-package/backend/ - fi - - # 复制部署脚本 - cp scripts/deploy-docker.sh deploy-package/ - chmod +x deploy-package/deploy-docker.sh + cp nginx/{docker-compose.production.yml,nginx.conf.docker} deploy-package/docker/ + cp scripts/deploy-docker.sh deploy-package/ && chmod +x deploy-package/deploy-docker.sh # 创建部署包 tar -czf deploy-package.tar.gz deploy-package/ @@ -149,93 +100,36 @@ pipeline { } } - stage('Deploy to Remote Server') { + stage('Deploy') { steps { echo '部署到远程服务器...' - script { - // 使用 SSH 传输文件并执行部署 - sh ''' - # 传输部署包到远程服务器 - scp -o StrictHostKeyChecking=no deploy-package.tar.gz ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/ - - # 在远程服务器上执行部署 - ssh -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << 'ENDSSH' - # 创建部署目录 - mkdir -p /opt/nginx/html/ai - - # 解压部署包 - cd /tmp - tar -xzf deploy-package.tar.gz - - # 备份旧版本(如果存在) - if [ -d /opt/nginx/html/ai/current ]; then - mv /opt/nginx/html/ai/current /opt/nginx/html/ai/backup-$(date +%Y%m%d-%H%M%S) - fi - - # 创建新版本目录 - mkdir -p /opt/nginx/html/ai/current - - # 复制前端文件 - cp -r deploy-package/frontend-dist/* /opt/nginx/html/ai/current/ - - # 复制后端文件 - mkdir -p /opt/nginx/html/ai/current/backend - cp -r deploy-package/backend/* /opt/nginx/html/ai/current/backend/ - - # 复制 Docker 配置文件 - mkdir -p /opt/nginx/html/ai/current/docker - cp deploy-package/docker/docker-compose.yml /opt/nginx/html/ai/current/docker/ - cp deploy-package/docker/nginx.conf.docker /opt/nginx/html/ai/current/docker/ - - # 创建 nginx logs 目录 - mkdir -p /opt/nginx/html/ai/current/docker/logs - - # 执行 Docker 部署脚本 - if [ -f deploy-package/deploy-docker.sh ]; then - chmod +x deploy-package/deploy-docker.sh - bash deploy-package/deploy-docker.sh /opt/nginx/html/ai/current - fi - - # 清理临时文件 - rm -rf /tmp/deploy-package /tmp/deploy-package.tar.gz - - echo "部署完成!" - ENDSSH - ''' - } - } - } - - stage('Restart Services') { - steps { - echo '重启 Docker 服务...' sh ''' + # 传输部署包 + scp -o StrictHostKeyChecking=no deploy-package.tar.gz ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/ + + # 在远程服务器上执行部署(deploy-docker.sh 已包含重启服务逻辑) ssh -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << 'ENDSSH' - # 使用 Docker Compose 重启服务 - cd /opt/nginx/html/ai/current/docker + set -e + mkdir -p /opt/nginx/html/ai + cd /tmp && tar -xzf deploy-package.tar.gz - # 检查 docker-compose 命令(支持新版本的 docker compose) - if command -v docker-compose &> /dev/null; then - COMPOSE_CMD="docker-compose" - else - COMPOSE_CMD="docker compose" - fi + # 备份旧版本 + [ -d /opt/nginx/html/ai/current ] && \ + mv /opt/nginx/html/ai/current /opt/nginx/html/ai/backup-$(date +%Y%m%d-%H%M%S) - # 停止旧容器 - $COMPOSE_CMD down || true + # 创建新版本目录并复制文件 + mkdir -p /opt/nginx/html/ai/current/{backend,docker/logs} + cp -r deploy-package/frontend-dist/* /opt/nginx/html/ai/current/ + cp -r deploy-package/backend/* /opt/nginx/html/ai/current/backend/ + [ -d deploy-package/shared ] && cp -r deploy-package/shared /opt/nginx/html/ai/current/ + cp deploy-package/docker/* /opt/nginx/html/ai/current/docker/ - # 重新构建并启动 - $COMPOSE_CMD up -d --build + # 执行部署脚本(包含停止、构建、启动和健康检查) + chmod +x deploy-package/deploy-docker.sh + bash deploy-package/deploy-docker.sh /opt/nginx/html/ai/current - # 等待服务启动 - sleep 5 - - # 检查服务状态 - $COMPOSE_CMD ps - - echo "Docker 服务已重启" - echo "前端访问: http://180.76.180.105:8080" - echo "后端 API: http://180.76.180.105:3001" + # 清理临时文件 + rm -rf /tmp/deploy-package /tmp/deploy-package.tar.gz ENDSSH ''' } diff --git a/backend/Dockerfile b/backend/Dockerfile index e3fb7db..57c5bf3 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -20,14 +20,21 @@ COPY backend/prisma ./prisma/ RUN sed -i 's|"@ai-learning/shared": "\*"|"@ai-learning/shared": "file:./shared"|' package.json && \ npm install --production +# 安装 tsx(用于执行 seed.ts) +RUN npm install -g tsx || npm install tsx --save-dev || true + # 复制构建产物 COPY backend/dist ./dist -# 生成 Prisma Client +# 复制并设置 entrypoint 脚本 +COPY backend/entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + +# 生成 Prisma Client(在构建时生成,启动时也会重新生成以确保最新) RUN npx prisma generate # 暴露端口 EXPOSE 3001 -# 启动应用 -CMD ["node", "dist/index.js"] +# 使用 entrypoint 脚本启动(会自动执行数据库迁移) +CMD ["/app/entrypoint.sh"] diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100644 index 0000000..05b1533 --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/sh +set -e + +echo "🚀 启动后端服务..." + +# 生成 Prisma Client(确保最新) +echo "📦 生成 Prisma Client..." +npx prisma generate + +# 检查数据库是否存在 +if [ ! -f "prisma/dev.db" ]; then + echo "📦 数据库不存在,执行初始化..." + echo "🔄 执行数据库迁移..." + npx prisma migrate deploy + + echo "🌱 填充初始数据..." + npx tsx prisma/seed.ts || echo "⚠️ Seed 执行失败或已存在数据" +else + echo "🔄 数据库已存在,执行迁移..." + npx prisma migrate deploy +fi + +# 启动应用 +echo "✅ 数据库就绪,启动应用..." +exec node dist/index.js diff --git a/nginx/docker-compose.production.yml b/nginx/docker-compose.production.yml index 686937f..f2b791b 100644 --- a/nginx/docker-compose.production.yml +++ b/nginx/docker-compose.production.yml @@ -21,8 +21,8 @@ services: # 后端服务 backend: build: - context: /opt/nginx/html/ai/current/backend - dockerfile: Dockerfile + context: /opt/nginx/html/ai/current # 使用项目根目录作为构建上下文(需要访问 shared 目录) + dockerfile: backend/Dockerfile container_name: ai-learning-backend ports: - "3001:3001"