Features - User Management (Phase 4.1): - Database: Add user_modules table for fine-grained module permissions - Database: Add 4 user permissions (view/create/edit/delete) to role_permissions - Backend: UserService (780 lines) - CRUD with tenant isolation - Backend: UserController + UserRoutes (648 lines) - 13 API endpoints - Backend: Batch import users from Excel - Frontend: UserListPage (412 lines) - list/filter/search/pagination - Frontend: UserFormPage (341 lines) - create/edit with module config - Frontend: UserDetailPage (393 lines) - details/tenant/module management - Frontend: 3 modal components (592 lines) - import/assign/configure - API: GET/POST/PUT/DELETE /api/admin/users/* endpoints Architecture Upgrade - Module Permission System: - Backend: Add getUserModules() method in auth.service - Backend: Login API returns modules array in user object - Frontend: AuthContext adds hasModule() method - Frontend: Navigation filters modules based on user.modules - Frontend: RouteGuard checks requiredModule instead of requiredVersion - Frontend: Remove deprecated version-based permission system - UX: Only show accessible modules in navigation (clean UI) - UX: Smart redirect after login (avoid 403 for regular users) Fixes: - Fix UTF-8 encoding corruption in ~100 docs files - Fix pageSize type conversion in userService (String to Number) - Fix authUser undefined error in TopNavigation - Fix login redirect logic with role-based access check - Update Git commit guidelines v1.2 with UTF-8 safety rules Database Changes: - CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled) - ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code) - INSERT 4 permissions + role assignments - UPDATE PUBLIC tenant with 8 module subscriptions Technical: - Backend: 5 new files (~2400 lines) - Frontend: 10 new files (~2500 lines) - Docs: 1 development record + 2 status updates + 1 guideline update - Total: ~4900 lines of code Status: User management 100% complete, module permission system operational
56 KiB
Node.js 后端 - SAE 容器部署完全指南
文档版本: v1.1 (修复 Prisma 构建和数据库同步问题)
创建时间: 2025-12-13
最后修订: 2025-12-13
适用范围: AIclinicalresearch 平台 - Node.js 后端服务
目标读者: 运维工程师、后端开发工程师
部署目标: 阿里云 SAE(Serverless 应用引擎)容器部署
v1.1 更新日志:
- 🛑 修复:Prisma 目录构建上下文问题(增加预处理步骤)
- 🛑 修复:生产环境缺少 Prisma CLI 导致迁移失败
- 🔥 新增:针对开发历史不规范的"反向同步"流程(关键)
- ✅ 优化:数据库迁移策略(pg_dump 导入后不执行 migrate)
- ✅ 简化:移除 Init Container 方案,改用启动命令
📋 目录
- 为什么选择 SAE 容器部署
- 部署前准备
- 后端服务分析
- 🔥 Prisma 反向同步(必读)
- 构建 Docker 镜像
- 本地测试验证
- 推送到 ACR
- SAE 应用配置
- 数据库部署策略
- 端到端测试
- 监控与维护
- 故障排查
- 注意事项与禁忌
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 微服务(SAE):
http://172.17.x.x:8000 - Dify 服务(ECS):
http://172.17.x.x:80
- Python 微服务(SAE):
敏感信息准备
准备以下配置信息(稍后配置到 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")
⚠️ 人工检查清单:
-
检查新增字段:
- 是否有意外的字段(如测试字段)?
- 字段类型是否正确?
-
检查删除字段:
- 如果 Schema 里的字段在数据库中不存在,
db pull会删除它 - 确认这些字段是否真的应该删除
- 如果 Schema 里的字段在数据库中不存在,
-
检查关系(Relations):
- 外键关系是否正确识别?
@relation注解是否合理?
-
检查索引:
- 是否保留了所有
@@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
🚨 常见问题处理
问题 1:db 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
问题 2:db pull 后,某些字段的类型变了
// 之前:
model User {
age Int
}
// db pull 后:
model User {
age String @db.VarChar(10) // ❌ 类型变成了 String
}
原因:数据库中的字段类型确实是 VARCHAR(10),而不是 INT。
解决方法:
- 修正数据库(推荐):
ALTER TABLE "User" ALTER COLUMN age TYPE INTEGER USING age::INTEGER; - 接受现状(不推荐):如果数据库确实要用
VARCHAR存储年龄,那就改代码逻辑。
问题 3:db 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-500MB(Alpine 基础镜像 + Node.js + 依赖)
如果构建失败:
# 常见问题 1:Prisma 文件找不到
# 错误信息: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
# 常见问题 3:Prisma 生成失败
# 解决:检查 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.0和latest - 镜像大小:~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
# CloseAI(OpenAI/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:
- 登录 ECS 控制台
- 找到 Dify 所在的 ECS 实例
- 查看"私有 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 将:
- 从 ACR 拉取镜像(~2 分钟)
- 启动容器实例(~1 分钟)
- 执行健康检查(~1 分钟)
- 流量切换(~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(阿里云数据管理)
- 登录阿里云控制台 → 数据管理 DMS
- 连接到 RDS 实例
- 数据方案 → SQL 窗口 → 粘贴 SQL 文件内容
- 执行
步骤 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 内网地址是否可达
# 错误 C:Prisma 迁移未执行
# 日志: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. 计算连接数
# 每实例连接数 = 18(connection_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
问题 5:OSS 上传失败
症状:
[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 监控显示:内存使用率持续上涨,最终导致 OOM(Out 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 查看内存快照
常见内存泄漏原因:
- 全局变量累积(如缓存无限增长)
- 事件监听器未移除
- 定时器未清理
- 闭包持有大对象
- 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)); // ✅ 优雅关闭时清理
问题 7:Prisma 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. 日志输出
// ✅ 正确做法:使用 stdout(SAE 自动收集)
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-boss(Postgres-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 # 仅生产依赖
📚 相关文档
🆘 获取帮助
遇到问题?
- 查看本文档的"故障排查"章节
- 查看 SAE 控制台的实时日志
- 查看 RDS 控制台的性能监控
- 联系团队技术支持
部署愉快!🚀