feat(storage): integrate Alibaba Cloud OSS for file persistence - Add OSSAdapter and LocalAdapter with StorageFactory pattern - Integrate PKB module with OSS upload - Rename difyDocumentId to storageKey - Create 4 OSS buckets and development specification
This commit is contained in:
249
docs/01-平台基础层/02-存储服务/OSS存储架构规划与最佳实践V4.2.md
Normal file
249
docs/01-平台基础层/02-存储服务/OSS存储架构规划与最佳实践V4.2.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# **OSS 存储架构规划与最佳实践 (V4.3)**
|
||||
|
||||
**文档状态:** 已发布 (v4.3 \- 含安全红线准则)
|
||||
|
||||
**适用项目:** AIclinicalresearch
|
||||
|
||||
**核心原则:** 后端统一管控、内网高速传输、数据绝对私有
|
||||
|
||||
**最后更新:** 2026-01-21
|
||||
|
||||
## **1\. 总体架构设计**
|
||||
|
||||
鉴于我们\*\*全线采用“后端流式上传”\*\*策略,架构重心从“端到端交互”转移至“服务器端流处理”。
|
||||
|
||||
### **1.1 核心优势**
|
||||
|
||||
* **开发极简**:前端只需普通的 Form Data 上传,无需引入阿里 SDK,无需处理签名。
|
||||
* **权限收敛**:OSS 的写权限仅由后端 Node.js 掌握,前端 0 权限,杜绝 Key 泄露风险。
|
||||
* **处理原子化**:上传 OSS \+ 写入数据库 \+ 触发 AI 解析,这三个动作在后端同一个事务流程中完成,不再有“孤儿文件”问题。
|
||||
|
||||
### **1.2 Bucket 规划矩阵**
|
||||
|
||||
继续沿用 **“四 Bucket 物理隔离”** 策略。
|
||||
|
||||
| 环境 | Bucket 名称 (示例) | 权限 (ACL) | 用途 | 生命周期策略 |
|
||||
| :---- | :---- | :---- | :---- | :---- |
|
||||
| **生产 (Prod)** | **ai-clinical-data** | **私有 (Private)** | **(核心资产)** 存放病历、文献、稿件、统计结果、RAG索引。 | 智能分层 (30天转低频) |
|
||||
| **生产 (Prod)** | **ai-clinical-static** | **公共读 (Public)** | **(静态资源)** 存放系统头像、**Admin上传资源**、RAG解析图片。 | 永久保存 |
|
||||
| **开发 (Dev)** | **ai-clinical-data-dev** | **私有** | **(开发测试)** 存放开发调试产生的垃圾数据。 | **30天自动清空** (防止成本失控) |
|
||||
| **开发 (Dev)** | **ai-clinical-static-dev** | **公共读** | **(开发资源)** 存放开发环境的静态资源。 | 永久保存 |
|
||||
|
||||
### **1.3 关键配置规范 (针对后端流式优化)**
|
||||
|
||||
1. **服务端加密 (Encryption)**:所有 **Data Bucket** 必须开启 **SSE-OSS (AES256)**。
|
||||
2. **跨域 (CORS) \- 仅需读权限**:
|
||||
* **场景**:仅用于前端直接通过 URL (签名或公开) 预览/下载文件。**上传不需要 CORS**。
|
||||
* **来源**:http://localhost:3000 (Dev), https://你的生产域名.com (Prod)
|
||||
* **允许 Methods**:仅需 **GET**, **HEAD**。 (严禁开启 PUT/POST/DELETE)
|
||||
* **允许 Headers**:\*
|
||||
* **暴露 Headers**:ETag, x-oss-request-id
|
||||
3. **内网加速**:
|
||||
* **SAE 环境变量**:必须配置 OSS\_ENDPOINT 为 oss-cn-beijing-internal.aliyuncs.com。
|
||||
* **效果**:SAE \-\> OSS 流量免费且速度极快(千兆级),这是后端流式上传可行的物理基础。
|
||||
|
||||
## **2\. 目录结构设计 (Object Key)**
|
||||
|
||||
即使上传方式改变,清晰的目录结构依然是多租户隔离的基础。
|
||||
|
||||
### **2.1 核心数据目录 (ai-clinical-data / ...-dev)**
|
||||
|
||||
\# 1\. 个人用户 (To C)
|
||||
users/{userId}/pkb/{kbId}/documents/{fileId}.pdf \# PKB 原始文献
|
||||
users/{userId}/asl/projects/{projId}/pdfs/{fileId}.pdf \# ASL 文献PDF
|
||||
users/{userId}/rvw/tasks/{taskId}/source.{ext} \# RVW 原始稿件
|
||||
users/{userId}/rvw/tasks/{taskId}/report.docx \# RVW 审稿报告
|
||||
users/{userId}/dc/tasks/{taskId}/source.xlsx \# Tool B 数据清洗任务
|
||||
|
||||
\# 2\. 统计模块 (SSA/ST)
|
||||
users/{userId}/ssa/projects/{projId}/data/{fileId}.xlsx \# SSA 原始数据
|
||||
users/{userId}/ssa/projects/{projId}/outputs/{runId}/... \# SSA 结果
|
||||
users/{userId}/st/tasks/{taskId}/source.xlsx \# ST 工具数据
|
||||
users/{userId}/st/tasks/{taskId}/plot.png \# ST 结果图表 (私有\!)
|
||||
|
||||
\# 3\. 机构租户 (To B)
|
||||
tenants/{tenantId}/ekb/{kbId}/documents/{year}/{id}.pdf
|
||||
tenants/{tenantId}/asl/projects/{projId}/pdfs/{id}.pdf
|
||||
tenants/{tenantId}/emr/original/{patientId}.json
|
||||
|
||||
\# 4\. 临时数据 (自动清理)
|
||||
temp/dc/sessions/{sessionId}/source.xlsx \# Tool C 原始文件 (1天删除)
|
||||
temp/dc/sessions/{sessionId}/clean\_data.json \# Tool C 中间缓存 (1天删除)
|
||||
temp/asl/imports/{date}/{uuid}.xlsx \# ASL 导入临时表 (1天删除)
|
||||
|
||||
\# 5\. 系统级数据 (Platform Assets)
|
||||
system/kb/guidelines/{year}/{fileId}.pdf \# 系统内置知识库(GCP/法规/指南)
|
||||
system/docs/manuals/{version}/{fileId}.pdf \# 用户操作手册/帮助文档
|
||||
system/samples/datasets/{fileId}.xlsx \# 系统预置演示数据(Demo Data)
|
||||
|
||||
### **2.2 静态资源目录 (ai-clinical-static / ...-dev)**
|
||||
|
||||
\# RAG 图片 & 系统资源
|
||||
images/rag/{YYYY}/{MM}/{long-random-hash-uuid}.png \# RAG 解析插图
|
||||
assets/avatars/{userId}.png \# 用户头像
|
||||
assets/system/logo.png \# 租户 Logo
|
||||
|
||||
## **3\. 后端流式上传:统一实施方案**
|
||||
|
||||
我们放弃复杂的 STS,全平台统一使用 **Node.js Stream Pipe** 模式。
|
||||
|
||||
### **3.1 基础上传流程 (ASL / RVW / SSA / ST)**
|
||||
|
||||
适用于只需存储、后续异步处理的场景。
|
||||
|
||||
**流程:**
|
||||
|
||||
1. **前端**:FormData POST 文件到后端。
|
||||
2. **后端**:
|
||||
* 使用 @fastify/multipart 获取文件流 (part.file)。
|
||||
* **限制大小**:在 Fastify 层配置 limits: { fileSize: 100MB }。
|
||||
* **流式传输**:使用 pipeline 将流直接对接 OSS SDK 的 putStream。
|
||||
* **零内存**:文件流不经过 RAM Buffer,直接走内网 I/O。
|
||||
|
||||
**代码范例:**
|
||||
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
// Controller
|
||||
const part \= await req.file();
|
||||
const stream \= part.file;
|
||||
const key \= \`users/${userId}/asl/.../${filename}\`;
|
||||
|
||||
// Service (common/storage)
|
||||
await ossClient.putStream(key, stream);
|
||||
// 成功后写入 DB
|
||||
|
||||
### **3.2 高级流程:流式分发 (RAG / Tool C / Tool B)**
|
||||
|
||||
适用于\*\*“既要存备份,又要立即解析”\*\*的场景。这是后端流式上传的最大优势——可以“一鱼两吃”。
|
||||
|
||||
**问题**:Stream 只能被消费一次。如果发给了 OSS,Python 微服务就拿不到了。 **解决**:使用 Node.js 的 PassThrough 创建流的分支。
|
||||
|
||||
**流程:**
|
||||
|
||||
1. **前端**:上传文件。
|
||||
2. **后端 (Node.js)**:
|
||||
* 创建两个 PassThrough 流:streamA (给 OSS), streamB (给 Python)。
|
||||
* 源流 .pipe(streamA), 源流 .pipe(streamB)。
|
||||
* **并行执行**:Promise.all(\[ 上传OSS(streamA), 调用Python(streamB) \])。
|
||||
3. **结果**:文件存下了,Markdown/清洗结果也拿到了,且只消耗了一次上传带宽。
|
||||
|
||||
**代码范例:**
|
||||
|
||||
import { PassThrough } from 'stream';
|
||||
|
||||
// 1\. 创建分支
|
||||
const uploadStream \= new PassThrough();
|
||||
const processStream \= new PassThrough();
|
||||
|
||||
// 2\. 分流
|
||||
part.file.pipe(uploadStream);
|
||||
part.file.pipe(processStream);
|
||||
|
||||
// 3\. 并行处理
|
||||
await Promise.all(\[
|
||||
// 任务A:存 OSS (备份)
|
||||
storage.uploadStream(key, uploadStream),
|
||||
|
||||
// 任务B:发给 Python 解析 (业务)
|
||||
extractionService.extract(processStream)
|
||||
\]);
|
||||
|
||||
## **4\. 权限与安全管理 (后端主导)**
|
||||
|
||||
由于前端不再直接接触 OSS,安全边界收缩至后端服务器。
|
||||
|
||||
### **4.1 RAM 角色配置 (SAE 实例身份)**
|
||||
|
||||
* **不再需要**:为每个用户签发 STS Token 的复杂逻辑。
|
||||
* **只需要**:为 SAE 实例绑定一个 RAM Role。
|
||||
* **权限策略**:
|
||||
* AliyunOSSFullAccess (简单粗暴,仅限 MVP)。
|
||||
* 或者自定义策略:仅允许对 ai-clinical-\* 开头的 Bucket 进行 PutObject, GetObject, DeleteObject。
|
||||
|
||||
### **4.2 下载/预览安全 (Presigned URL)**
|
||||
|
||||
对于私有 Bucket (ai-clinical-data) 的文件,前端如何访问?
|
||||
|
||||
* **API**:GET /api/storage/url?key=...
|
||||
* **后端**:调用 ossClient.signatureUrl(key, { expires: 3600 })。
|
||||
* **前端**:拿到带签名的长 URL,直接 window.open 或 \<img src="..."\>。
|
||||
* **安全**:签名 URL 默认 1 小时过期,且无法被枚举。
|
||||
|
||||
## **5\. 生命周期与成本控制**
|
||||
|
||||
利用 OSS 规则自动兜底,防止垃圾文件堆积。
|
||||
|
||||
| Bucket | 目录前缀 | 策略 | 目的 |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| **Data (Prod)** | temp/ | **1天后删除** | **Tool C Session、ASL 导入 Excel,统统自动清理。** |
|
||||
| **Data (Prod)** | users/\*/st/ | **90天后转归档** | ST 小工具历史记录。 |
|
||||
| **Data (Prod)** | business/dc/ | **30天后转低频** | Tool B 长期任务。 |
|
||||
| **Data (Dev)** | **整个 Bucket** | **30天后删除** | 开发环境自动复位。 |
|
||||
|
||||
## **6\. 实施 Checklist (V4.3)**
|
||||
|
||||
* ![][image1]**1\. Bucket 创建与配置**
|
||||
* **![][image1]**创建 4 个 Bucket (Prod/Dev x Data/Static)。
|
||||
* ![][image1]**Prod Data Bucket**:开启 SSE-OSS 加密。
|
||||
* ![][image1]**OSS CORS**:仅配置 GET 方法 (Allow \* Headers)。
|
||||
* ![][image1]**生命周期**:配置 temp/ 1天删除规则。
|
||||
* ![][image1]**2\. 环境变量配置 (SAE & Local)**
|
||||
* **![][image1]**OSS\_ENDPOINT: 生产环境务必填 oss-cn-beijing-internal.aliyuncs.com。
|
||||
* ![][image1]OSS\_ACCESS\_KEY\_ID: 使用 SAE 绑定的 RAM Role (最佳) 或 专用 AK。
|
||||
* ![][image1]**3\. 后端代码实现**
|
||||
* **![][image1]**完善 common/storage/OssAdapter.ts:实现 uploadStream 方法。
|
||||
* ![][image1]完善 SessionService.ts (Tool C):使用 PassThrough 实现“边存边算”。
|
||||
* ![][image1]完善 ReviewService.ts (RVW):实现报告生成的流式上传。
|
||||
* ![][image1]**4\. 移除旧代码** (清理债)
|
||||
* ![][image1]删除所有 STS Token 获取接口 (get-sts-token)。
|
||||
* ![][image1]删除前端所有 ali-oss 依赖。
|
||||
|
||||
## **7\. 逆向审计:潜在风险与应对策略 (Red Teaming)**
|
||||
|
||||
从**逆向思维**角度审视“全后端流式上传”方案,梳理出物理层与逻辑层的 7 大隐患。
|
||||
|
||||
### **7.1 物理网络风险 (Physical Layer)**
|
||||
|
||||
#### **风险 1:带宽“发卡弯”效应 (The Bandwidth Hairpin)**
|
||||
|
||||
* **原理**:流量路径为 用户 \-\> SAE (公网入) \-\> OSS (内网出)。
|
||||
* **隐患**:SAE 到 OSS 虽快,但 **用户到 SAE 的入网带宽 (Ingress)** 是受限的。如果 SAE 公网带宽仅购买了 5Mbps,所有用户上传总速度将被卡死在 600KB/s。
|
||||
* **✅ 应对策略**:
|
||||
* **SAE 计费模式**:务必选择 **"按流量计费"** (Pay-By-Traffic),将带宽峰值拉高至 **100Mbps**。这样既保证了速度,又只在有上传时扣费,成本可控。
|
||||
|
||||
#### **风险 2:浏览器连接槽位耗尽 (Connection Slot Exhaustion)**
|
||||
|
||||
* **场景**:ASL 模块批量上传 100 个文件。
|
||||
* **隐患**:浏览器对同一域名的并发连接数有限制(通常 6 个)。如果前端并发 100 个请求,后续请求会处于 Pending 状态,甚至超时失败。
|
||||
* **✅ 应对策略**:
|
||||
* **前端队列**:前端上传组件必须实现队列机制(Queueing),严格控制 concurrency: 3。
|
||||
|
||||
### **7.2 Runtime 风险 (Application Layer)**
|
||||
|
||||
#### **风险 3:Node.js 流背压 (Stream Backpressure)**
|
||||
|
||||
* **场景**:使用 PassThrough 分流时,用户网速极快 (5G),但 Python 微服务处理较慢。
|
||||
* **隐患**:Node.js 会在内存中缓存积压的数据。如果文件很大且 Python 处理很慢,可能导致 Node.js 内存飙升 (OOM)。
|
||||
* **✅ 应对策略**:
|
||||
* **流式接收**:确保 Python 微服务支持流式接收 (StreamingResponse),而不是等接收完整个 Body 再处理。
|
||||
* **熔断机制**:如果 Python 响应超时,Node.js 应主动切断该分支,但不影响主上传流(OSS)。
|
||||
|
||||
#### **风险 4:大文件内存溢出 (OOM)**
|
||||
|
||||
* **场景**:开发者误用了 await part.toBuffer()。
|
||||
* **后果**:大文件直接进堆内存,服务卡死。
|
||||
* **✅ 应对策略**:
|
||||
* **Code Review**:严禁在上传接口使用 toBuffer(),必须使用 pipe。
|
||||
|
||||
### **7.3 注意事项
|
||||
|
||||
我们统一用 Node.js 的 Stream Pipeline,水管直接接通 OSS 内网,数据不进内存。代码我已经让人整理好了,就参照 common/storage 模块写,这套逻辑全平台复用。”
|
||||
|
||||
这 3 条红线,请遵守:
|
||||
|
||||
红线 1:SAE 的环境变量 OSS_ENDPOINT 必须填内网地址 (-internal),否则流量费会让你肉疼。
|
||||
|
||||
红线 2:后端代码里严禁出现 await file.toBuffer() 这种写法,必须用 stream.pipeline (新流式)。这是 2 人团队服务器不崩的关键。
|
||||
|
||||
红线 3:ai-clinical-data Bucket 绝对不能开公有读,不管测试有多方便都不行。
|
||||
Reference in New Issue
Block a user