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

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

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

25 KiB
Raw Blame History

关键配置补充说明 - 部署文档勘误与增强

文档版本: v1.0
创建日期: 2025-12-14
文档性质: 对5个独立部署文档的关键补充
优先级: 必读包含3个P0/P1致命问题


📋 文档说明

本文档基于对5个独立部署文档的深度审查补充了3个致命问题若干最佳实践。这些内容在原文档中遗漏或未充分强调,但对生产环境部署至关重要。

请在部署前务必阅读本文档!


🚨 致命问题修正P0/P1

1. SAE孤岛效应 - NAT网关配置

问题严重度P0致命

问题描述

SAE部署在VPC内默认没有公网出口

影响场景:
❌ 后端调用 DeepSeek/OpenAI API → 超时
❌ Python下载公网PDF → 超时
❌ npm install公网依赖构建时→ 失败

结果所有AI功能不可用系统基本瘫痪

解决方案

方案ANAT网关推荐生产环境

# 步骤1创建NAT网关
阿里云控制台 > VPC > NAT网关 > 创建NAT网关
├─ VPC选择SAE所在的VPC
├─ 交换机选择SAE所在的交换机
├─ 规格:小型(够用)
└─ 计费方式:按使用量计费

# 步骤2创建并绑定EIP
NAT网关详情 > 弹性公网IP > 绑定弹性公网IP
├─ 创建新EIP或选择已有EIP
├─ 带宽:按使用流量(成本低)
└─ 确认绑定

# 步骤3配置SNAT条目
NAT网关详情 > SNAT管理 > 创建SNAT条目
├─ 选择交换机SAE所在的交换机如 vsw-xxxxx
├─ 选择公网IP刚才绑定的EIP
└─ 确认创建

成本NAT网关¥60/月 + EIP流量费¥30-50/月 = ¥90-110/月

方案BSAE绑定公网IP部分地域支持

SAE控制台 > 应用配置 > 网络配置
└─ 查看是否有"公网访问""绑定EIP"选项

⚠️ 注意:
- 并非所有地域都支持
- 优先使用方案A更稳定

验证NAT网关是否生效

# 方法1在SAE应用日志中查看
# 应用启动后查看是否有DeepSeek API调用成功的日志

# 方法2通过云助手执行命令SAE控制台 > 实例列表 > 登录实例)
curl -I https://api.deepseek.com
# 应该返回 200 OK而不是超时

# 方法3测试Python下载公网PDF
curl -I https://arxiv.org/pdf/2301.00001.pdf
# 应该返回 200 OK

更新的文档

  • 00-部署架构总览.md物理架构图已增加NAT网关
  • 00-部署架构总览.md成本估算已更新¥1,200-1,250/月)
  • ⚠️ 05-Node.js后端-SAE容器部署指南.md:需要在"SAE应用配置"章节增加网络配置说明
  • ⚠️ 04-Python微服务-SAE容器部署指南.md:同上

2. 部署依赖死锁 - Dify API Key鸡生蛋问题

问题严重度P1严重

问题描述

死锁链:
1. 后端启动需要 DIFY_API_KEY
2. DIFY_API_KEY 需要 Dify 启动并人工登录后才能生成
3. 后端如果健康检查失败,会无限重启

结果后端无法启动或启动后PKB模块不可用

解决方案(分阶段部署)

阶段1首次部署后端临时配置

# SAE环境变量配置
DIFY_API_KEY=temp_placeholder_will_update_later

# ⚠️ 重要:后端代码需要容错处理
# backend/src/common/rag/DifyClient.ts
constructor() {
  const apiKey = process.env.DIFY_API_KEY
  
  if (!apiKey || apiKey === 'temp' || apiKey.startsWith('temp_')) {
    console.warn('⚠️ Dify API Key未配置PKB模块将不可用')
    this.enabled = false
    return
  }
  
  this.client = new DifySDK(apiKey)
  this.enabled = true
}

// 所有Dify调用前检查
async createDataset(name: string) {
  if (!this.enabled) {
    throw new Error('Dify服务未配置请先配置DIFY_API_KEY环境变量')
  }
  // ... 正常逻辑
}

阶段2部署Dify并获取真实Key

# 1. 部署Dify到ECS参考 03-Dify-ECS部署完全指南.md
cd /opt/dify
docker-compose up -d

