# **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 或 \。 * **安全**:签名 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 绝对不能开公有读,不管测试有多方便都不行。