Files
AIclinicalresearch/docs/09-架构实施/03-云原生部署架构指南.md
HaHafeng 66255368b7 feat(admin): Add user management and upgrade to module permission system
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
2026-01-16 13:42:10 +08:00

1171 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 云原生部署架构指南
> **文档版本:** V1.0
> **创建日期:** 2025-11-16
> **适用对象:** 后端开发、架构师、运维
> **维护者:** 架构团队
> **状态:** ✅ 已完成
---
## 📋 文档说明
本文档提供 **AI临床研究平台** 部署到阿里云 Serverless 架构的完整指南。
> **⭐ 重要更新2025-11-16**
> 平台基础设施的详细实施计划和代码实现已迁移到:
> **[平台基础设施规划](./04-平台基础设施规划.md)**
>
> 本文档聚焦于:
> - 云原生架构总体设计
> - 阿里云服务选型和配置
> - Docker容器化和部署流程
> - 成本估算和监控告警
**文档定位**
- 本文档03**云原生部署架构总览** - 侧重云服务和部署流程
- 04文档**平台基础设施规划** - 侧重代码实现和开发指南
**阅读时间**20 分钟
**实施时间**:参见 [平台基础设施规划](./04-平台基础设施规划.md) 的2.5天实施计划
---
## 🏗️ 架构详解
### 1. Serverless 应用引擎 (SAE)
#### **产品特性**
| 特性 | 说明 | 优势 |
|------|------|------|
| **自动扩缩容** | 根据流量自动调整实例数0-100 | 高峰期不宕机,低谷期省成本 |
| **按需付费** | ¥0.000110592/请求次 + 实例费 | 初期月费约 ¥200-500 |
| **容器化部署** | 支持 Docker 镜像 | 环境一致性,快速回滚 |
| **内置负载均衡** | 自动分配流量 | 无需单独购买 SLB |
| **健康检查** | 自动重启异常实例 | 提高可用性 |
#### **实例规格选择**
| 阶段 | 规格 | vCPU | 内存 | 适用场景 |
|------|------|------|------|---------|
| **开发/测试** | 0.5C1G | 0.5核 | 1GB | 日请求 < 1000 |
| **初期** | 1C2G | 1核 | 2GB | 日请求 1000-5000 |
| **成长期** | 2C4G | 2核 | 4GB | 日请求 5000-20000 |
| **成熟期** | 4C8G | 4核 | 8GB | 日请求 > 20000 |
**建议配置**
```yaml
# SAE 应用配置
实例规格: 1C2G
最小实例数: 1 # 避免冷启动
最大实例数: 10
CPU 触发扩容阈值: 70%
内存触发扩容阈值: 80%
```
---
### 2. 云数据库 RDS (PostgreSQL 15)
#### **规格选型**
| 阶段 | 规格 | vCPU | 内存 | 最大连接数 | 月费 |
|------|------|------|------|-----------|------|
| **开发/测试** | 基础版 1C1G | 1核 | 1GB | 100 | ¥120 |
| **初期** | 通用版 2C4G | 2核 | 4GB | 400 | ¥300 |
| **成长期** | 通用版 4C8G | 4核 | 8GB | 800 | ¥600 |
| **成熟期** | 独享版 8C16G | 8核 | 16GB | 1600 | ¥1200 |
#### **关键配置**
```sql
-- 查看当前最大连接数
SHOW max_connections;
-- 查看当前活跃连接
SELECT count(*) FROM pg_stat_activity;
-- 按数据库分组统计连接
SELECT datname, count(*)
FROM pg_stat_activity
GROUP BY datname;
```
**连接池计算公式**
```
每实例连接数 = RDS最大连接数 / SAE最大实例数 × 0.8(安全系数)
示例:
RDS: 400连接
SAE: 最多10实例
每实例: 400 / 10 × 0.8 = 32连接
```
---
### 3. 对象存储 OSS
#### **Bucket 配置**
```yaml
Bucket名称: aiclinical-prod
区域: 华东1杭州oss-cn-hangzhou
存储类型: 标准存储
访问权限: 私有Private
版本控制: 开启
跨域设置: 允许前端域名
```
#### **目录结构规划**
```
aiclinical-prod/
├── asl/
│ ├── pdfs/ # PDF文件
│ ├── excel/ # Excel文件
│ └── exports/ # 导出文件
├── avatars/ # 用户头像
├── documents/ # 知识库文档
└── temp/ # 临时文件1天后自动删除
```
#### **生命周期管理**
```json
{
"Rules": [
{
"ID": "delete-temp-files",
"Prefix": "temp/",
"Status": "Enabled",
"Expiration": {
"Days": 1
}
},
{
"ID": "archive-old-pdfs",
"Prefix": "asl/pdfs/",
"Status": "Enabled",
"Transitions": [
{
"Days": 90,
"StorageClass": "IA" // 转为低频访问
}
]
}
]
}
```
---
## 💻 存储抽象层设计(核心)
### 接口定义
**文件**`backend/src/common/storage/StorageAdapter.ts`
```typescript
/**
* 存储抽象层接口
*
* @description
* - 支持本地文件系统 + 阿里云 OSS 无缝切换
* - 通过环境变量控制实现类
*
* @example
* const storage = StorageFactory.create()
* const url = await storage.upload('files/doc.pdf', buffer)
*/
export interface StorageAdapter {
/**
* 上传文件
* @param key 存储键(路径),如 'asl/pdfs/xxx.pdf'
* @param buffer 文件内容
* @returns 访问URL
*/
upload(key: string, buffer: Buffer): Promise<string>
/**
* 下载文件
* @param key 存储键
* @returns 文件内容
*/
download(key: string): Promise<Buffer>
/**
* 删除文件
* @param key 存储键
*/
delete(key: string): Promise<void>
/**
* 获取访问URL
* @param key 存储键
* @returns 完整访问URL
*/
getUrl(key: string): string
/**
* 批量上传
* @param files 文件列表
* @returns URL列表
*/
uploadMany(files: Array<{ key: string; buffer: Buffer }>): Promise<string[]>
}
```
---
### LocalAdapter 实现(本地开发)
**文件**`backend/src/common/storage/LocalAdapter.ts`
```typescript
import fs from 'fs/promises'
import path from 'path'
import { StorageAdapter } from './StorageAdapter.js'
/**
* 本地文件系统存储适配器
*
* @description
* - 用于本地开发环境
* - 文件存储在 ./uploads 目录
* - 通过 HTTP 访问http://localhost:3001/uploads/xxx
*/
export class LocalAdapter implements StorageAdapter {
private uploadDir: string
private baseUrl: string
constructor() {
this.uploadDir = path.resolve(process.cwd(), 'uploads')
this.baseUrl = process.env.BASE_URL || 'http://localhost:3001'
// 确保目录存在
this.ensureUploadDir()
}
private async ensureUploadDir() {
try {
await fs.mkdir(this.uploadDir, { recursive: true })
} catch (error) {
console.error('创建上传目录失败:', error)
}
}
async upload(key: string, buffer: Buffer): Promise<string> {
const filePath = path.join(this.uploadDir, key)
// 确保父目录存在
await fs.mkdir(path.dirname(filePath), { recursive: true })
// 写入文件
await fs.writeFile(filePath, buffer)
// 返回访问URL
return this.getUrl(key)
}
async download(key: string): Promise<Buffer> {
const filePath = path.join(this.uploadDir, key)
return await fs.readFile(filePath)
}
async delete(key: string): Promise<void> {
const filePath = path.join(this.uploadDir, key)
try {
await fs.unlink(filePath)
} catch (error) {
// 文件不存在时忽略错误
if ((error as any).code !== 'ENOENT') {
throw error
}
}
}
getUrl(key: string): string {
return `${this.baseUrl}/uploads/${key}`
}
async uploadMany(files: Array<{ key: string; buffer: Buffer }>): Promise<string[]> {
const urls = await Promise.all(
files.map(file => this.upload(file.key, file.buffer))
)
return urls
}
}
```
---
### OSSAdapter 实现(生产环境)
**文件**`backend/src/common/storage/OSSAdapter.ts`
```typescript
import OSS from 'ali-oss'
import { StorageAdapter } from './StorageAdapter.js'
/**
* 阿里云 OSS 存储适配器
*
* @description
* - 用于生产环境
* - 文件存储在阿里云 OSS
* - 支持内网/外网访问
*/
export class OSSAdapter implements StorageAdapter {
private client: OSS
private bucket: string
private region: string
constructor() {
this.region = process.env.OSS_REGION!
this.bucket = process.env.OSS_BUCKET!
if (!this.region || !this.bucket) {
throw new Error('OSS配置缺失OSS_REGION 或 OSS_BUCKET 未设置')
}
this.client = new OSS({
region: this.region,
accessKeyId: process.env.OSS_ACCESS_KEY_ID!,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!,
bucket: this.bucket,
// 使用内网endpointSAE访问OSS免流量费
internal: process.env.NODE_ENV === 'production',
})
}
async upload(key: string, buffer: Buffer): Promise<string> {
try {
const result = await this.client.put(key, buffer)
return result.url
} catch (error) {
console.error('OSS上传失败:', error)
throw new Error(`OSS上传失败: ${key}`)
}
}
async download(key: string): Promise<Buffer> {
try {
const result = await this.client.get(key)
return result.content as Buffer
} catch (error) {
console.error('OSS下载失败:', error)
throw new Error(`OSS下载失败: ${key}`)
}
}
async delete(key: string): Promise<void> {
try {
await this.client.delete(key)
} catch (error) {
console.error('OSS删除失败:', error)
// 文件不存在时不抛出错误
}
}
getUrl(key: string): string {
// 返回外网访问URL
return `https://${this.bucket}.${this.region}.aliyuncs.com/${key}`
}
async uploadMany(files: Array<{ key: string; buffer: Buffer }>): Promise<string[]> {
// 并行上传最多10个并发
const chunks = []
for (let i = 0; i < files.length; i += 10) {
chunks.push(files.slice(i, i + 10))
}
const urls: string[] = []
for (const chunk of chunks) {
const chunkUrls = await Promise.all(
chunk.map(file => this.upload(file.key, file.buffer))
)
urls.push(...chunkUrls)
}
return urls
}
/**
* 生成签名URL临时访问
* @param key 存储键
* @param expires 过期时间默认1小时
*/
async getSignedUrl(key: string, expires: number = 3600): Promise<string> {
return this.client.signatureUrl(key, { expires })
}
}
```
---
### StorageFactory 工厂类
**文件**`backend/src/common/storage/StorageFactory.ts`
```typescript
import { StorageAdapter } from './StorageAdapter.js'
import { LocalAdapter } from './LocalAdapter.js'
import { OSSAdapter } from './OSSAdapter.js'
/**
* 存储工厂类
*
* @description
* - 根据环境变量自动选择存储实现
* - STORAGE_TYPE=local → LocalAdapter
* - STORAGE_TYPE=oss → OSSAdapter
*/
export class StorageFactory {
private static instance: StorageAdapter | null = null
/**
* 创建存储实例(单例模式)
*/
static create(): StorageAdapter {
if (this.instance) {
return this.instance
}
const storageType = process.env.STORAGE_TYPE || 'local'
switch (storageType) {
case 'oss':
console.log('📦 使用阿里云 OSS 存储')
this.instance = new OSSAdapter()
break
case 'local':
console.log('📁 使用本地文件存储')
this.instance = new LocalAdapter()
break
default:
throw new Error(`未知的存储类型: ${storageType}`)
}
return this.instance
}
/**
* 重置实例(用于测试)
*/
static reset() {
this.instance = null
}
}
// 导出单例
export const storage = StorageFactory.create()
```
---
### 使用示例
```typescript
// backend/src/modules/asl/controllers/literatureController.ts
import { storage } from '../../../common/storage/StorageFactory.js'
import { prisma } from '../../../config/database.js'
/**
* 上传PDF文件
*/
export async function uploadPdf(req, res) {
try {
const { literatureId } = req.params
const file = await req.file()
if (!file) {
return res.status(400).send({ error: 'No file uploaded' })
}
// 读取文件内容
const buffer = await file.toBuffer()
// 生成存储键
const key = `asl/pdfs/${Date.now()}-${file.filename}`
// ✅ 上传到存储自动根据环境选择Local或OSS
const url = await storage.upload(key, buffer)
// 保存到数据库
await prisma.aslLiterature.update({
where: { id: literatureId },
data: {
pdfUrl: url,
pdfOssKey: key,
pdfFileSize: buffer.length,
}
})
res.send({
success: true,
url,
size: buffer.length
})
} catch (error) {
console.error('PDF上传失败:', error)
res.status(500).send({ error: 'Upload failed' })
}
}
/**
* Excel上传不需要存储直接解析
*/
export async function importExcel(req, res) {
const file = await req.file()
const buffer = await file.toBuffer()
// ✅ 直接从内存解析,不落盘
const workbook = xlsx.read(buffer, { type: 'buffer' })
const data = xlsx.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]])
// 批量入库
await prisma.aslLiterature.createMany({ data })
res.send({ success: true, count: data.length })
}
```
---
## 🔧 数据库连接池配置
### Prisma配置
**文件**`backend/src/config/database.ts`
```typescript
import { PrismaClient } from '@prisma/client'
// 环境判断
const isProduction = process.env.NODE_ENV === 'production'
// 计算连接池大小
// 生产环境RDS 400连接 / SAE 10实例 × 0.8 = 32连接/实例
// 开发环境:本地 PostgreSQL5连接足够
const connectionLimit = isProduction ? 32 : 5
/**
* Prisma 客户端配置
*/
export const prisma = new PrismaClient({
log: isProduction
? ['error', 'warn'] // 生产环境只记录错误和警告
: ['query', 'error', 'warn'], // 开发环境记录所有查询
datasources: {
db: {
url: process.env.DATABASE_URL
}
},
// 关键配置:连接池
...(isProduction && {
// 仅在生产环境配置连接池限制
datasources: {
db: {
url: process.env.DATABASE_URL,
// 连接池配置
pool: {
timeout: 5, // 获取连接超时(秒)
maxsize: connectionLimit, // 最大连接数
min: 2, // 最小保持连接
}
}
}
})
})
// 优雅关闭
process.on('SIGINT', async () => {
console.log('📦 正在关闭数据库连接...')
await prisma.$disconnect()
process.exit(0)
})
process.on('SIGTERM', async () => {
console.log('📦 收到SIGTERM正在关闭数据库连接...')
await prisma.$disconnect()
process.exit(0)
})
// 连接数监控(仅生产环境)
if (isProduction) {
setInterval(async () => {
try {
const result = await prisma.$queryRaw<Array<{ count: bigint }>>`
SELECT count(*) as count
FROM pg_stat_activity
WHERE datname = current_database()
`
const connectionCount = Number(result[0].count)
console.log(`📊 当前数据库连接数: ${connectionCount}`)
// 告警阈值80%
const maxConnections = 400 // 根据RDS规格设置
if (connectionCount > maxConnections * 0.8) {
console.error(`⚠️ 数据库连接数过高: ${connectionCount}/${maxConnections}`)
}
} catch (error) {
console.error('连接数监控失败:', error)
}
}, 60000) // 每60秒检查一次
}
```
---
## 🌍 环境变量管理
### 本地开发环境
**文件**`backend/.env.development`
```bash
# 环境
NODE_ENV=development
# 存储配置
STORAGE_TYPE=local
BASE_URL=http://localhost:3001
# 数据库
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/aiclinical_dev
# LLM配置
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://api.deepseek.com
# 其他服务(本地无需配置)
OSS_REGION=
OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
OSS_BUCKET=
```
---
### 生产环境配置
**在SAE控制台配置不要写入文件**
```bash
# 环境
NODE_ENV=production
# 存储配置
STORAGE_TYPE=oss
OSS_REGION=oss-cn-hangzhou
OSS_ACCESS_KEY_ID=LTAI5t***从RAM用户获取
OSS_ACCESS_KEY_SECRET=***
OSS_BUCKET=aiclinical-prod
# 数据库
DATABASE_URL=postgresql://aiclinical:***@rm-xxx.mysql.rds.aliyuncs.com:5432/aiclinical_prod
# LLM配置
LLM_API_KEY=sk-***
LLM_BASE_URL=https://api.deepseek.com
# 日志级别
LOG_LEVEL=info
```
---
## 🐳 Docker 配置
### Dockerfile
**文件**`backend/Dockerfile`
```dockerfile
# ==================== 构建阶段 ====================
FROM node:20-alpine AS builder
WORKDIR /app
# 复制依赖文件
COPY package*.json ./
COPY prisma ./prisma/
# 安装依赖包括dev依赖用于构建
RUN npm ci
# 复制源代码
COPY . .
# 生成 Prisma Client
RUN npx prisma generate
# 构建 TypeScript
RUN npm run build
# ==================== 生产阶段 ====================
FROM node:20-alpine
WORKDIR /app
# 复制依赖文件
COPY package*.json ./
COPY prisma ./prisma/
# 仅安装生产依赖
RUN npm ci --only=production
# 生成 Prisma Client
RUN npx prisma generate
# 从构建阶段复制编译后的代码
COPY --from=builder /app/dist ./dist
# 复制静态文件如prompts
COPY prompts ./prompts
COPY config ./config
# 创建非root用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# 切换到非root用户
USER nodejs
# 暴露端口
EXPOSE 3001
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# 启动应用
CMD ["node", "dist/index.js"]
```
---
### docker-compose.yml本地测试
**文件**`docker-compose.yml`
```yaml
version: '3.8'
services:
# PostgreSQL数据库
postgres:
image: postgres:15-alpine
container_name: aiclinical-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: aiclinical_dev
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# Redis可选
redis:
image: redis:7-alpine
container_name: aiclinical-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
# 后端应用
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: aiclinical-backend
ports:
- "3001:3001"
environment:
NODE_ENV: development
STORAGE_TYPE: local
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/aiclinical_dev
depends_on:
postgres:
condition: service_healthy
volumes:
- ./backend/uploads:/app/uploads # 本地存储目录
volumes:
postgres-data:
redis-data:
```
**使用方式**
```bash
# 启动所有服务
docker-compose up -d
# 查看日志
docker-compose logs -f backend
# 停止服务
docker-compose down
```
---
## 🚀 部署流程
### Step 1: 准备阿里云服务
#### 1.1 开通云数据库 RDS
```bash
# 1. 登录阿里云控制台
# 2. 搜索"云数据库 RDS"
# 3. 创建实例
# - 数据库类型PostgreSQL 15
# - 规格2核4GB通用型
# - 存储空间20GBSSD
# - 网络VPC与SAE同区域
# 4. 设置白名单(添加 SAE 应用的 VPC网段
# 5. 创建数据库用户
# 6. 创建数据库aiclinical_prod
```
#### 1.2 开通对象存储 OSS
```bash
# 1. 搜索"对象存储 OSS"
# 2. 创建 Bucket
# - Bucket名称aiclinical-prod
# - 区域华东1杭州
# - 存储类型:标准存储
# - 访问权限:私有
# 3. 创建 RAM 用户用于API访问
# - 权限AliyunOSSFullAccess
# - 获取 AccessKeyId 和 AccessKeySecret
```
#### 1.3 开通容器镜像服务
```bash
# 1. 搜索"容器镜像服务 ACR"
# 2. 创建命名空间aiclinical
# 3. 创建镜像仓库backend
# - 类型:私有
# - 地域华东1杭州
```
---
### Step 2: 构建和推送镜像
```bash
# 1. 登录阿里云容器镜像服务
docker login --username=<你的阿里云账号> registry.cn-hangzhou.aliyuncs.com
# 2. 构建镜像
cd backend
docker build -t aiclinical-backend:v1.0.0 .
# 3. 打标签
docker tag aiclinical-backend:v1.0.0 \
registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.0
# 4. 推送到阿里云
docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.0
```
**脚本自动化**
**文件**`backend/scripts/build-and-push.sh`
```bash
#!/bin/bash
VERSION=$1
if [ -z "$VERSION" ]; then
echo "Usage: ./build-and-push.sh <version>"
echo "Example: ./build-and-push.sh v1.0.0"
exit 1
fi
echo "🔨 构建镜像: $VERSION"
docker build -t aiclinical-backend:$VERSION .
echo "🏷️ 打标签"
docker tag aiclinical-backend:$VERSION \
registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:$VERSION
echo "📤 推送到阿里云"
docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:$VERSION
echo "✅ 完成!"
echo "镜像地址: registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:$VERSION"
```
---
### Step 3: 创建 SAE 应用
#### 3.1 基本配置
```yaml
应用名称: aiclinical-backend
应用类型: 容器镜像
镜像地址: registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.0
端口: 3001
健康检查路径: /health
```
#### 3.2 实例配置
```yaml
实例规格: 1C2G
最小实例数: 1
最大实例数: 10
CPU触发扩容: 70%
内存触发扩容: 80%
```
#### 3.3 环境变量配置
在 SAE 控制台配置环境变量(**重要!**
```bash
NODE_ENV=production
STORAGE_TYPE=oss
DATABASE_URL=postgresql://aiclinical:***@rm-xxx.mysql.rds.aliyuncs.com:5432/aiclinical_prod
OSS_REGION=oss-cn-hangzhou
OSS_ACCESS_KEY_ID=LTAI5t***
OSS_ACCESS_KEY_SECRET=***
OSS_BUCKET=aiclinical-prod
LLM_API_KEY=sk-***
LOG_LEVEL=info
```
#### 3.4 VPC网络配置
```yaml
VPC: 选择与RDS相同的VPC
安全组: 允许3001端口入站
公网访问: 开启分配公网SLB
```
---
### Step 4: 部署应用
```bash
# 1. 在 SAE 控制台点击"部署应用"
# 2. 选择镜像版本
# 3. 确认配置无误
# 4. 点击"确定"开始部署
# 等待3-5分钟查看部署日志
```
**部署成功标志**
- ✅ 实例状态:运行中
- ✅ 健康检查:通过
- ✅ 访问 `http://<SAE公网地址>/health` 返回 200
---
### Step 5: 验证部署
```bash
# 1. 健康检查
curl http://<SAE公网地址>/health
# 预期响应:
{
"status": "ok",
"database": "connected",
"timestamp": "2025-11-16T10:30:00.000Z"
}
# 2. API测试
curl http://<SAE公网地址>/api/v1/health
# 3. 查看日志
# 在 SAE 控制台 → 应用详情 → 实时日志
```
---
## 📊 成本估算
### 初期阶段100用户日活20
| 服务 | 规格 | 月费 |
|------|------|------|
| SAE | 1C2G × 1实例 | ¥200 |
| RDS | 2C4G 通用版 | ¥300 |
| OSS | 100GB标准存储 + 10GB流量 | ¥15 |
| ACR | 私有仓库 | ¥0免费额度 |
| **合计** | | **¥515/月** |
### 成长期1000用户日活200
| 服务 | 规格 | 月费 |
|------|------|------|
| SAE | 2C4G × 平均3实例 | ¥600 |
| RDS | 4C8G 通用版 | ¥600 |
| OSS | 500GB标准存储 + 50GB流量 | ¥70 |
| CDN | 100GB流量可选 | ¥20 |
| **合计** | | **¥1290/月** |
### 成熟期10000用户日活2000
| 服务 | 规格 | 月费 |
|------|------|------|
| SAE | 4C8G × 平均10实例 | ¥2000 |
| RDS | 8C16G 独享版 + 读写分离 | ¥2000 |
| OSS | 2TB标准存储 + 500GB流量 | ¥300 |
| CDN | 1TB流量 | ¥200 |
| Redis | 2GB标准版 | ¥200 |
| **合计** | | **¥4700/月** |
---
## 🔍 监控与告警
### 应用性能监控ARMS
```yaml
# 在 SAE 控制台开通 ARMS
监控指标:
- RT响应时间
- QPS每秒请求数
- 错误率
- JVM内存
告警规则:
- RT > 3秒
- 错误率 > 5%
- 可用率 < 99%
```
### 数据库监控
```yaml
# RDS 控制台 → 监控与报警
监控指标:
- CPU利用率
- 内存利用率
- 连接数
- IOPS
告警规则:
- CPU > 80%
- 连接数 > 32080%
- 磁盘使用率 > 80%
```
### 成本告警
```bash
# 费用中心 → 费用预警
设置预算:
- 每月预算: ¥1000
- 80%预警: ¥800
- 通知方式: 邮件 + 短信
```
---
## 📚 故障排查
### 常见问题
#### 问题1数据库连接失败
**症状**:应用启动失败,日志显示 `Connection refused`
**解决方案**
```bash
# 1. 检查 RDS 白名单
# 确保添加了 SAE 应用的 VPC网段
# 2. 检查 DATABASE_URL 格式
# 正确格式: postgresql://user:pass@host:port/db
# 3. 检查 RDS 实例状态
# 确保实例运行中
```
---
#### 问题2OSS 上传失败
**症状**:文件上传返回 403 Forbidden
**解决方案**
```bash
# 1. 检查 RAM 用户权限
# 确保有 AliyunOSSFullAccess
# 2. 检查 Bucket 权限
# 确保应用有写入权限
# 3. 检查环境变量
# OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET 是否正确
```
---
#### 问题3连接数耗尽
**症状**`Connection pool exhausted`
**解决方案**
```typescript
// 1. 检查当前连接数
const result = await prisma.$queryRaw`
SELECT count(*) FROM pg_stat_activity
`
// 2. 调整连接池配置
// 减少每实例连接数,或增加 RDS 规格
// 3. 检查是否有连接泄漏
// 确保所有查询都正确关闭
```
---
## 📝 更新日志
| 日期 | 版本 | 变更内容 | 维护者 |
|------|------|---------|--------|
| 2025-11-16 | V1.0 | 创建文档,定义云原生部署架构 | 架构团队 |
---
## 📚 相关文档
- [前后端模块化架构设计-V2](../00-系统总体设计/前后端模块化架构设计-V2.md) - 架构总纲
- [云原生开发规范](../04-开发规范/08-云原生开发规范.md) - DO/DON'T 检查清单
- [Schema隔离架构设计](./01-Schema隔离架构设计10个.md)
- [数据库连接配置](./02-数据库连接配置.md)
---
**文档维护者:** 架构团队
**最后更新:** 2025-11-16
**文档状态:** ✅ 已完成