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
2182 lines
56 KiB
Markdown
2182 lines
56 KiB
Markdown
# 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 方案,改用启动命令
|
||
|
||
---
|
||
|
||
## 📋 目录
|
||
|
||
1. [为什么选择 SAE 容器部署](#1-为什么选择-sae-容器部署)
|
||
2. [部署前准备](#2-部署前准备)
|
||
3. [后端服务分析](#3-后端服务分析)
|
||
4. [🔥 Prisma 反向同步(必读)](#4-prisma-反向同步必读)
|
||
5. [构建 Docker 镜像](#5-构建-docker-镜像)
|
||
6. [本地测试验证](#6-本地测试验证)
|
||
7. [推送到 ACR](#7-推送到-acr)
|
||
8. [SAE 应用配置](#8-sae-应用配置)
|
||
9. [数据库部署策略](#9-数据库部署策略)
|
||
10. [端到端测试](#10-端到端测试)
|
||
11. [监控与维护](#11-监控与维护)
|
||
12. [故障排查](#12-故障排查)
|
||
13. [注意事项与禁忌](#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`)
|
||
|
||
验证命令:
|
||
|
||
```bash
|
||
# 检查 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`
|
||
|
||
#### 敏感信息准备
|
||
|
||
准备以下配置信息(稍后配置到 SAE 环境变量):
|
||
|
||
```bash
|
||
# 数据库
|
||
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 |
|
||
|
||
### 📝 启动流程
|
||
|
||
```bash
|
||
# 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` 文件**
|
||
|
||
**后果**:
|
||
```typescript
|
||
// 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:
|
||
|
||
```bash
|
||
# 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:执行反向同步(关键步骤)
|
||
|
||
```bash
|
||
# 让 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:查看并确认变更
|
||
|
||
```bash
|
||
# 查看 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
|
||
|
||
```bash
|
||
# 基于新的 Schema 生成客户端
|
||
npx prisma generate
|
||
|
||
# 输出示例:
|
||
# ✔ Generated Prisma Client (5.7.0) to ./node_modules/@prisma/client in 234ms
|
||
```
|
||
|
||
**验证生成结果**:
|
||
|
||
```typescript
|
||
// 在代码中测试(临时)
|
||
import { PrismaClient } from '@prisma/client';
|
||
const prisma = new PrismaClient();
|
||
|
||
// TypeScript 应该能识别新字段
|
||
const user = await prisma.user.findUnique({ where: { id: '123' } });
|
||
console.log(user.phone); // ✅ 现在可以访问了
|
||
```
|
||
|
||
#### 步骤 5:提交代码
|
||
|
||
```bash
|
||
# 恢复本地 .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` 报错:无法连接数据库
|
||
|
||
```bash
|
||
# 错误信息:
|
||
Error: P1001: Can't reach database server at `pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432`
|
||
```
|
||
|
||
**解决方法**:
|
||
|
||
```bash
|
||
# 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` 后,某些字段的类型变了
|
||
|
||
```prisma
|
||
// 之前:
|
||
model User {
|
||
age Int
|
||
}
|
||
|
||
// db pull 后:
|
||
model User {
|
||
age String @db.VarChar(10) // ❌ 类型变成了 String
|
||
}
|
||
```
|
||
|
||
**原因**:数据库中的字段类型确实是 `VARCHAR(10)`,而不是 `INT`。
|
||
|
||
**解决方法**:
|
||
1. **修正数据库**(推荐):
|
||
```sql
|
||
ALTER TABLE "User" ALTER COLUMN age TYPE INTEGER USING age::INTEGER;
|
||
```
|
||
2. **接受现状**(不推荐):如果数据库确实要用 `VARCHAR` 存储年龄,那就改代码逻辑。
|
||
|
||
#### 问题 3:`db pull` 后,丢失了自定义注释
|
||
|
||
```prisma
|
||
// 之前:
|
||
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 目录(推荐)**
|
||
|
||
```bash
|
||
# 在项目根目录执行
|
||
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 自动化构建,可以在根目录构建:
|
||
|
||
```bash
|
||
# 在根目录构建,需要修改 Dockerfile 的路径
|
||
# 这里先不展开,推荐使用方案 A
|
||
```
|
||
|
||
### 📝 创建 Dockerfile
|
||
|
||
在 `backend/` 目录下创建 `Dockerfile`:
|
||
|
||
```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
|
||
|
||
```dockerfile
|
||
# 🔥 在阶段 2 新增:
|
||
RUN npm install -g prisma@6.17.0
|
||
```
|
||
|
||
**原因**:
|
||
- `npm ci` + `npm prune --production` 会删除 devDependencies 中的 `prisma` 包
|
||
- 但生产环境可能需要执行 `npx prisma db pull` 或排查问题
|
||
- 全局安装确保 `prisma` 命令始终可用
|
||
|
||
**代价**:
|
||
- 镜像体积增加约 50MB
|
||
- 但这是值得的(避免"命令找不到"的问题)
|
||
|
||
#### 修改 2:不执行数据库迁移
|
||
|
||
```dockerfile
|
||
# ❌ 错误做法(会导致"表已存在"错误):
|
||
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(可选优化)
|
||
|
||
```dockerfile
|
||
# 移除:
|
||
# 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 文件(必须执行)
|
||
|
||
```bash
|
||
# 1. 回到项目根目录
|
||
cd AIclinicalresearch
|
||
|
||
# 2. 复制 prisma 到 backend 目录(确保构建时能找到)
|
||
cp -r prisma backend/prisma
|
||
|
||
# 3. 验证复制成功
|
||
ls backend/prisma/schema.prisma
|
||
# 应该输出:backend/prisma/schema.prisma
|
||
```
|
||
|
||
### 步骤 1:构建镜像
|
||
|
||
```bash
|
||
# 在 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 + 依赖)
|
||
```
|
||
|
||
**如果构建失败**:
|
||
|
||
```bash
|
||
# 常见问题 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:本地运行测试
|
||
|
||
```bash
|
||
# 创建测试环境变量文件
|
||
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:测试健康检查
|
||
|
||
```bash
|
||
# 测试健康检查端点
|
||
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 端点
|
||
|
||
```bash
|
||
# 测试用户注册(示例)
|
||
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:清理测试容器
|
||
|
||
```bash
|
||
# 停止并删除测试容器
|
||
docker stop backend-test
|
||
docker rm backend-test
|
||
|
||
# 删除测试环境变量文件
|
||
rm .env.docker.test
|
||
```
|
||
|
||
### 步骤 5:清理临时文件
|
||
|
||
```bash
|
||
# 构建成功后,清理 backend/prisma(避免误提交到 Git)
|
||
cd backend
|
||
rm -rf prisma
|
||
|
||
# 验证清理成功
|
||
ls prisma 2>/dev/null || echo "✅ prisma 目录已清理"
|
||
|
||
# 注意:根目录的 prisma/ 文件夹保留,这是源文件
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 推送到 ACR
|
||
|
||
### 步骤 1:登录 ACR
|
||
|
||
```bash
|
||
# 获取 ACR 登录地址(阿里云控制台 → 容器镜像服务 → 访问凭证)
|
||
# 示例:registry.cn-hangzhou.aliyuncs.com
|
||
|
||
# 登录(使用 ACR 密码,不是阿里云账号密码)
|
||
docker login --username=your-aliyun-account registry.cn-hangzhou.aliyuncs.com
|
||
|
||
# 输入密码后看到:
|
||
# Login Succeeded
|
||
```
|
||
|
||
### 步骤 2:标记镜像
|
||
|
||
```bash
|
||
# 格式: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:推送镜像
|
||
|
||
```bash
|
||
# 推送指定版本
|
||
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:配置环境变量(关键步骤)
|
||
|
||
**⚠️ 重要:请仔细配置以下环境变量**
|
||
|
||
#### 基础配置
|
||
|
||
```bash
|
||
NODE_ENV=production
|
||
PORT=3001
|
||
HOST=0.0.0.0
|
||
LOG_LEVEL=info
|
||
SERVICE_NAME=backend-service
|
||
```
|
||
|
||
#### 数据库配置(必需)
|
||
|
||
```bash
|
||
# ⚠️ 使用 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 个连接给管理任务和其他服务
|
||
|
||
#### 存储配置(必需)
|
||
|
||
```bash
|
||
# 使用阿里云 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)
|
||
|
||
```bash
|
||
# 使用 PostgreSQL 作为缓存(不需要 Redis)
|
||
CACHE_TYPE=postgres
|
||
|
||
# 使用 pg-boss 作为队列(不需要 Redis)
|
||
QUEUE_TYPE=pgboss
|
||
```
|
||
|
||
#### LLM API 配置(至少配置一个)
|
||
|
||
```bash
|
||
# 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 配置(必需)
|
||
|
||
```bash
|
||
# ⚠️ 使用 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`)
|
||
|
||
#### 安全配置(必需)
|
||
|
||
```bash
|
||
# ⚠️ 生产环境必须修改为强密码(至少 32 位随机字符串)
|
||
JWT_SECRET=your-strong-random-secret-min-32-chars-change-in-production
|
||
JWT_EXPIRES_IN=7d
|
||
```
|
||
|
||
**生成强密码**:
|
||
|
||
```bash
|
||
# Linux/Mac
|
||
openssl rand -base64 32
|
||
|
||
# Windows PowerShell
|
||
-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32 | ForEach-Object {[char]$_})
|
||
```
|
||
|
||
#### CORS 配置(可选)
|
||
|
||
```bash
|
||
# 如果前端使用自定义域名,配置允许的源
|
||
# 注意:如果前端使用 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. 数据库部署策略
|
||
|
||
### 🎯 您的实际情况(非常重要)
|
||
|
||
根据您的开发历史,数据库部署策略与标准流程**完全不同**:
|
||
|
||
**标准流程(不适合您)**:
|
||
```mermaid
|
||
graph LR
|
||
A[代码] --> B[Prisma Migrations]
|
||
B --> C[空数据库]
|
||
C --> D[自动创建表结构]
|
||
```
|
||
|
||
**您的实际流程**:
|
||
```mermaid
|
||
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:导出本地数据库(如果还没做)
|
||
|
||
```bash
|
||
# 在本地开发环境执行
|
||
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 命令行(推荐)**
|
||
|
||
```bash
|
||
# 连接到 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:验证导入成功
|
||
|
||
```bash
|
||
# 连接到 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 反向同步
|
||
|
||
**⚠️ 这一步非常关键!**
|
||
|
||
```bash
|
||
# 确保第 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` 导入的,请跳过这一节!**
|
||
|
||
<details>
|
||
<summary>点击展开:标准迁移流程(仅供参考)</summary>
|
||
|
||
#### 适用场景
|
||
|
||
- 全新项目,RDS 数据库是空的
|
||
- 从未手动修改过数据库结构
|
||
- 所有表结构都通过 Prisma Migrations 管理
|
||
|
||
#### 执行步骤
|
||
|
||
```bash
|
||
# 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;"
|
||
```
|
||
|
||
</details>
|
||
|
||
### 🚨 常见错误与修正
|
||
|
||
#### 错误 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 会冲突。
|
||
|
||
**解决方法**:
|
||
|
||
```bash
|
||
# 方法 1:移除启动命令中的 migrate(推荐)
|
||
# SAE 控制台 → 应用配置 → 启动命令
|
||
# 确保启动命令是:
|
||
node dist/index.js
|
||
|
||
# 不要写:
|
||
# sh -c "npx prisma migrate deploy && node dist/index.js" # ❌
|
||
```
|
||
|
||
#### 错误 2:应用启动后,访问数据库报错
|
||
|
||
**错误信息**:
|
||
|
||
```typescript
|
||
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:测试健康检查
|
||
|
||
```bash
|
||
# 使用公网地址测试
|
||
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:测试用户注册
|
||
|
||
```bash
|
||
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 模块)
|
||
|
||
```bash
|
||
# 获取 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 微服务调用
|
||
|
||
```bash
|
||
# 测试 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 服务调用
|
||
|
||
```bash
|
||
# 测试 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 文件上传
|
||
|
||
```bash
|
||
# 上传大文件(测试 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 控制台** → **应用详情** → **日志** → **实时日志**
|
||
|
||
**关键日志示例**:
|
||
|
||
```bash
|
||
# ✅ 正常启动
|
||
[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. 数据库连接监控
|
||
|
||
```bash
|
||
# 在 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 连接
|
||
```
|
||
|
||
### 🔧 日常维护任务
|
||
|
||
#### 每日检查
|
||
|
||
```bash
|
||
# 1. 检查应用健康状态
|
||
# SAE 控制台 → 应用列表 → 查看运行状态(绿色为正常)
|
||
|
||
# 2. 查看错误日志
|
||
# SAE 控制台 → 应用详情 → 日志 → 筛选 ERROR 级别
|
||
|
||
# 3. 检查数据库连接数
|
||
# RDS 控制台 → 实例监控 → 连接数(< 80% 为健康)
|
||
```
|
||
|
||
#### 每周任务
|
||
|
||
```bash
|
||
# 1. 查看性能指标趋势
|
||
# SAE 控制台 → 应用详情 → 监控 → 选择"最近 7 天"
|
||
# 关注:
|
||
# - CPU/内存是否有持续上涨(内存泄漏?)
|
||
# - 响应时间是否变慢
|
||
# - 错误率是否增加
|
||
|
||
# 2. 清理日志(如果日志存储空间紧张)
|
||
# SAE 控制台 → 应用详情 → 日志配置 → 清理旧日志
|
||
```
|
||
|
||
#### 每月任务
|
||
|
||
```bash
|
||
# 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 控制台显示:实例启动中 → 健康检查失败 → 实例停止
|
||
```
|
||
|
||
**排查步骤**:
|
||
|
||
```bash
|
||
# 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)
|
||
|
||
**排查步骤**:
|
||
|
||
```bash
|
||
# 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';
|
||
"
|
||
```
|
||
|
||
**解决方法**:
|
||
|
||
```bash
|
||
# 方法 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
|
||
```
|
||
|
||
**排查步骤**:
|
||
|
||
```bash
|
||
# 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 端口
|
||
```
|
||
|
||
**解决方法**:
|
||
|
||
```bash
|
||
# 如果内网地址错误,更新环境变量:
|
||
# 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
|
||
```
|
||
|
||
**排查步骤**:
|
||
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
**解决方法**:
|
||
|
||
```bash
|
||
# 如果 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
|
||
```
|
||
|
||
**排查步骤**:
|
||
|
||
```bash
|
||
# 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));
|
||
"
|
||
```
|
||
|
||
**解决方法**:
|
||
|
||
```bash
|
||
# 如果 AccessKey 错误,重新生成:
|
||
# 阿里云控制台 → AccessKey 管理 → 创建 AccessKey
|
||
# 更新 SAE 环境变量
|
||
|
||
# 如果 Bucket 权限错误:
|
||
# OSS 控制台 → Bucket 列表 → clinical-research-files → 访问控制
|
||
# 确认 Bucket 为"私有",且 RAM 用户有读写权限
|
||
```
|
||
|
||
### 问题 6:内存泄漏
|
||
|
||
**症状**:
|
||
|
||
```
|
||
SAE 监控显示:内存使用率持续上涨,最终导致 OOM(Out of Memory)
|
||
日志:JavaScript heap out of memory
|
||
```
|
||
|
||
**排查步骤**:
|
||
|
||
```bash
|
||
# 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 查询结果未释放**
|
||
|
||
**解决方法**:
|
||
|
||
```typescript
|
||
// 错误示例:全局缓存无限增长
|
||
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 与数据库不一致
|
||
|
||
**症状**:
|
||
|
||
```typescript
|
||
// 代码中访问字段:
|
||
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 生成,不知道新字段
|
||
|
||
**解决方法**:
|
||
|
||
```bash
|
||
# 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. **环境变量管理**
|
||
|
||
```bash
|
||
# ✅ 正确做法:使用 SAE 环境变量
|
||
# SAE 控制台 → 应用配置 → 环境变量
|
||
|
||
# ❌ 错误做法:在代码或 Dockerfile 中硬编码
|
||
# Dockerfile
|
||
ENV DATABASE_URL="postgresql://user:pass@host/db" # ❌ 泄露敏感信息
|
||
```
|
||
|
||
#### 2. **连接池配置**
|
||
|
||
```bash
|
||
# ✅ 正确做法:根据 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. **文件存储**
|
||
|
||
```typescript
|
||
// ✅ 正确做法:使用 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. **日志输出**
|
||
|
||
```typescript
|
||
// ✅ 正确做法:使用 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. **优雅关闭**
|
||
|
||
```typescript
|
||
// ✅ 正确做法:监听 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(致命)**
|
||
|
||
```bash
|
||
# ❌ 错误做法:
|
||
# 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`**
|
||
|
||
```bash
|
||
# ❌ 错误做法(如果数据库是通过 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 反向同步步骤**
|
||
|
||
```bash
|
||
# ❌ 错误流程:
|
||
pg_dump → 导入 RDS → 直接构建镜像 → 部署
|
||
# ❌ 问题:Schema 与数据库不一致
|
||
|
||
# ✅ 正确流程:
|
||
pg_dump → 导入 RDS → prisma db pull(同步)→ 构建镜像 → 部署
|
||
# ✅ 关键:prisma db pull 确保一致性
|
||
```
|
||
|
||
#### 4. **禁止在代码中硬编码敏感信息**
|
||
|
||
```typescript
|
||
// ❌ 错误示例
|
||
const dbUrl = 'postgresql://admin:P@ssw0rd@pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432/ai_clinical';
|
||
|
||
// ✅ 正确做法
|
||
const dbUrl = process.env.DATABASE_URL;
|
||
```
|
||
|
||
#### 5. **禁止在 backend/ 目录构建前不复制 Prisma 文件**
|
||
|
||
```bash
|
||
# ❌ 错误做法:
|
||
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. **禁止使用内存作为缓存(多实例环境)**
|
||
|
||
```typescript
|
||
// ❌ 错误示例:内存缓存不共享
|
||
const cache = new Map(); // 实例 1 的缓存,实例 2 看不到
|
||
|
||
// ✅ 正确做法:使用 PostgreSQL 缓存(Postgres-Only)
|
||
import { CacheFactory } from './common/cache';
|
||
const cache = CacheFactory.create(); // 根据 CACHE_TYPE 选择
|
||
```
|
||
|
||
#### 7. **禁止使用本地文件作为队列**
|
||
|
||
```typescript
|
||
// ❌ 错误示例:文件队列不共享
|
||
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. **禁止使用同步阻塞操作**
|
||
|
||
```typescript
|
||
// ❌ 错误示例:阻塞事件循环
|
||
import fs from 'fs';
|
||
const data = fs.readFileSync('/large-file.pdf'); // 阻塞其他请求
|
||
|
||
// ✅ 正确做法:使用异步 API
|
||
const data = await fs.promises.readFile('/large-file.pdf');
|
||
```
|
||
|
||
#### 9. **禁止跳过健康检查**
|
||
|
||
```typescript
|
||
// ❌ 错误示例:健康检查总是返回 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. **禁止忽略错误处理**
|
||
|
||
```typescript
|
||
// ❌ 错误示例:吞掉错误
|
||
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 模式**
|
||
|
||
```bash
|
||
# ❌ 错误配置
|
||
NODE_ENV=development # 会打印大量调试日志,暴露敏感信息
|
||
|
||
# ✅ 正确配置
|
||
NODE_ENV=production
|
||
LOG_LEVEL=info
|
||
```
|
||
|
||
#### 12. **禁止使用弱 JWT 密钥**
|
||
|
||
```bash
|
||
# ❌ 错误配置
|
||
JWT_SECRET=secret # 太弱,容易被破解
|
||
|
||
# ✅ 正确配置(至少 32 位随机字符串)
|
||
JWT_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 # 使用 openssl rand -base64 32 生成
|
||
```
|
||
|
||
#### 13. **禁止直接暴露 Prisma 错误到前端**
|
||
|
||
```typescript
|
||
// ❌ 错误示例:泄露数据库结构
|
||
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 镜像优化**
|
||
|
||
```dockerfile
|
||
# ❌ 错误示例:单阶段构建,镜像臃肿
|
||
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 # 仅生产依赖
|
||
```
|
||
|
||
---
|
||
|
||
## 📚 相关文档
|
||
|
||
- [03-Dify-ECS部署完全指南.md](./03-Dify-ECS部署完全指南.md)
|
||
- [04-Python微服务-SAE容器部署指南.md](./04-Python微服务-SAE容器部署指南.md)
|
||
- [06-前端Nginx-SAE容器部署指南.md](./06-前端Nginx-SAE容器部署指南.md)(待创建)
|
||
|
||
---
|
||
|
||
## 🆘 获取帮助
|
||
|
||
**遇到问题?**
|
||
|
||
1. 查看本文档的"故障排查"章节
|
||
2. 查看 SAE 控制台的实时日志
|
||
3. 查看 RDS 控制台的性能监控
|
||
4. 联系团队技术支持
|
||
|
||
---
|
||
|
||
**部署愉快!🚀**
|
||
|