# 2. 等待服务启动约2-3分钟
docker-compose logs -f api

# 3. 浏览器访问 http://ECS公网IP
# 4. 注册管理员账号(首次访问会提示)
# 5. 创建API Key
#    设置 > API密钥 > 创建密钥 > 复制
#    格式app-xxxxxxxxxxxxxxxxxxxxx

# 6. 记录API Key妥善保存
DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxx

阶段3更新后端配置

# SAE控制台 > 应用详情 > 环境变量
# 找到 DIFY_API_KEY修改为真实值
DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxx

# 保存 > 重启应用
# SAE会执行滚动重启零停机

阶段4验证PKB功能

# 测试知识库创建
curl -X POST https://your-api.com/api/v1/pkb/knowledge-bases \
  -H "Authorization: Bearer YOUR_USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"测试知识库","description":"测试"}'

# 应该返回 200 OK而不是 "Dify服务未配置" 错误

更新的文档

  • 00-部署架构总览.md:部署顺序已更新,明确分阶段部署
  • ⚠️ 05-Node.js后端-SAE容器部署指南.md:需要在"环境变量配置"章节增加临时配置说明
  • ⚠️ 03-Dify-ECS部署完全指南.md:需要在"首次访问"章节增加API Key生成步骤

3. HTTP Client超时配置 - 防止连接泄漏

问题严重度P1严重

问题描述

Python服务处理PDF/OCR可能需要60-120秒
如果后端HTTP Client没有设置超时会导致
❌ 连接数堆积
❌ 后端实例内存耗尽
❌ 数据库连接池耗尽

解决方案

后端HTTP Client配置

// backend/src/common/http/httpClient.ts
import axios from 'axios'

export const pythonServiceClient = axios.create({
  baseURL: process.env.EXTRACTION_SERVICE_URL || 'http://localhost:8000',
  timeout: 120000,  // ⚠️ 120秒2分钟
  timeoutErrorMessage: 'Python微服务响应超时>2分钟',
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器(可选,用于日志)
pythonServiceClient.interceptors.request.use(
  (config) => {
    console.log(`[HTTP] 调用Python服务: ${config.method?.toUpperCase()} ${config.url}`)
    return config
  },
  (error) => Promise.reject(error)
)

// 响应拦截器(错误处理)
pythonServiceClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.code === 'ECONNABORTED') {
      console.error('[HTTP] Python服务超时:', error.message)
    }
    return Promise.reject(error)
  }
)

Dify Client配置

// backend/src/common/rag/DifyClient.ts
import axios from 'axios'

const difyHttpClient = axios.create({
  baseURL: process.env.DIFY_API_URL || 'http://localhost/v1',
  timeout: 60000,  // ⚠️ 60秒Dify响应较快
  headers: {
    'Authorization': `Bearer ${process.env.DIFY_API_KEY}`,
    'Content-Type': 'application/json'
  }
})

超时时间建议

服务 超时时间 理由
Python微服务 120秒 PDF解析Nougat OCR可能需要60-120秒
Dify API 60秒 RAG检索通常<10秒60秒足够
外部LLM API 60秒 DeepSeek/OpenAI流式响应60秒足够
数据库查询 30秒 Prisma默认复杂查询可能需要10-20秒

更新的文档

  • ⚠️ 05-Node.js后端-SAE容器部署指南.md:需要在"代码准备"章节增加HTTP Client配置

⚠️ 重要安全配置

4. ECS端口安全 - Redis/Weaviate不对外开放

问题严重度P0致命安全风险

问题描述

Dify的Redis6379和Weaviate8080如果对公网开放
❌ Redis无密码可被攻击者直接访问
❌ Weaviate包含敏感的向量数据
❌ 可能被用于DDoS攻击的跳板

正确配置

docker-compose.yaml 端口配置

