Files
AIclinicalresearch/docs/05-部署文档/_archive-2025首次部署/05-Node.js后端-SAE容器部署指南.md
HaHafeng 6124c7abc6 docs(platform): Add database documentation system and restructure deployment docs
Completed:
- Add 6 core database documents (docs/01-平台基础层/07-数据库/)
  Architecture overview, migration history, environment comparison,
  tech debt tracking, seed data management, PostgreSQL extensions
- Restructure deployment docs: archive 20 legacy files to _archive-2025/
- Create unified daily operations manual (01-日常更新操作手册.md)
- Add pending deployment change tracker (03-待部署变更清单.md)
- Update database development standard to v3.0 (three iron rules)
- Fix Prisma schema type drift: align @db.* annotations with actual DB
  IIT: UUID/Timestamptz(6), SSA: Timestamp(6)/VarChar(20/50/100)
- Add migration: 20260227_align_schema_with_db_types (idempotent ALTER)
- Add Cursor Rule for auto-reminding deployment change documentation
- Update system status guide v6.4 with deployment and DB doc references
- Add architecture consultation docs (Prisma guide, SAE deployment guide)

Technical details:
- Manual migration due to shadow DB limitation (TD-001 in tech debt)
- Deployment docs reduced from 20+ scattered files to 3 core documents
- Cursor Rule triggers on schema.prisma, package.json, Dockerfile changes

Made-with: Cursor
2026-02-27 14:35:25 +08:00

56 KiB
Raw Blame History

Node.js 后端 - SAE 容器部署完全指南

文档版本: v1.1 (修复 Prisma 构建和数据库同步问题)
创建时间: 2025-12-13
最后修订: 2025-12-13
适用范围: AIclinicalresearch 平台 - Node.js 后端服务
目标读者: 运维工程师、后端开发工程师
部署目标: 阿里云 SAEServerless 应用引擎)容器部署

v1.1 更新日志:

  • 🛑 修复Prisma 目录构建上下文问题(增加预处理步骤)
  • 🛑 修复:生产环境缺少 Prisma CLI 导致迁移失败
  • 🔥 新增:针对开发历史不规范的"反向同步"流程(关键)
  • 优化数据库迁移策略pg_dump 导入后不执行 migrate
  • 简化:移除 Init Container 方案,改用启动命令

📋 目录

  1. 为什么选择 SAE 容器部署
  2. 部署前准备
  3. 后端服务分析
  4. 🔥 Prisma 反向同步(必读)
  5. 构建 Docker 镜像
  6. 本地测试验证
  7. 推送到 ACR
  8. SAE 应用配置
  9. 数据库部署策略
  10. 端到端测试
  11. 监控与维护
  12. 故障排查
  13. 注意事项与禁忌

1. 为什么选择 SAE 容器部署

核心优势

优势 说明 对您的价值
环境一致性 Docker 镜像保证本地开发Node 22 + Prisma与线上环境完全一致 杜绝"我本地明明能跑"的问题
弹性伸缩 SAE 根据 CPU/内存使用率自动增减实例数量 大量用户 AI 对话时自动扩容
免运维 无需管理服务器 OS、安全补丁SAE 全托管 1-2 人团队节省运维精力
内网互通 与 SAE 的 Python 服务和 ECS 的 Dify 通过 VPC 内网高速通信 毫秒级延迟,无公网流量费
私有化就绪 Docker 镜像可直接交付给医院,部署到内网环境 满足医疗数据合规要求
统一架构 前端Nginx、后端Node.js、Python 微服务都用 Docker 一套部署流程,降低学习成本

🎯 为什么不用 Code Package 部署?

对比项 Code Package 容器部署(推荐)
环境一致性 ⚠️ 云端 Node 版本可能不同 镜像锁定版本
私有化交付 无法直接交付 打包即可交付
系统依赖 ⚠️ 无法自定义 Dockerfile 完全控制
构建速度 快(上传即可) ⚠️ 慢(需构建镜像)
适用场景 快速原型验证 生产环境、私有化

结论:对于需要私有化部署的医疗科研产品,容器部署是唯一选择。


2. 部署前准备

前置条件检查清单

