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

25 KiB
Raw Permalink Blame History

云原生部署架构指南

文档版本: V1.0
创建日期: 2025-11-16
适用对象: 后端开发、架构师、运维
维护者: 架构团队
状态: 已完成


📋 文档说明

本文档提供 AI临床研究平台 部署到阿里云 Serverless 架构的完整指南。

重要更新2025-11-16
平台基础设施的详细实施计划和代码实现已迁移到:
平台基础设施规划

本文档聚焦于:

  • 云原生架构总体设计
  • 阿里云服务选型和配置
  • Docker容器化和部署流程
  • 成本估算和监控告警

文档定位

  • 本文档03云原生部署架构总览 - 侧重云服务和部署流程
  • 04文档平台基础设施规划 - 侧重代码实现和开发指南

阅读时间20 分钟
实施时间:参见 平台基础设施规划 的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

建议配置

# 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

关键配置

-- 查看当前最大连接数
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 配置

Bucket名称: aiclinical-prod
区域: 华东1杭州oss-cn-hangzhou
存储类型: 标准存储
访问权限: 私有Private
版本控制: 开启
跨域设置: 允许前端域名

目录结构规划

aiclinical-prod/
├── asl/
│   ├── pdfs/               # PDF文件
│   ├── excel/              # Excel文件
│   └── exports/            # 导出文件
├── avatars/                # 用户头像
├── documents/              # 知识库文档
└── temp/                   # 临时文件1天后自动删除

生命周期管理

{
  "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

/**
 * 存储抽象层接口
 * 
 * @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

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

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

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()

使用示例

// 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

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

# 环境
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控制台配置不要写入文件

# 环境
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

# ==================== 构建阶段 ====================
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

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:

使用方式

# 启动所有服务
docker-compose up -d

# 查看日志
docker-compose logs -f backend

# 停止服务
docker-compose down

🚀 部署流程

Step 1: 准备阿里云服务

1.1 开通云数据库 RDS

# 1. 登录阿里云控制台
# 2. 搜索"云数据库 RDS"
# 3. 创建实例
#    - 数据库类型PostgreSQL 15
#    - 规格2核4GB通用型
#    - 存储空间20GBSSD
#    - 网络VPC与SAE同区域
# 4. 设置白名单(添加 SAE 应用的 VPC网段
# 5. 创建数据库用户
# 6. 创建数据库aiclinical_prod

1.2 开通对象存储 OSS

# 1. 搜索"对象存储 OSS"
# 2. 创建 Bucket
#    - Bucket名称aiclinical-prod
#    - 区域华东1杭州
#    - 存储类型:标准存储
#    - 访问权限:私有
# 3. 创建 RAM 用户用于API访问
#    - 权限AliyunOSSFullAccess
#    - 获取 AccessKeyId 和 AccessKeySecret

1.3 开通容器镜像服务

# 1. 搜索"容器镜像服务 ACR"
# 2. 创建命名空间aiclinical
# 3. 创建镜像仓库backend
#    - 类型:私有
#    - 地域华东1杭州

Step 2: 构建和推送镜像

# 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

#!/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 基本配置

应用名称: aiclinical-backend
应用类型: 容器镜像
镜像地址: registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.0
端口: 3001
健康检查路径: /health

3.2 实例配置

实例规格: 1C2G
最小实例数: 1
最大实例数: 10
CPU触发扩容: 70%
内存触发扩容: 80%

3.3 环境变量配置

在 SAE 控制台配置环境变量(重要!

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网络配置

VPC: 选择与RDS相同的VPC
安全组: 允许3001端口入站
公网访问: 开启分配公网SLB

Step 4: 部署应用

# 1. 在 SAE 控制台点击"部署应用"
# 2. 选择镜像版本
# 3. 确认配置无误
# 4. 点击"确定"开始部署

# 等待3-5分钟查看部署日志

部署成功标志

  • 实例状态:运行中
  • 健康检查:通过
  • 访问 http://<SAE公网地址>/health 返回 200

Step 5: 验证部署

# 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

# 在 SAE 控制台开通 ARMS
监控指标:
  - RT响应时间
  - QPS每秒请求数
  - 错误率
  - JVM内存
  
告警规则:
  - RT > 3秒
  - 错误率 > 5%
  - 可用率 < 99%

数据库监控

# RDS 控制台 → 监控与报警
监控指标:
  - CPU利用率
  - 内存利用率
  - 连接数
  - IOPS
  
告警规则:
  - CPU > 80%
  - 连接数 > 32080%
  - 磁盘使用率 > 80%

成本告警

# 费用中心 → 费用预警
设置预算:
  - 每月预算: ¥1000
  - 80%预警: ¥800
  - 通知方式: 邮件 + 短信

📚 故障排查

常见问题

问题1数据库连接失败

症状:应用启动失败,日志显示 Connection refused

解决方案

# 1. 检查 RDS 白名单
#    确保添加了 SAE 应用的 VPC网段

# 2. 检查 DATABASE_URL 格式
#    正确格式: postgresql://user:pass@host:port/db

# 3. 检查 RDS 实例状态
#    确保实例运行中

问题2OSS 上传失败

症状:文件上传返回 403 Forbidden

解决方案

# 1. 检查 RAM 用户权限
#    确保有 AliyunOSSFullAccess

# 2. 检查 Bucket 权限
#    确保应用有写入权限

# 3. 检查环境变量
#    OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET 是否正确

问题3连接数耗尽

症状Connection pool exhausted

解决方案

// 1. 检查当前连接数
const result = await prisma.$queryRaw`
  SELECT count(*) FROM pg_stat_activity
`

// 2. 调整连接池配置
// 减少每实例连接数,或增加 RDS 规格

// 3. 检查是否有连接泄漏
// 确保所有查询都正确关闭

📝 更新日志

日期 版本 变更内容 维护者
2025-11-16 V1.0 创建文档,定义云原生部署架构 架构团队

📚 相关文档


文档维护者: 架构团队
最后更新: 2025-11-16
文档状态: 已完成