services:
  # ❌ 错误示例(危险)
  redis:
    ports:
      - "6379:6379"  # 对所有网卡开放,包括公网!
  
  # ✅ 正确配置
  redis:
    image: redis:6-alpine
    ports:
      - "127.0.0.1:6379:6379"  # 只监听 localhost
    restart: always
    volumes:
      - ./volumes/redis/data:/data
    command: redis-server --save 60 1 --loglevel warning
  
  # ✅ 正确配置
  weaviate:
    image: semitechnologies/weaviate:1.19.0
    ports:
      - "127.0.0.1:8080:8080"  # 只监听 localhost
    restart: always
    environment:
      - QUERY_DEFAULTS_LIMIT=25
      - AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true
      - PERSISTENCE_DATA_PATH=/var/lib/weaviate
  
  # ✅ 只有Nginx需要对外VPC内网
  nginx:
    image: nginx:latest
    ports:
      - "80:80"  # 对VPC内网开放不是公网
    restart: always
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - api
      - web

ECS安全组配置

# 安全组规则ECS控制台 > 安全组 > 配置规则)

入方向规则:
├─ 允许 80/TCP    来源VPC网段172.16.0.0/12  # Nginx
├─ 允许 22/TCP    来源您的办公室IP             # SSH管理
└─ 拒绝 所有       来源0.0.0.0/0                # 默认拒绝

出方向规则:
└─ 允许 所有       目标0.0.0.0/0                # 允许访问公网

验证安全配置

# 从公网测试(应该失败)
telnet ECS公网IP 6379
# 应该超时或拒绝连接

telnet ECS公网IP 8080
# 应该超时或拒绝连接

# 从VPC内测试应该成功
# 在SAE应用中执行
curl http://172.16.x.x  # Dify内网地址
# 应该返回 Dify 的响应

更新的文档

  • ⚠️ 03-Dify-ECS部署完全指南.md:需要在"docker-compose.yaml配置"章节强调端口安全

5. Nginx client_max_body_size - 支持大文件上传

问题严重度P2一般

问题描述

医疗PDF可能很大10-50MB
Nginx默认限制1MB
结果:用户上传大文件时返回 413 Request Entity Too Large

解决方案

前端Nginx配置

# frontend-v2/nginx.conf.template
http {
    # ⚠️ 新增:支持大文件上传
    client_max_body_size 50M;
    
    # ⚠️ 新增开启gzipReact大体积JS
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_min_length 1000;
    gzip_comp_level 6;
    
    server {
        listen 8080;
        server_name _;
        
        # 根目录
        root /usr/share/nginx/html;
        index index.html;
        
        # API反向代理
        location /api/ {
            proxy_pass http://${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT}/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            
            # ⚠️ 新增:代理超时配置
            proxy_connect_timeout 120s;
            proxy_send_timeout 120s;
            proxy_read_timeout 120s;
        }
        
        # SPA路由
        location / {
            try_files $uri $uri/ /index.html;
        }
        
        # 健康检查
        location /health {
            access_log off;
            return 200 "healthy\n";
            add_header Content-Type text/plain;
        }
    }
}

更新的文档

  • ⚠️ 06-前端Nginx-SAE容器部署指南.md:需要在"nginx.conf.template"章节增加配置

6. Python Workers限制 - 防止OOM

问题严重度P1严重

问题描述

PyMuPDF/Nougat OCR非常吃内存单个请求可能占用500MB-1GB
SAE配置2GB内存
如果Gunicorn workers过多会导致OOMOut of Memory

解决方案

Dockerfile配置

# extraction_service/Dockerfile

# 运行阶段
FROM python:3.11-slim

# ... 其他配置 ...

# ⚠️ 关键限制workers防止OOM
CMD ["gunicorn", "app.main:app", \
     "--workers", "2", \
     "--worker-class", "uvicorn.workers.UvicornWorker", \
     "--bind", "0.0.0.0:8000", \
     "--timeout", "120", \
     "--max-requests", "100", \
     "--max-requests-jitter", "10", \
     "--access-logfile", "-", \
     "--error-logfile", "-"]

# workers=2: 最多2个worker2GB内存限制
# timeout=120: 单个请求最多120秒OCR可能很慢
# max-requests=100: 100个请求后重启worker防止内存泄漏

SAE配置

# SAE控制台 > 应用配置 > 实例规格
CPU: 1核
内存: 2GB  # ⚠️ 不要低于2GB

# 实例数量
最小实例数: 1
最大实例数: 3根据流量自动扩容

Workers数量计算公式

# 经验公式
workers = (CPU核数 × 2) + 1

# 但对于内存密集型应用如PDF解析
workers = min((内存GB / 单worker内存GB), (CPU核数 × 2) + 1)