本地开发环境

  • Docker Desktop 已安装并运行(版本 20.10+
  • Node.js 已安装(版本 22+
  • 后端代码已拉取到本地
  • 后端在本地能正常启动(npm run dev

验证命令:

# 检查 Docker
docker --version
# 输出示例Docker version 24.0.6

# 检查 Node.js
node --version
# 输出示例v22.11.0

# 检查 npm
npm --version
# 输出示例10.9.0

阿里云资源

  • RDS PostgreSQL 15 实例已创建并运行

    • 数据库名称:ai_clinical(或自定义)
    • 用户名和密码已准备
    • 内网地址已获取(如 pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432
    • 白名单已配置(允许 SAE VPC 访问)
  • 阿里云容器镜像服务 ACR 已开通

    • 命名空间已创建(如 clinical-research
    • 登录密码已设置
  • SAE 应用 已创建(或准备创建)

    • VPC 和交换机已选择(与 RDS 在同一 VPC
  • 依赖服务的内网地址已获取

    • Python 微服务SAEhttp://172.17.x.x:8000
    • Dify 服务ECShttp://172.17.x.x:80

敏感信息准备

准备以下配置信息(稍后配置到 SAE 环境变量):

# 数据库
DATABASE_URL=postgresql://username:password@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical?connection_limit=18&pool_timeout=10

# LLM API Keys至少配置一个
DEEPSEEK_API_KEY=sk-xxxxx
DASHSCOPE_API_KEY=sk-xxxxx
CLOSEAI_API_KEY=sk-xxxxx

# Dify
DIFY_API_KEY=app-xxxxx
DIFY_API_URL=http://172.17.x.x:80/v1

# 阿里云 OSS
OSS_REGION=oss-cn-beijing
OSS_BUCKET=clinical-research-files
OSS_ACCESS_KEY_ID=LTAI5t...
OSS_ACCESS_KEY_SECRET=xxx...

# JWT 安全密钥(生产环境必须修改)
JWT_SECRET=your-strong-random-secret-min-32-chars

3. 后端服务分析

📦 技术栈

技术 版本 用途
Node.js 22+ 运行时环境
Fastify 5.6+ Web 框架(高性能)
TypeScript 5.9+ 类型安全
Prisma 6.17+ ORM数据库访问
pg-boss 12.5+ 队列Postgres-Only
winston 3.18+ 日志系统

📊 依赖服务

Node.js 后端SAE
    │
    ├──→ RDS PostgreSQL 15数据库
    │
    ├──→ Python 微服务SAE  - 文档提取
    │     └─ http://172.17.x.x:8000
    │
    ├──→ Dify 服务ECS       - RAG 知识库
    │     └─ http://172.17.x.x:80/v1
    │
    └──→ 阿里云 OSS              - 文件存储
          └─ clinical-research-files

🔧 核心功能模块

模块 路径 功能 依赖服务
AIA /api/chat AI 智能助理 DeepSeek/千问/Claude
PKB /api/knowledge-bases 个人知识库 Dify + OSS
ASL /api/asl/* 智能文献 Python 微服务 + OSS
DC /api/dc/* 数据清洗 Python 微服务 + OSS
RVW /api/review 稿件审查 DeepSeek/千问
Health /health 健康检查 RDS

📝 启动流程

# 1. 安装依赖
npm install

# 2. 生成 Prisma Client
npm run prisma:generate

# 3. 数据库迁移(仅首次或更新时)
npx prisma migrate deploy

# 4. 编译 TypeScript
npm run build

# 5. 启动应用
npm start

4. 🔥 Prisma 反向同步(必读)

⚠️ 重要警告:为什么需要这一步?

您的开发历史

  • 使用 pg_dump 导出了本地 PostgreSQL 数据库(包含表结构和数据)
  • 已经导入到阿里云 RDS PostgreSQL
  • ⚠️ 但是:开发过程中,经常直接用 SQL 或 Navicat 修改数据库,没有同步更新 schema.prisma 文件

后果

// schema.prisma 中定义:
model User {
  id    String
  email String
  name  String
  // ❌ 缺少 phone 字段
}

// 但数据库里实际有:
CREATE TABLE "User" (
  id    VARCHAR(255),
  email VARCHAR(255),
  name  VARCHAR(255),
  phone VARCHAR(50)  --  这个字段存在,但 Prisma 不知道
);

// 代码里尝试访问:
const user = await prisma.user.findUnique({ where: { id: '123' } });
console.log(user.phone); // ❌ TypeScript 报错Property 'phone' does not exist on type 'User'

更严重的后果

  • Prisma Client 生成的类型与数据库不一致
  • 运行时可能读不到数据或报错
  • 如果强行运行 prisma migrate deploy,可能因为表已存在而失败

解决方案反向同步Introspection

步骤 1连接到 RDS 数据库

在本地开发环境,临时连接到 RDS

# 1. 备份当前的 .env 文件
cp backend/.env backend/.env.backup

# 2. 创建临时 RDS 连接配置
cat > backend/.env.rds <<EOF
DATABASE_URL="postgresql://username:password@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical?connection_limit=18&pool_timeout=10"
EOF

# 3. 使用 RDS 配置
cd backend
export $(cat .env.rds | xargs)

步骤 2执行反向同步关键步骤

# 让 Prisma 读取 RDS 的真实结构,自动重写 schema.prisma
npx prisma db pull

# 输出示例:
# Prisma schema loaded from prisma/schema.prisma
# Datasource "db": PostgreSQL database "ai_clinical" at "pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432"
# 
# Introspecting based on datasource defined in prisma/schema.prisma …
# 
# ✔ Introspected 45 models and wrote them into prisma/schema.prisma in 2.34s
# 
# Run prisma generate to generate Prisma Client.

这个命令的魔力

  • 它会扫描数据库的所有表、字段、类型、关系
  • 然后完全重写 prisma/schema.prisma 文件
  • 保证 Schema 与数据库 100% 一致

步骤 3查看并确认变更

# 查看 schema.prisma 的变化
git diff prisma/schema.prisma

# 你会看到:
# + phone       String?  @db.VarChar(50)  // 新增的字段
# - role        String   @default("user")  // 如果数据库里改成了 user_role
# + user_role   String   @default("user")

⚠️ 人工检查清单

  1. 检查新增字段

    • 是否有意外的字段(如测试字段)?
    • 字段类型是否正确?
  2. 检查删除字段

    • 如果 Schema 里的字段在数据库中不存在,db pull 会删除它
    • 确认这些字段是否真的应该删除
  3. 检查关系Relations

    • 外键关系是否正确识别?
    • @relation 注解是否合理?
  4. 检查索引

    • 是否保留了所有 @@index@@unique

步骤 4重新生成 Prisma Client

# 基于新的 Schema 生成客户端
npx prisma generate

# 输出示例:
# ✔ Generated Prisma Client (5.7.0) to ./node_modules/@prisma/client in 234ms

验证生成结果

// 在代码中测试(临时)
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// TypeScript 应该能识别新字段
const user = await prisma.user.findUnique({ where: { id: '123' } });
console.log(user.phone); // ✅ 现在可以访问了

步骤 5提交代码

# 恢复本地 .env重要
cd backend
cp .env.backup .env
rm .env.rds

# 提交同步后的 Schema
git add prisma/schema.prisma
git commit -m "chore: sync Prisma schema with RDS database (introspection)"
git push

🚨 常见问题处理

问题 1db pull 报错:无法连接数据库

# 错误信息:
Error: P1001: Can't reach database server at `pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432`

解决方法

# 1. 检查 RDS 白名单
# 阿里云控制台 → RDS → 数据安全性 → 白名单设置
# 添加你的本地公网 IP查询curl ipinfo.io

# 2. 测试连接
psql "postgresql://username:password@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical"

# 如果能连上,再执行 npx prisma db pull

问题 2db pull 后,某些字段的类型变了

// 之前:
model User {
  age Int
}

// db pull 后:
model User {
  age String  @db.VarChar(10)  // ❌ 类型变成了 String
}

原因:数据库中的字段类型确实是 VARCHAR(10),而不是 INT

解决方法

  1. 修正数据库(推荐):
    ALTER TABLE "User" ALTER COLUMN age TYPE INTEGER USING age::INTEGER;
    
  2. 接受现状(不推荐):如果数据库确实要用 VARCHAR 存储年龄,那就改代码逻辑。

问题 3db pull 后,丢失了自定义注释

// 之前:
model User {
  /// 用户的唯一标识符
  id String @id @default(uuid())
}

// db pull 后:
model User {
  id String @id @default(uuid())  // ❌ 注释丢失
}

原因db pull 只能读取数据库结构,无法读取代码注释。

解决方法:手动恢复重要的注释。


5. 构建 Docker 镜像

🛑 前置步骤:准备 Prisma 文件(必须执行)

问题:项目结构中,prisma 文件夹在根目录,但 Dockerfile 在 backend/ 目录。如果直接在 backend/ 目录构建Docker 看不到上一层的 prisma 文件夹。

解决方案 A复制 Prisma 到 backend 目录(推荐)

# 在项目根目录执行
cd AIclinicalresearch

# 复制 prisma 文件夹到 backend 目录
cp -r prisma backend/prisma

# 验证复制成功
ls backend/prisma/schema.prisma
# 应该输出backend/prisma/schema.prisma

# 注意:
# 1. 这个复制是临时的,用于构建镜像
# 2. 不要把 backend/prisma 提交到 Git已在 .gitignore 中)
# 3. 每次构建镜像前都需要重新复制(确保最新)

解决方案 B在根目录构建适合 CI/CD

如果你使用 CI/CD 自动化构建,可以在根目录构建:

# 在根目录构建,需要修改 Dockerfile 的路径
# 这里先不展开,推荐使用方案 A

📝 创建 Dockerfile

backend/ 目录下创建 Dockerfile

# ==================== 阶段 1: 构建阶段 ====================
FROM node:22-alpine AS builder

# 安装编译工具Prisma 需要)
RUN apk add --no-cache \
    python3 \
    make \
    g++ \
    openssl

WORKDIR /app

# 1. 复制依赖文件
COPY package*.json ./

# 2. 复制 Prisma Schema确保在 backend/ 目录下已有 prisma 文件夹)
COPY prisma ./prisma/

# 3. 安装依赖(包括 devDependencies用于构建
# ⚠️ 注意:这里安装全部依赖,包括 prisma CLI
RUN npm ci

# 4. 复制源代码
COPY . .

# 5. 生成 Prisma Client
RUN npm run prisma:generate

# 6. 编译 TypeScript
RUN npm run build

# ⚠️ 不要在这里执行 npm prune --production
# 因为我们需要在阶段 2 保留 prisma CLI 用于生产环境迁移

# ==================== 阶段 2: 运行阶段 ====================
FROM node:22-alpine

# 安装运行时依赖 + 时区数据
RUN apk add --no-cache \
    openssl \
    curl \
    ca-certificates \
    tzdata

# ⚠️ 统一时区Asia/Shanghai
ENV TZ=Asia/Shanghai

# 创建非 root 用户(安全最佳实践)
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# 从构建阶段复制产物
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
COPY --from=builder --chown=nodejs:nodejs /app/prisma ./prisma

# 🔥 关键:全局安装 Prisma CLI用于生产环境可能的迁移操作
# 注意:这会增加约 50MB 镜像体积,但确保生产环境可以执行 prisma 命令
RUN npm install -g prisma@6.17.0

# 创建上传目录(用于临时文件)
RUN mkdir -p /app/uploads && chown -R nodejs:nodejs /app/uploads

# 切换到非 root 用户
USER nodejs

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3001/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); })"

# 暴露端口
EXPOSE 3001

# 🔥 启动命令(仅启动应用,不执行数据库迁移)
# 解释:因为数据库已通过 pg_dump 导入,结构已就绪,无需 migrate
CMD ["node", "dist/index.js"]

📝 Dockerfile 关键修改说明

修改 1全局安装 Prisma CLI

# 🔥 在阶段 2 新增:
RUN npm install -g prisma@6.17.0

原因

  • npm ci + npm prune --production 会删除 devDependencies 中的 prisma
  • 但生产环境可能需要执行 npx prisma db pull 或排查问题
  • 全局安装确保 prisma 命令始终可用

代价

  • 镜像体积增加约 50MB
  • 但这是值得的(避免"命令找不到"的问题)

修改 2不执行数据库迁移

# ❌ 错误做法(会导致"表已存在"错误):
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/index.js"]

# ✅ 正确做法(仅启动应用):
CMD ["node", "dist/index.js"]

原因

  • 您的数据库是通过 pg_dump 导入的,表结构已经存在
  • 如果执行 prisma migrate deploy,可能因为迁移记录对不上而报错
  • 正确的做法:启动前确保 schema.prisma 与数据库一致(第 4 节已处理)

修改 3移除 dumb-init可选优化

# 移除:
# RUN apk add --no-cache dumb-init
# ENTRYPOINT ["dumb-init", "--"]

# Node.js 22 在处理信号时已经足够稳定
# 如果你发现下载 dumb-init 很慢或报错,可以去掉这个优化

📝 创建 .dockerignore

backend/ 目录下创建 .dockerignore

# Node.js
node_modules
npm-debug.log
yarn-error.log

# 开发文件
.env
.env.*
*.local

# 构建产物(已在 Dockerfile 中生成)
dist

# 测试文件
test
tests
*.test.ts
*.spec.ts
coverage

# 文档和临时文件
docs
*.md
.vscode
.idea
.DS_Store
Thumbs.db

# 上传文件(运行时生成)
uploads/*

# Git
.git
.gitignore

# 日志
*.log
logs

# 临时文件
temp
tmp
*.swp
*.swo
*~

# 数据库文件SQLite如果有
*.db
*.sqlite

# 脚本文件(仅开发使用)
scripts/*.ts
*.bat
*.ps1

6. 本地测试验证

🛑 前置步骤:复制 Prisma 文件(必须执行)

# 1. 回到项目根目录
cd AIclinicalresearch

# 2. 复制 prisma 到 backend 目录(确保构建时能找到)
cp -r prisma backend/prisma

# 3. 验证复制成功
ls backend/prisma/schema.prisma
# 应该输出backend/prisma/schema.prisma

步骤 1构建镜像

# 在 backend 目录下执行
cd backend

# 🔥 确保已执行上面的复制步骤!

# 构建镜像(需要 5-10 分钟)
docker build -t backend-service:v1.0.0 .

# 查看镜像大小
docker images backend-service:v1.0.0
# 预期大小:~300-500MBAlpine 基础镜像 + Node.js + 依赖)

如果构建失败

# 常见问题 1Prisma 文件找不到
# 错误信息COPY failed: file not found in build context or excluded by .dockerignore: stat prisma: file does not exist
# 解决确保执行了复制步骤cp -r ../prisma ./prisma在 backend 目录外执行)

# 常见问题 2网络超时npm install 慢)
# 解决:使用国内镜像源
# 在 Dockerfile 的 npm ci 之前添加:
RUN npm config set registry https://registry.npmmirror.com

# 常见问题 3Prisma 生成失败
# 解决:检查 backend/prisma/schema.prisma 是否存在且格式正确

步骤 2本地运行测试

# 创建测试环境变量文件
cat > .env.docker.test <<EOF
NODE_ENV=production
PORT=3001
DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/ai_clinical?connection_limit=18
STORAGE_TYPE=local
CACHE_TYPE=postgres
QUEUE_TYPE=pgboss
JWT_SECRET=test-secret-key-change-in-production
DEEPSEEK_API_KEY=sk-xxxxx
DIFY_API_KEY=app-xxxxx
DIFY_API_URL=http://host.docker.internal/v1
LOG_LEVEL=info
EOF

# 运行容器
docker run -d \
  --name backend-test \
  --env-file .env.docker.test \
  -p 3001:3001 \
  backend-service:v1.0.0

# 查看启动日志
docker logs -f backend-test

# 应该看到:
# [Config] Environment validation passed
# [Database] Connection pool calculation:
# [Database] ✅ 数据库连接成功!
# [Fastify] Server listening on http://0.0.0.0:3001

步骤 3测试健康检查

# 测试健康检查端点
curl http://localhost:3001/health

# 预期返回:
{
  "status": "healthy",
  "timestamp": "2025-12-13T10:30:00.000Z",
  "uptime": 45.123,
  "database": {
    "status": "connected",
    "connections": 2
  },
  "version": "1.0.0"
}

步骤 4测试 API 端点

# 测试用户注册(示例)
curl -X POST http://localhost:3001/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "Test123456",
    "name": "测试用户"
  }'

# 预期返回:
{
  "success": true,
  "user": {
    "id": "...",
    "email": "test@example.com",
    "name": "测试用户"
  },
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

步骤 5清理测试容器

# 停止并删除测试容器
docker stop backend-test
docker rm backend-test

# 删除测试环境变量文件
rm .env.docker.test

步骤 5清理临时文件

# 构建成功后,清理 backend/prisma避免误提交到 Git
cd backend
rm -rf prisma

# 验证清理成功
ls prisma 2>/dev/null || echo "✅ prisma 目录已清理"

# 注意:根目录的 prisma/ 文件夹保留,这是源文件

7. 推送到 ACR

步骤 1登录 ACR

# 获取 ACR 登录地址(阿里云控制台 → 容器镜像服务 → 访问凭证)
# 示例registry.cn-hangzhou.aliyuncs.com

# 登录(使用 ACR 密码,不是阿里云账号密码)
docker login --username=your-aliyun-account registry.cn-hangzhou.aliyuncs.com

# 输入密码后看到:
# Login Succeeded

步骤 2标记镜像

# 格式registry地址/命名空间/仓库名:版本号
docker tag backend-service:v1.0.0 \
  registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:v1.0.0

# 同时打一个 latest 标签(方便测试)
docker tag backend-service:v1.0.0 \
  registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:latest

步骤 3推送镜像

# 推送指定版本
docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:v1.0.0

# 推送 latest
docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:latest

# 推送过程需要 5-10 分钟(视网络速度)

步骤 4验证推送成功

登录阿里云控制台 → 容器镜像服务 → 镜像仓库 → backend-service

  • 应该看到版本:v1.0.0latest
  • 镜像大小:~300-500MB
  • 推送时间:刚才的时间

8. SAE 应用配置

步骤 1创建应用

阿里云控制台SAE应用列表创建应用

配置项 说明
应用名称 backend-service 后端服务
命名空间 选择已创建的命名空间 与 Python 服务同一命名空间
VPC 选择 RDS 所在 VPC 必须与 RDS 在同一 VPC
交换机 选择可用区 建议多可用区
应用实例规格 2核4G 初始规格
实例数量 2 最小 2 个实例(高可用)

步骤 2配置镜像

配置项
镜像类型 容器镜像服务企业版实例
镜像仓库 registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service
镜像版本 v1.0.0
镜像拉取策略 总是拉取镜像

步骤 3配置端口

配置项
容器端口 3001
协议 TCP

步骤 4配置环境变量关键步骤

⚠️ 重要:请仔细配置以下环境变量

基础配置

NODE_ENV=production
PORT=3001
HOST=0.0.0.0
LOG_LEVEL=info
SERVICE_NAME=backend-service

数据库配置(必需)

# ⚠️ 使用 RDS 内网地址
DATABASE_URL=postgresql://username:password@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical?connection_limit=18&pool_timeout=10

# 连接池配置(根据 RDS 规格调整)
DB_MAX_CONNECTIONS=400
MAX_INSTANCES=20

连接池计算说明

  • 假设 RDS 最大连接数400
  • SAE 最大实例数20
  • 每实例连接数 = (400 / 20) - 预留 = 18
  • 预留 10 个连接给管理任务和其他服务

存储配置(必需)

# 使用阿里云 OSS
STORAGE_TYPE=oss
OSS_REGION=oss-cn-beijing
OSS_BUCKET=clinical-research-files
OSS_ACCESS_KEY_ID=LTAI5t...
OSS_ACCESS_KEY_SECRET=xxx...

缓存和队列配置Postgres-Only

# 使用 PostgreSQL 作为缓存(不需要 Redis
CACHE_TYPE=postgres

# 使用 pg-boss 作为队列(不需要 Redis
QUEUE_TYPE=pgboss

LLM API 配置(至少配置一个)

# DeepSeek推荐性价比高
DEEPSEEK_API_KEY=sk-xxxxx
DEEPSEEK_BASE_URL=https://api.deepseek.com

# 通义千问(备选)
DASHSCOPE_API_KEY=sk-xxxxx

# CloseAIOpenAI/Claude 代理)
CLOSEAI_API_KEY=sk-xxxxx
CLOSEAI_OPENAI_BASE_URL=https://api.openai-proxy.org/v1
CLOSEAI_CLAUDE_BASE_URL=https://api.openai-proxy.org/anthropic

Dify 配置(必需)

# ⚠️ 使用 ECS 内网 IP不要使用公网域名
DIFY_API_URL=http://172.17.x.x:80/v1
DIFY_API_KEY=app-xxxxx

如何获取 Dify 内网 IP

  1. 登录 ECS 控制台
  2. 找到 Dify 所在的 ECS 实例
  3. 查看"私有 IP 地址"(如 172.16.0.20

安全配置(必需)

# ⚠️ 生产环境必须修改为强密码(至少 32 位随机字符串)
JWT_SECRET=your-strong-random-secret-min-32-chars-change-in-production
JWT_EXPIRES_IN=7d

生成强密码

# Linux/Mac
openssl rand -base64 32

# Windows PowerShell
-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32 | ForEach-Object {[char]$_})

CORS 配置(可选)

# 如果前端使用自定义域名,配置允许的源
# 注意:如果前端使用 Nginx 反向代理则不需要配置Nginx 已处理)
CORS_ORIGIN=https://your-frontend-domain.com

步骤 5配置健康检查

SAE 控制台应用配置健康检查

配置项 说明
检查方式 HTTP 请求
检查路径 /health 后端健康检查端点
检查端口 3001 与容器端口一致
检查协议 HTTP
初始延迟 60 秒 给 Prisma 初始化足够时间
检查间隔 10 秒
超时时间 3 秒
不健康阈值 3 次 连续失败 3 次标记为不健康
健康阈值 2 次 连续成功 2 次标记为健康

步骤 6配置弹性伸缩

SAE 控制台应用配置弹性伸缩

配置项 说明
最小实例数 2 高可用保证
最大实例数 10 根据预期负载调整
扩容条件 CPU > 70% 持续 3 分钟
缩容条件 CPU < 30% 持续 5 分钟

步骤 7配置日志

SAE 控制台应用配置日志配置

配置项
日志类型 标准输出stdout
日志存储 开启(保存 7 天)

步骤 8部署应用

点击"部署"按钮SAE 将:

  1. 从 ACR 拉取镜像(~2 分钟)
  2. 启动容器实例(~1 分钟)
  3. 执行健康检查(~1 分钟)
  4. 流量切换(~30 秒)

总耗时:约 5 分钟


9. 数据库部署策略

🎯 您的实际情况(非常重要)

根据您的开发历史,数据库部署策略与标准流程完全不同

标准流程(不适合您)

graph LR
    A[代码] --> B[Prisma Migrations]
    B --> C[空数据库]
    C --> D[自动创建表结构]

您的实际流程

graph LR
    A[本地 PostgreSQL<br/>包含数据] --> B[pg_dump 导出]
    B --> C[RDS 导入]
    C --> D[表结构已存在]
    D --> E[Prisma 反向同步]

正确的部署策略

策略总览

步骤 操作 何时执行 目的
1. 导出本地数据库 pg_dump 首次部署前 备份现有数据和结构
2. 导入到 RDS psql 或 DMS 首次部署前 迁移到云端
3. Prisma 反向同步 prisma db pull 首次部署前(第 4 节) 同步 Schema
4. 启动应用 node dist/index.js SAE 部署时 正常运行
5. 数据库迁移 prisma migrate deploy 不执行 表已存在,无需迁移

方案 1使用 pg_dump 导入(您的情况,推荐)

步骤 1导出本地数据库如果还没做

# 在本地开发环境执行
docker exec ai-clinical-postgres pg_dump -U postgres -d ai_clinical \
  --no-owner --no-acl --clean --if-exists \
  > ai_clinical_backup_$(date +%Y%m%d).sql

# 查看导出文件
ls -lh ai_clinical_backup_*.sql
# 应该看到文件大小(如 5.2MB

步骤 2导入到 RDS如果还没做

方法 A使用 psql 命令行(推荐)

# 连接到 RDS 并导入
psql "postgresql://username:password@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical" \
  < ai_clinical_backup_20251213.sql

# 输出示例:
# DROP TABLE
# CREATE TABLE
# INSERT 0 123
# ...
# ✅ 导入完成

方法 B使用 DMS阿里云数据管理

  1. 登录阿里云控制台 → 数据管理 DMS
  2. 连接到 RDS 实例
  3. 数据方案 → SQL 窗口 → 粘贴 SQL 文件内容
  4. 执行

步骤 3验证导入成功

# 连接到 RDS
psql "postgresql://username:password@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical"

# 查看所有 Schema
\dn

# 应该看到:
# aia_schema
# asl_schema
# common_schema
# dc_schema
# pkb_schema
# platform_schema
# rvw_schema
# ssa_schema
# st_schema
# public

# 查看用户表
SELECT count(*) FROM "User";
# 应该返回正确的用户数量

# 退出
\q

步骤 4确认已执行 Prisma 反向同步

⚠️ 这一步非常关键!

# 确保第 4 节的步骤已完成:
# 1. 已执行 npx prisma db pull
# 2. 已执行 npx prisma generate
# 3. 已提交 schema.prisma 到 Git

# 验证:检查 schema.prisma 的修改时间
ls -l prisma/schema.prisma
# 修改时间应该是最近(今天)

# 如果还没做,立即返回第 4 节执行!

方案 2标准 Prisma Migrations仅适用于新项目

⚠️ 如果您的数据库是通过 pg_dump 导入的,请跳过这一节!

点击展开:标准迁移流程(仅供参考)

适用场景

  • 全新项目RDS 数据库是空的
  • 从未手动修改过数据库结构
  • 所有表结构都通过 Prisma Migrations 管理

执行步骤

# 1. 连接到 RDS
export DATABASE_URL="postgresql://username:password@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical"

# 2. 执行迁移
cd backend
npx prisma migrate deploy

# 3. 验证
npx prisma db execute --stdin <<< "SELECT schemaname FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema') GROUP BY schemaname;"

🚨 常见错误与修正

错误 1启动时执行 prisma migrate deploy 导致失败

错误信息

Error: P3005: The database schema is not empty. Read more about how to baseline an existing production database: https://pris.ly/d/migrate-baseline

原因:表已通过 pg_dump 导入,再执行 migrate 会冲突。

解决方法

# 方法 1移除启动命令中的 migrate推荐
# SAE 控制台 → 应用配置 → 启动命令
# 确保启动命令是:
node dist/index.js

# 不要写:
# sh -c "npx prisma migrate deploy && node dist/index.js"  # ❌

错误 2应用启动后访问数据库报错

错误信息

PrismaClientKnownRequestError: 
Invalid `prisma.user.findMany()` invocation:
  column "phone" does not exist

原因schema.prisma 与数据库不一致。

解决方法

返回第 4 节,执行 npx prisma db pull 反向同步。

部署检查清单

在启动 SAE 应用前,确认以下步骤已完成:

  • 本地数据库已通过 pg_dump 导出
  • SQL 文件已导入到 RDS表结构和数据都存在
  • 已执行 npx prisma db pull 同步 Schema
  • 已执行 npx prisma generate 生成客户端
  • 已提交 schema.prisma 到 Git
  • Docker 镜像的启动命令不包含 prisma migrate deploy
  • SAE 环境变量中的 DATABASE_URL 指向 RDS

如果以上都确认,可以放心部署!


10. 端到端测试

步骤 1获取应用访问地址

SAE 控制台应用详情应用访问配置

复制以下地址:

# 公网访问地址(用于前端调用)
https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com

# VPC 内网访问地址(用于服务间调用)
http://172.16.0.30:3001

步骤 2测试健康检查

# 使用公网地址测试
curl https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/health

# 预期返回:
{
  "status": "healthy",
  "timestamp": "2025-12-13T10:30:00.000Z",
  "uptime": 12345.678,
  "database": {
    "status": "connected",
    "connections": 8
  },
  "version": "1.0.0"
}

步骤 3测试用户注册

curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "sae-test@example.com",
    "password": "Test123456",
    "name": "SAE测试用户"
  }'

# 预期返回:
{
  "success": true,
  "user": {
    "id": "...",
    "email": "sae-test@example.com",
    "name": "SAE测试用户"
  },
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

步骤 4测试文件上传PKB 模块)

# 获取 Token从步骤 3
TOKEN="eyJhbGciOiJIUzI1NiIs..."

# 上传 PDF 文档
curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/knowledge-bases/kb-xxx/documents \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@test.pdf" \
  -F "name=测试文档"

# 预期返回:
{
  "success": true,
  "document": {
    "id": "...",
    "name": "测试文档",
    "status": "processing"
  }
}

步骤 5测试 Python 微服务调用

# 测试 ASL 模块的 PDF 提取功能
curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/asl/extract \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@paper.pdf"

# 查看后端日志SAE 控制台 → 应用详情 → 日志)
# 应该看到:
# [INFO] Calling extraction service: http://172.17.x.x:8000/extract/pdf
# [INFO] Extraction completed in 3.2s

步骤 6测试 Dify 服务调用

# 测试 PKB 模块的知识库对话
curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/knowledge-bases/kb-xxx/chat \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "这篇文献的研究方法是什么?",
    "conversationId": "conv-xxx"
  }'

# 查看后端日志
# 应该看到:
# [INFO] Calling Dify API: http://172.17.x.x:80/v1/chat-messages
# [INFO] Dify response received in 2.5s

步骤 7测试 OSS 文件上传

# 上传大文件(测试 OSS
curl -X POST https://backend-service-xxxxx.cn-hangzhou.sae.aliyuncs.com/api/asl/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@large-paper.pdf"

# 查看日志,应该看到 OSS 上传成功:
# [INFO] File uploaded to OSS: clinical-research-files/asl/xxx.pdf

11. 监控与维护

📊 SAE 自带监控

1. 实时监控

SAE 控制台应用详情监控

关键指标

指标 健康阈值 告警阈值 说明
CPU 使用率 < 60% > 80% LLM 调用是 CPU 密集型
内存使用率 < 70% > 85% 监控内存泄漏
请求 QPS - - 了解负载
平均响应时间 < 500ms > 2000ms AI 对话除外(可能 10s+
错误率 < 0.5% > 2% 监控服务稳定性
实例数量 2+ - 确保高可用

2. 日志查看

SAE 控制台应用详情日志实时日志

关键日志示例

# ✅ 正常启动
[Config] Environment validation passed
[Database] ✅ 数据库连接成功!
[Fastify] Server listening on http://0.0.0.0:3001

# ✅ 正常请求
[INFO] POST /api/chat 200 2345ms

# ⚠️ 警告日志(需关注)
[WARN] Database connection pool near limit: 16/18 connections

# ❌ 错误日志(需立即处理)
[ERROR] Failed to connect to Python service: ECONNREFUSED
[ERROR] Prisma timeout: Database connection pool exhausted
[ERROR] Dify API error: 502 Bad Gateway

3. 数据库连接监控

# 在 SAE Webshell 中执行(或使用 RDS 控制台)
psql $DATABASE_URL -c "
SELECT 
  count(*) as total_connections,
  count(*) FILTER (WHERE state = 'active') as active_connections,
  count(*) FILTER (WHERE state = 'idle') as idle_connections
FROM pg_stat_activity 
WHERE datname = 'ai_clinical';
"

# 输出示例:
# total_connections | active_connections | idle_connections
# ------------------+--------------------+-----------------
#                35 |                  5 |               30

# 告警条件total_connections > (MAX_INSTANCES * connection_limit * 0.8)
# 示例20 实例 * 18 连接/实例 * 0.8 = 288 连接

🔧 日常维护任务

每日检查

# 1. 检查应用健康状态
# SAE 控制台 → 应用列表 → 查看运行状态(绿色为正常)

# 2. 查看错误日志
# SAE 控制台 → 应用详情 → 日志 → 筛选 ERROR 级别

# 3. 检查数据库连接数
# RDS 控制台 → 实例监控 → 连接数(< 80% 为健康)

每周任务

# 1. 查看性能指标趋势
# SAE 控制台 → 应用详情 → 监控 → 选择"最近 7 天"
# 关注:
# - CPU/内存是否有持续上涨(内存泄漏?)
# - 响应时间是否变慢
# - 错误率是否增加

# 2. 清理日志(如果日志存储空间紧张)
# SAE 控制台 → 应用详情 → 日志配置 → 清理旧日志

每月任务

# 1. 更新依赖(安全补丁)
# 在本地开发环境执行:
npm outdated
npm update
npm audit fix

# 重新测试后部署

# 2. 重建镜像(包含系统安全更新)
docker build -t backend-service:v1.0.1 .
docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:v1.0.1

# 3. 在 SAE 中灰度更新
# SAE 控制台 → 应用详情 → 部署
# 选择新镜像版本v1.0.1
# 灰度发布:先更新 1 个实例,观察 10 分钟后全量发布

# 4. 数据库备份RDS 自动备份,仅需验证)
# RDS 控制台 → 备份恢复 → 查看最近备份时间

🚨 告警配置

云监控应用监控创建告警规则

推荐告警规则

告警项 阈值 通知方式
CPU 使用率 > 80% 持续 5 分钟 告警 钉钉/邮件
内存使用率 > 85% 持续 5 分钟 告警 钉钉/邮件
错误率 > 2% 持续 3 分钟 紧急 短信+钉钉
实例健康检查失败 > 3 次 紧急 短信+钉钉
RDS 连接数 > 80% 警告 钉钉/邮件

12. 故障排查

问题 1应用启动失败

症状

SAE 控制台显示:实例启动中 → 健康检查失败 → 实例停止

排查步骤

# 1. 查看启动日志
# SAE 控制台 → 应用详情 → 日志 → 筛选最近 5 分钟

# 2. 常见错误原因:

# 错误 A环境变量验证失败
# 日志:❌ [Config] Environment validation failed: DATABASE_URL is required
# 解决:检查 SAE 环境变量配置,补充缺失的变量

# 错误 B数据库连接失败
# 日志:❌ 数据库连接失败: getaddrinfo ENOTFOUND pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com
# 解决:
#   - 检查 DATABASE_URL 是否正确
#   - 检查 RDS 白名单是否允许 SAE VPC 访问
#   - 检查 RDS 内网地址是否可达

# 错误 CPrisma 迁移未执行
# 日志Error: P1001: Can't reach database server at `pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com`
# 解决:先执行数据库迁移(参见第 8 节)

# 错误 D端口冲突
# 日志Error: listen EADDRINUSE: address already in use :::3001
# 解决:检查 PORT 环境变量是否为 3001与 Dockerfile EXPOSE 一致)

问题 2数据库连接池耗尽

症状

[ERROR] Prisma timeout: Database connection pool exhausted
[ERROR] P2024: Timed out fetching a new connection from the pool

根本原因

  • SAE 实例数 * 每实例连接数 > RDS 最大连接数
  • 连接未正确释放(代码 Bug

排查步骤

# 1. 检查当前连接数
psql $DATABASE_URL -c "
SELECT count(*) as current_connections 
FROM pg_stat_activity 
WHERE datname = 'ai_clinical';
"

# 2. 检查 SAE 实例数
# SAE 控制台 → 应用详情 → 实例列表
# 假设有 10 个实例

# 3. 计算连接数
# 每实例连接数 = 18connection_limit
# 总连接数 = 10 * 18 = 180
# 如果实际连接数远超 180说明连接未释放

# 4. 查找连接泄漏
psql $DATABASE_URL -c "
SELECT pid, state, wait_event, query_start, state_change, query
FROM pg_stat_activity
WHERE datname = 'ai_clinical'
  AND state_change < now() - interval '5 minutes'
ORDER BY state_change;
"

# 5. 紧急处理:杀死长时间空闲连接
psql $DATABASE_URL -c "
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'ai_clinical'
  AND state = 'idle'
  AND state_change < now() - interval '10 minutes';
"

解决方法

# 方法 1调整连接池配置临时
# 在 DATABASE_URL 中降低 connection_limit
DATABASE_URL=postgresql://...?connection_limit=10&pool_timeout=10

# 方法 2限制 SAE 实例数(临时)
# SAE 控制台 → 应用配置 → 弹性伸缩
# 将最大实例数从 20 降低到 10

# 方法 3升级 RDS 规格(长期)
# RDS 控制台 → 变更配置 → 升级到更大规格(增加最大连接数)

# 方法 4修复代码如果是连接泄漏
# 检查代码中是否有未释放的连接:
# - 未使用 Prisma 的事务 API
# - 手动创建的数据库连接未关闭
# - 长时间运行的查询

问题 3无法连接 Python 微服务

症状

[ERROR] Failed to connect to Python service: ECONNREFUSED
或
[ERROR] connect ETIMEDOUT 172.17.x.x:8000

排查步骤

# 1. 确认 Python 服务是否运行
# SAE 控制台 → 应用列表 → 查看 extraction-service 状态

# 2. 确认内网地址是否正确
# SAE 控制台 → extraction-service 应用 → 应用访问配置
# 复制"VPC 内网访问地址",更新后端环境变量

# 3. 测试内网连通性
# 在后端应用的 Webshell 中执行:
curl -v http://172.17.x.x:8000/health

# 4. 检查安全组规则
# SAE 控制台 → extraction-service 应用 → 网络配置
# 确认入站规则允许 VPC 内访问 8000 端口

解决方法

# 如果内网地址错误,更新环境变量:
# SAE 控制台 → backend-service 应用 → 应用配置 → 环境变量
# 修改或添加:
EXTRACTION_SERVICE_URL=http://<正确的内网IP>:8000

# 重启应用使环境变量生效
# SAE 控制台 → backend-service 应用 → 重启

问题 4无法连接 Dify 服务

症状

[ERROR] Dify API error: ECONNREFUSED
或
[ERROR] Dify API error: 502 Bad Gateway

排查步骤

# 1. 确认 Dify 服务是否运行
# ECS 控制台 → 实例列表 → 找到 Dify 实例 → 远程连接

# 在 ECS 中执行:
docker ps | grep dify
# 应该看到 dify-api, dify-worker, dify-web, redis, weaviate 等容器运行中

# 2. 测试 Dify API
curl http://localhost/v1/info

# 3. 从 SAE 测试连通性
# 在后端应用的 Webshell 中执行:
curl -v http://172.17.x.x:80/v1/info

解决方法

# 如果 Dify 未启动,在 ECS 中重启:
cd /path/to/dify
docker-compose up -d

# 如果 API Key 错误,更新环境变量:
# SAE 控制台 → backend-service 应用 → 环境变量
DIFY_API_KEY=app-<正确的Key>

# 如果内网地址错误,更新环境变量:
DIFY_API_URL=http://<ECS内网IP>:80/v1

问题 5OSS 上传失败

症状

[ERROR] OSS upload failed: AccessDenied
或
[ERROR] OSS upload failed: InvalidAccessKeyId

排查步骤

# 1. 检查环境变量
# SAE 控制台 → 应用详情 → 环境变量
# 确认以下变量正确:
OSS_REGION=oss-cn-beijing
OSS_BUCKET=clinical-research-files
OSS_ACCESS_KEY_ID=LTAI5t...
OSS_ACCESS_KEY_SECRET=xxx...

# 2. 测试 OSS 访问
# 在后端应用的 Webshell 中执行:
node -e "
const OSS = require('ali-oss');
const client = new OSS({
  region: process.env.OSS_REGION,
  accessKeyId: process.env.OSS_ACCESS_KEY_ID,
  accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
  bucket: process.env.OSS_BUCKET
});
client.list().then(result => console.log('✅ OSS connection successful')).catch(err => console.error('❌ OSS error:', err.message));
"

解决方法

# 如果 AccessKey 错误,重新生成:
# 阿里云控制台 → AccessKey 管理 → 创建 AccessKey
# 更新 SAE 环境变量

# 如果 Bucket 权限错误:
# OSS 控制台 → Bucket 列表 → clinical-research-files → 访问控制
# 确认 Bucket 为"私有",且 RAM 用户有读写权限

问题 6内存泄漏

症状

SAE 监控显示:内存使用率持续上涨,最终导致 OOMOut of Memory
日志JavaScript heap out of memory

排查步骤

# 1. 查看内存趋势
# SAE 控制台 → 应用详情 → 监控 → 选择"最近 24 小时"
# 如果内存曲线持续上涨,不下降,说明有内存泄漏

# 2. 查看实例内存详情
# 在 Webshell 中执行:
node -e "console.log(process.memoryUsage())"

# 输出示例:
# {
#   rss: 123456789,        // 总内存(字节)
#   heapTotal: 45678910,   // V8 堆总大小
#   heapUsed: 34567890,    // V8 堆已使用
#   external: 1234567,     // 外部内存Buffer等
#   arrayBuffers: 123456   // ArrayBuffer
# }

# 3. 启用内存快照(本地调试)
# 在本地开发环境添加 --inspect 参数:
node --inspect dist/index.js
# 使用 Chrome DevTools 查看内存快照

常见内存泄漏原因

  1. 全局变量累积(如缓存无限增长)
  2. 事件监听器未移除
  3. 定时器未清理
  4. 闭包持有大对象
  5. Prisma 查询结果未释放

解决方法

// 错误示例:全局缓存无限增长
const cache = {}; // ❌ 永不清理

// 正确示例:使用 LRU 缓存
import LRUCache from 'lru-cache';
const cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 10 }); // ✅ 最多 1000 项10 分钟过期

// 错误示例:事件监听器泄漏
emitter.on('event', handler); // ❌ 从不移除

// 正确示例:及时移除监听器
emitter.once('event', handler); // ✅ 自动移除
// 或
emitter.on('event', handler);
// 使用后:
emitter.off('event', handler); // ✅ 手动移除

// 错误示例:定时器泄漏
setInterval(() => { ... }, 1000); // ❌ 永不清理

// 正确示例:清理定时器
const timer = setInterval(() => { ... }, 1000);
process.on('SIGTERM', () => clearInterval(timer)); // ✅ 优雅关闭时清理

问题 7Prisma Schema 与数据库不一致

症状

// 代码中访问字段:
const user = await prisma.user.findUnique({ where: { id: '123' } });
console.log(user.phone); // ❌ TypeScript 报错Property 'phone' does not exist

// 或运行时报错:
PrismaClientKnownRequestError: column "phone" does not exist

原因

  • 开发过程中直接修改了数据库(加了 phone 字段)
  • 但没有更新 schema.prisma
  • Prisma Client 基于旧的 Schema 生成,不知道新字段

解决方法

# 1. 在本地开发环境,连接到 RDS
export DATABASE_URL="postgresql://username:password@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical"

# 2. 反向同步 Schema
npx prisma db pull

# 3. 重新生成 Client
npx prisma generate

# 4. 提交代码
git add prisma/schema.prisma
git commit -m "fix: sync Prisma schema with database"
git push

# 5. 重新构建镜像并部署
cp -r prisma backend/prisma
cd backend
docker build -t backend-service:v1.0.1 .
docker push registry.cn-hangzhou.aliyuncs.com/clinical-research/backend-service:v1.0.1

# 6. 在 SAE 中更新镜像版本
# SAE 控制台 → 应用详情 → 部署 → 选择新版本 v1.0.1

13. 注意事项与禁忌

最佳实践

1. 环境变量管理

# ✅ 正确做法:使用 SAE 环境变量
# SAE 控制台 → 应用配置 → 环境变量

# ❌ 错误做法:在代码或 Dockerfile 中硬编码
# Dockerfile
ENV DATABASE_URL="postgresql://user:pass@host/db"  # ❌ 泄露敏感信息

2. 连接池配置

# ✅ 正确做法:根据 RDS 和 SAE 配置动态计算
DATABASE_URL=postgresql://...?connection_limit=18&pool_timeout=10

# 计算公式:
# connection_limit = (RDS_MAX_CONNECTIONS / MAX_INSTANCES) - 预留
# 示例:(400 / 20) - 2 = 18

# ❌ 错误做法:使用默认值
DATABASE_URL=postgresql://...  # ❌ 默认无限制,导致连接耗尽

3. 文件存储

// ✅ 正确做法:使用 OSS 存储
import { StorageFactory } from './common/storage';
const storage = StorageFactory.create(); // 根据 STORAGE_TYPE 自动选择
await storage.upload('uploads/file.pdf', buffer);

// ❌ 错误做法:存储到容器文件系统
import fs from 'fs';
fs.writeFileSync('/app/uploads/file.pdf', buffer); // ❌ 容器重启后丢失

4. 日志输出

// ✅ 正确做法:使用 stdoutSAE 自动收集)
console.log('[INFO] User logged in:', userId);
logger.info('User logged in', { userId });

// ❌ 错误做法:写入文件
import fs from 'fs';
fs.appendFileSync('/var/log/app.log', message); // ❌ 容器重启后丢失

5. 优雅关闭

// ✅ 正确做法:监听 SIGTERM 信号
process.on('SIGTERM', async () => {
  console.log('[Server] Received SIGTERM, shutting down gracefully...');
  await fastify.close(); // 关闭 HTTP 服务器
  await prisma.$disconnect(); // 关闭数据库连接
  process.exit(0);
});

// ❌ 错误做法:直接退出
process.on('SIGTERM', () => process.exit(0)); // ❌ 连接未释放

绝对禁止

1. 禁止直接修改数据库而不同步 Prisma Schema致命

# ❌ 错误做法:
# 1. 在 Navicat 中直接添加字段
ALTER TABLE "User" ADD COLUMN phone VARCHAR(50);

# 2. 然后直接在代码中使用
const user = await prisma.user.create({
  data: {
    email: 'test@example.com',
    phone: '13800138000'  // ❌ TypeScript 报错phone 不存在
  }
});

# ✅ 正确做法:
# 1. 先修改 schema.prisma
model User {
  email String
  phone String?  @db.VarChar(50)  // 添加字段定义
}

# 2. 生成迁移(仅开发环境)
npx prisma migrate dev --name add_user_phone

# 3. 生成客户端
npx prisma generate

# 4. 在代码中使用
const user = await prisma.user.create({
  data: {
    email: 'test@example.com',
    phone: '13800138000'  // ✅ TypeScript 识别
  }
});

# 或者,如果数据库已手动修改:
# 1. 反向同步
npx prisma db pull

# 2. 生成客户端
npx prisma generate

原因

  • Prisma Client 基于 Schema 生成
  • Schema 与数据库不一致会导致运行时错误或类型不匹配
  • 这是导致生产环境崩溃的最常见原因之一

2. 禁止在启动命令中无脑执行 prisma migrate deploy

# ❌ 错误做法(如果数据库是通过 pg_dump 导入的):
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/index.js"]

# 错误原因:
# - 表结构已存在(通过 pg_dump 导入)
# - migrate deploy 会尝试重新创建表
# - 导致错误Table already exists

# ✅ 正确做法(针对您的情况):
CMD ["node", "dist/index.js"]

# 解释:
# - 数据库结构已通过 pg_dump 导入
# - Schema 已通过 prisma db pull 同步
# - 无需在启动时执行迁移

3. 禁止忽略 Prisma 反向同步步骤

# ❌ 错误流程:
pg_dump → 导入 RDS → 直接构建镜像 → 部署
# ❌ 问题Schema 与数据库不一致

# ✅ 正确流程:
pg_dump → 导入 RDS → prisma db pull同步→ 构建镜像 → 部署
# ✅ 关键prisma db pull 确保一致性

4. 禁止在代码中硬编码敏感信息

// ❌ 错误示例
const dbUrl = 'postgresql://admin:P@ssw0rd@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical';

// ✅ 正确做法
const dbUrl = process.env.DATABASE_URL;

5. 禁止在 backend/ 目录构建前不复制 Prisma 文件

# ❌ 错误做法:
cd backend
docker build -t backend-service:v1.0.0 .
# ❌ 错误COPY prisma ./prisma 会失败(找不到文件)

# ✅ 正确做法:
cd AIclinicalresearch  # 项目根目录
cp -r prisma backend/prisma  # 先复制
cd backend
docker build -t backend-service:v1.0.0 .  # 再构建

6. 禁止使用内存作为缓存(多实例环境)

// ❌ 错误示例:内存缓存不共享
const cache = new Map(); // 实例 1 的缓存,实例 2 看不到

// ✅ 正确做法:使用 PostgreSQL 缓存Postgres-Only
import { CacheFactory } from './common/cache';
const cache = CacheFactory.create(); // 根据 CACHE_TYPE 选择

7. 禁止使用本地文件作为队列

// ❌ 错误示例:文件队列不共享
import fs from 'fs';
fs.writeFileSync('/tmp/queue.json', JSON.stringify(tasks)); // 实例间不同步

// ✅ 正确做法:使用 pg-bossPostgres-Only
import { jobQueue } from './common/jobs';
await jobQueue.send('pdf-extraction', { fileId: '123' });

8. 禁止使用同步阻塞操作

// ❌ 错误示例:阻塞事件循环
import fs from 'fs';
const data = fs.readFileSync('/large-file.pdf'); // 阻塞其他请求

// ✅ 正确做法:使用异步 API
const data = await fs.promises.readFile('/large-file.pdf');

9. 禁止跳过健康检查

// ❌ 错误示例:健康检查总是返回 200
app.get('/health', async (req, reply) => {
  return { status: 'ok' }; // 即使数据库断开也返回 ok
});

// ✅ 正确做法:真正检查依赖服务
app.get('/health', async (req, reply) => {
  const dbHealthy = await testDatabaseConnection();
  if (!dbHealthy) {
    reply.code(503);
    return { status: 'unhealthy', database: 'disconnected' };
  }
  return { status: 'healthy' };
});

10. 禁止忽略错误处理

// ❌ 错误示例:吞掉错误
try {
  await riskyOperation();
} catch (error) {
  // 什么都不做
}

// ✅ 正确做法:记录错误并返回适当的响应
try {
  await riskyOperation();
} catch (error) {
  logger.error('Risky operation failed', { error, stack: error.stack });
  reply.code(500).send({ error: 'Internal server error' });
}

11. 禁止在生产环境使用 development 模式

# ❌ 错误配置
NODE_ENV=development  # 会打印大量调试日志,暴露敏感信息

# ✅ 正确配置
NODE_ENV=production
LOG_LEVEL=info

12. 禁止使用弱 JWT 密钥

# ❌ 错误配置
JWT_SECRET=secret  # 太弱,容易被破解

# ✅ 正确配置(至少 32 位随机字符串)
JWT_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6  # 使用 openssl rand -base64 32 生成

13. 禁止直接暴露 Prisma 错误到前端

// ❌ 错误示例:泄露数据库结构
try {
  await prisma.user.create({ data: { email: 'test@example.com' } });
} catch (error) {
  reply.code(500).send({ error: error.message }); // 可能暴露表名、字段名
}

// ✅ 正确做法:返回通用错误
try {
  await prisma.user.create({ data: { email: 'test@example.com' } });
} catch (error) {
  logger.error('Failed to create user', { error });
  reply.code(500).send({ error: 'Failed to create user' });
}

14. 禁止忽略 Docker 镜像优化

# ❌ 错误示例:单阶段构建,镜像臃肿
FROM node:22
COPY . .
RUN npm install  # 包含 devDependencies
CMD ["node", "dist/index.js"]

# ✅ 正确做法:多阶段构建,减小镜像体积
FROM node:22-alpine AS builder
# ... 构建阶段 ...
FROM node:22-alpine
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules  # 仅生产依赖

📚 相关文档


🆘 获取帮助

遇到问题?

  1. 查看本文档的"故障排查"章节
  2. 查看 SAE 控制台的实时日志
  3. 查看 RDS 控制台的性能监控
  4. 联系团队技术支持

部署愉快!🚀