# 示例SAE 1核2GB
单worker内存  800MBPyMuPDF + Nougat
workers = min(2GB / 0.8GB, 1×2+1) = min(2.5, 3) = 2

# 结论workers=2 是安全值

监控OOM

# SAE控制台 > 监控 > 内存使用率
# 如果经常达到90%+,说明需要:
# 1. 减少workers从2降到1
# 2. 增加内存从2GB升到4GB
# 3. 优化代码(减少内存占用)

更新的文档

  • ⚠️ 04-Python微服务-SAE容器部署指南.md:需要在"Dockerfile"章节强调workers限制

📖 开发调试最佳实践

7. SSH隧道 - 本地直连RDS数据库

用途:开发便利性(非必需,但强烈推荐)

场景

开发人员需要用Navicat/DBeaver查看RDS数据
但RDS只允许VPC内网访问
解决通过ECS作为跳板机建立SSH隧道

操作步骤

步骤1确保ECS有SSH访问权限

# 本地生成SSH密钥如果还没有
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

# 将公钥添加到ECS
# ECS控制台 > 实例 > 远程连接 > 重置密钥对
# 或者手动添加到 ~/.ssh/authorized_keys

步骤2建立SSH隧道

# 格式
ssh -N -L 本地端口:RDS内网地址:RDS端口 root@ECS公网IP -i 密钥文件

# 示例
ssh -N -L 5433:rm-bp1xxxxx.pg.rds.aliyuncs.com:5432 \
    root@120.55.xx.xx \
    -i ~/.ssh/dify-ecs.pem

# 参数说明:
# -N: 不执行远程命令,只建立隧道
# -L: 本地端口转发
# 5433: 本地监听端口避免与本地PostgreSQL 5432冲突
# rm-bp1xxxxx...: RDS内网地址
# 5432: RDS端口

步骤3Navicat连接

连接类型PostgreSQL
主机localhost
端口5433
用户名aiclinical_rw
密码RDS密码
数据库ai_clinical_research

测试连接 → 成功!

步骤4后台运行隧道可选

# 方法1nohup后台运行
nohup ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 \
    root@ECS-IP -i key.pem > /dev/null 2>&1 &

# 方法2创建systemd服务Linux
# /etc/systemd/system/rds-tunnel.service
[Unit]
Description=SSH Tunnel to RDS
After=network.target

[Service]
Type=simple
User=your-user
ExecStart=/usr/bin/ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 root@ECS-IP -i /home/your-user/.ssh/key.pem
Restart=always

[Install]
WantedBy=multi-user.target

# 启动服务
sudo systemctl start rds-tunnel
sudo systemctl enable rds-tunnel

安全注意事项

⚠️ 不要将SSH密钥提交到Git
⚠️ 定期轮换ECS的SSH密钥
⚠️ 只在开发环境使用生产环境通过VPN访问

8. 时区统一配置 - 防止日志时间混乱

问题严重度P2重要

问题描述

不同服务的时区不一致会导致:
❌ 日志时间对不上前端14:00后端06:00
❌ pg-boss定时任务在错误时间触发
❌ 用户看到的时间戳错误
❌ 排查故障极为痛苦

解决方案

所有服务统一使用 Asia/Shanghai 时区

# backend/Dockerfile - Node.js后端
FROM node:22-alpine
RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai
# ... 其他配置

# extraction_service/Dockerfile - Python微服务
FROM python:3.11-slim
RUN apt-get update && apt-get install -y tzdata
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# ... 其他配置

# frontend-v2/Dockerfile - 前端(已配置)
FROM nginx:1.25-alpine
RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai
# ... 其他配置
-- RDS PostgreSQL 时区配置
-- RDS控制台 > 参数设置 > timezone
timezone = Asia/Shanghai

验证时区:

# 查看容器时区
docker exec <container-id> date
# 应该显示Sat Dec 14 14:30:00 CST 2024

# 查看RDS时区
psql -c "SHOW timezone;"
# 应该显示Asia/Shanghai

影响范围

  • Node.js后端需要更新Dockerfile
  • Python微服务需要更新Dockerfile
  • 前端Nginx已正确配置
  • RDS PostgreSQL需要修改参数

修复步骤

# 1. 修改Node.js后端Dockerfile
cd backend
# 在Dockerfile中添加时区配置见上方示例

# 2. 修改Python微服务Dockerfile
cd extraction_service
# 在Dockerfile中添加时区配置见上方示例

# 3. 修改RDS时区
# RDS控制台 > 参数设置 > timezone > Asia/Shanghai
# 需要重启RDS实例

# 4. 验证
docker exec backend-container date
docker exec python-container date
psql -h rds-host -c "SHOW timezone;"

9. 镜像拉取策略 - 防止代码不更新

问题严重度P2重要

问题描述

场景:
开发者修改代码 → 构建镜像 → 推送到ACR覆盖v1.0.0
→ SAE部署 → 发现代码没更新???

原因:
SAE默认镜像拉取策略可能是 IfNotPresent
如果本地已有 v1.0.0,不会重新拉取

解决方案

方案A每次部署使用新版本号强烈推荐

# 使用语义化版本号
v1.0.0 → v1.0.1 → v1.0.2 ...

# 或使用时间戳
v20251214-1430 → v20251214-1530 ...

# 或使用Git SHA
v-a1b2c3d → v-b2c3d4e ...

# 构建示例
docker build -t backend:v1.0.1 .
docker tag backend:v1.0.1 registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.1
docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.1

# SAE部署时选择新版本号

方案B配置SAE镜像拉取策略测试环境

# SAE控制台 > 应用配置 > 镜像设置
镜像拉取策略Always

# ⚠️ 注意:
# - 每次重启都会拉取镜像(启动稍慢)
# - 适合测试环境,不推荐生产环境

最佳实践

环境 推荐方案 理由
生产环境 方案A版本号管理 版本可追溯,稳定
测试环境 方案BAlways拉取 始终最新,方便
开发环境 方案A 避免混乱

不要:

# 始终使用 latest 标签(无法追溯版本)
docker tag backend:latest ...

10. Python服务内存管理 - 防止OOM

问题严重度P2重要

问题描述

Python服务PyMuPDF/Nougat内存密集容易OOM
❌ 单个PDF OCR可能占用500MB-1GB内存
❌ 多个并发请求会导致内存溢出
❌ SAE默认2GB内存可能不够

解决方案

规格建议:

场景 CPU 内存 Workers 适用情况
基础版 1核 2GB 2 简单PDF解析
标准版 2核 4GB 3 包含OCRNougat
增强版 2核 8GB 4 大量OCR + 高并发

Dockerfile优化已应用

# extraction_service/Dockerfile
CMD ["gunicorn", "main:app", \
     "--bind", "0.0.0.0:8000", \
     "--workers", "2", \              # ⚠️ 限制并发
     "--timeout", "120", \             # ⚠️ 2分钟超时
     "--max-requests", "100", \        # ⚠️ 处理100个请求后重启worker
     "--max-requests-jitter", "10"]    # ⚠️ 随机抖动,避免同时重启

监控与告警:

# SAE控制台 > 监控告警 > 创建告警规则
指标:内存使用率
阈值:> 80%
动作:发送通知 + 自动扩容(可选)

如果遇到OOM

方案1升级内存推荐

# SAE控制台 > 应用配置 > 规格调整
1核2GB → 2核4GB增加¥100/月)

方案2限制并发临时

# 修改Dockerfile
CMD ["gunicorn", "main:app", \
     "--workers", "1", \      # 降低并发
     "--threads", "2"]        # 使用线程而非进程

11. OSS签名URL - 安全的文件访问

用途:安全最佳实践

问题

如果OSS Bucket设置为Public
❌ 任何人都可以访问所有文件
❌ 无法追踪谁访问了哪些文件
❌ 无法控制访问时效

解决方案

OSS Bucket配置

# OSS控制台 > Bucket列表 > aiclinical-data-prod
# 读写权限私有Private

后端生成签名URL

// backend/src/common/storage/OSSAdapter.ts
import OSS from 'ali-oss'

export class OSSAdapter {
  private client: OSS
  
  constructor() {
    this.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!
    })
  }
  
  /**
   * 生成签名URL1小时有效期
   */
  async getSignedUrl(objectKey: string, expiresIn: number = 3600): Promise<string> {
    try {
      const url = this.client.signatureUrl(objectKey, {
        expires: expiresIn,  // 秒默认1小时
        method: 'GET'
      })
      
      return url
    } catch (error) {
      console.error('[OSS] 生成签名URL失败:', error)
      throw error
    }
  }
  
  /**
   * 上传文件
   */
  async uploadFile(objectKey: string, filePath: string): Promise<string> {
    try {
      const result = await this.client.put(objectKey, filePath)
      console.log(`[OSS] 文件上传成功: ${objectKey}`)
      
      // 返回签名URL不是公开URL
      return this.getSignedUrl(objectKey)
    } catch (error) {
      console.error('[OSS] 文件上传失败:', error)
      throw error
    }
  }
}

API返回签名URL

// backend/src/modules/pkb/documentController.ts
router.get('/documents/:id/download', async (req, res) => {
  const { id } = req.params
  
  // 1. 查询文档元数据
  const document = await prisma.document.findUnique({
    where: { id }
  })
  
  if (!document) {
    return res.status(404).json({ error: '文档不存在' })
  }
  
  // 2. 权限校验(确保用户有权访问)
  if (document.userId !== req.user.id) {
    return res.status(403).json({ error: '无权访问此文档' })
  }
  
  // 3. 生成签名URL1小时有效
  const ossAdapter = new OSSAdapter()
  const signedUrl = await ossAdapter.getSignedUrl(document.ossKey, 3600)
  
  // 4. 返回签名URL
  res.json({
    url: signedUrl,
    expiresIn: 3600,
    filename: document.filename
  })
})

前端使用签名URL

// frontend-v2/src/api/documents.ts
export async function downloadDocument(documentId: string) {
  // 1. 调用后端API获取签名URL
  const response = await fetch(`/api/v1/pkb/documents/${documentId}/download`, {
    headers: {
      'Authorization': `Bearer ${getToken()}`
    }
  })
  
  const { url, filename } = await response.json()
  
  // 2. 使用签名URL下载文件
  const link = document.createElement('a')
  link.href = url  // 签名URL1小时有效
  link.download = filename
  link.click()
}

优势

✅ 安全只有授权用户才能获取签名URL
✅ 时效URL自动过期1小时后失效
✅ 审计:可以记录谁访问了哪些文件
✅ 灵活:可以动态调整过期时间

📝 总结

必须立即修复的问题

# 问题 严重度 影响 修复时间
1 NAT网关缺失 P0 所有AI功能不可用 15分钟
2 Dify API Key死锁 P1 PKB模块不可用 10分钟分阶段部署
3 HTTP超时未配置 P1 连接泄漏,系统崩溃 5分钟代码修改
4 ECS端口对外开放 P0 安全风险,可被攻击 5分钟docker-compose修改
5 Python Workers过多 P1 OOM服务崩溃 2分钟Dockerfile修改
6 Nginx文件大小限制 P2 大文件上传失败 2分钟nginx.conf修改

推荐但非必需的优化

# 优化 价值 实施时间
7 SSH隧道 开发便利性 10分钟
8 OSS签名URL 安全最佳实践 30分钟代码修改

下一步行动

☐ 1. 创建NAT网关必需15分钟⭐⭐⭐⭐⭐
☐ 2. 修改docker-compose.yamlECS端口安全5分钟⭐⭐⭐⭐⭐
☐ 3. 修改DockerfilePython workers限制2分钟⭐⭐⭐⭐
☐ 4. 修改nginx.conf文件大小限制2分钟⭐⭐⭐⭐
☐ 5. 修改后端代码HTTP超时5分钟⭐⭐⭐⭐
☐ 6. 修改后端代码Dify容错5分钟⭐⭐⭐⭐
☐ 7. 更新部署流程(分阶段部署,文档更新)⭐⭐⭐⭐
☐ 8. 统一时区配置必需15分钟⭐⭐⭐⭐⭐
☐ 9. 配置镜像拉取策略必需5分钟⭐⭐⭐⭐⭐
☐ 10. Python内存管理必需10分钟⭐⭐⭐⭐
☐ 11. 可选配置SSH隧道开发便利10分钟
☐ 12. 可选实现OSS签名URL安全30分钟

总计必需修复约70分钟可选优化约40分钟

文档创建人: AI助手
最后更新: 2025-12-14
版本: v1.0

核心理念:安全第一、稳定第二、便利第三