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:
148
docs/01-平台基础层/02-存储服务/OSS存储架构规划与最佳实践 V5.md
Normal file
148
docs/01-平台基础层/02-存储服务/OSS存储架构规划与最佳实践 V5.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# **OSS 存储架构规划与最佳实践 (V5.0)**
|
||||
|
||||
**文档状态:** 已发布 (v5.0 \- 极简生存版)
|
||||
|
||||
**适用项目:** AIclinicalresearch
|
||||
|
||||
**核心原则:** **不做过早优化,只做必要隔离**
|
||||
|
||||
**最后更新:** 2026-01-22
|
||||
|
||||
## **1\. 架构核心决策 (The MVP Way)**
|
||||
|
||||
我们摒弃一切复杂的云原生概念,怎么简单怎么来。
|
||||
|
||||
1. **上传方式**:全线 **后端流式上传** (stream.pipeline)。前端只负责 Form Data 提交。
|
||||
2. **权限管理**:SAE 实例绑定 **AliyunOSSFullAccess**。不搞自定义策略。
|
||||
3. **成本策略**:**全标准存储**。不搞分层,不搞归档,直到月账单超过 100 元。
|
||||
|
||||
## **2\. Bucket 物理规划**
|
||||
|
||||
为了**安全底线**(防止生产数据被误删、防止患者数据裸奔),我们保留 **4 个 Bucket** 的设计。这是唯一的坚持。
|
||||
|
||||
| 环境 | Bucket 名称 (示例) | 权限 (ACL) | 说明 |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| **生产** | **ai-clinical-data** | **私有** | 核心数据。**仅配置跨域(CORS)用于GET**。 |
|
||||
| **生产** | **ai-clinical-static** | **公共读** | 头像、Logo、RAG切片图。**配置跨域(CORS)**。 |
|
||||
| **开发** | **ai-clinical-data-dev** | **私有** | 开发测试用。 |
|
||||
| **开发** | **ai-clinical-static-dev** | **公共读** | 开发测试用。 |
|
||||
|
||||
**配置项 (仅需配这 2 项)**:
|
||||
|
||||
1. **CORS**:来源 \*,允许 Methods GET。
|
||||
2. **生命周期**:仅针对 temp/ 目录设置 1 天删除。其他不用管。
|
||||
|
||||
## **3\. 极简目录结构 (Flat & Simple)**
|
||||
|
||||
基于**全员租户化**的管理逻辑,所有数据根目录统一为 tenants/。
|
||||
|
||||
后端生成 Key 的逻辑统一为:tenants/{TenantID}/{Scope}/{Module}/{UUID}.{ext}
|
||||
|
||||
### **3.1 核心数据 Bucket (ai-clinical-data)**
|
||||
|
||||
\# 1\. 用户私有数据 (User-Level Data)
|
||||
\# 逻辑:归属于特定租户下的特定用户
|
||||
\# 结构: tenants/{tenantId}/users/{userId}/{module}/{uuid}.pdf
|
||||
tenants/t999/users/u123/pkb/a1b2c3d4.pdf \# PKB 文献 (个人)
|
||||
tenants/t999/users/u123/asl/e5f6g7h8.pdf \# ASL 文献 (个人)
|
||||
tenants/t999/users/u123/rvw/i9j0k1l2.docx \# RVW 稿件 (个人)
|
||||
tenants/t999/users/u123/ssa/m3n4o5p6.xlsx \# 统计数据 (个人)
|
||||
|
||||
\# 2\. 租户共享数据 (Tenant-Level Shared Data)
|
||||
\# 逻辑:归属于租户,通常由管理员上传或全员共享
|
||||
\# 结构: tenants/{tenantId}/shared/{module}/{uuid}.pdf
|
||||
tenants/t999/shared/ekb/q7r8s9t0.pdf \# 租户知识库 (EKB)
|
||||
tenants/t999/shared/emr/u1v2w3x4.json \# 原始病历数据 (EMR)
|
||||
|
||||
\# 3\. 临时中转区 (唯一配置生命周期的目录)
|
||||
\# 结构: temp/{date}/{uuid}.xlsx
|
||||
temp/20260122/y5z6a7b8.xlsx \# Tool C 上传 / ASL 导入 / 临时导出
|
||||
\# (OSS配置: 1天后自动删除)
|
||||
|
||||
\# 4\. 系统级文件
|
||||
system/templates/gcp\_guide.pdf \# 系统模板
|
||||
|
||||
### **3.2 静态资源 Bucket (ai-clinical-static)**
|
||||
|
||||
\# 1\. RAG 切片图 (混淆文件名)
|
||||
\# 结构: rag/{uuid}.png
|
||||
rag/f9e8d7c6b5a4.png
|
||||
|
||||
\# 2\. 头像
|
||||
avatars/u123.png
|
||||
|
||||
## **4\. 后端实现指南 (Node.js)**
|
||||
|
||||
### **4.1 环境变量 (SAE)**
|
||||
|
||||
不需要复杂的 AK/SK 管理,直接用 SAE 的 RAM 角色(需绑定 AliyunOSSFullAccess)。
|
||||
|
||||
\# 只有这 3 个变量是必须的
|
||||
OSS\_REGION=oss-cn-beijing
|
||||
OSS\_ENDPOINT=oss-cn-beijing-internal.aliyuncs.com \# 内网地址,省流量费
|
||||
OSS\_BUCKET\_DATA=ai-clinical-data
|
||||
|
||||
### **4.2 核心代码 (Common Service)**
|
||||
|
||||
不要过度封装,一个简单的 StorageService 足矣。
|
||||
|
||||
// common/storage/storage.service.ts
|
||||
import OSS from 'ali-oss';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
// 使用 STS 或 SAE 自动注入的凭证,或者直接配 AK/SK (MVP阶段最简单)
|
||||
const client \= new OSS({
|
||||
region: process.env.OSS\_REGION,
|
||||
accessKeyId: process.env.OSS\_ACCESS\_KEY\_ID, // 简单起见,MVP先用 AK
|
||||
accessKeySecret: process.env.OSS\_ACCESS\_KEY\_SECRET,
|
||||
bucket: process.env.OSS\_BUCKET\_DATA,
|
||||
internal: true, // 开启内网模式
|
||||
});
|
||||
|
||||
export const StorageService \= {
|
||||
// 1\. 流式上传 (核心方法)
|
||||
async uploadStream(key: string, stream: any) {
|
||||
// usePutObject 接口支持 stream
|
||||
return await client.putStream(key, stream);
|
||||
},
|
||||
|
||||
// 2\. 获取临时链接 (私有文件预览)
|
||||
getSignedUrl(key: string) {
|
||||
return client.signatureUrl(key, { expires: 3600 });
|
||||
},
|
||||
|
||||
// 3\. 删除文件
|
||||
async delete(key: string) {
|
||||
return await client.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
### **4.3 业务调用范例**
|
||||
|
||||
// Tool C 上传接口
|
||||
fastify.post('/upload', async (req, reply) \=\> {
|
||||
const data \= await req.file();
|
||||
|
||||
// 极简 Key 生成
|
||||
const key \= \`temp/${Date.now()}/${data.filename}\`;
|
||||
|
||||
// 一行代码上传,无需 try-catch 复杂逻辑 (全局 Error Handler 会捕获)
|
||||
await StorageService.uploadStream(key, data.file);
|
||||
|
||||
return { url: key }; // 返回 Key 即可,前端不需要知道真实 URL
|
||||
});
|
||||
|
||||
## **5\. 2人团队的“生死红线”**
|
||||
|
||||
在极简模式下,只有这 3 条规则必须死守,其他都可以妥协:
|
||||
|
||||
1. **内网连接**:SAE 必须配 \-internal 的 Endpoint。一旦配成公网,上传大文件会卡死且扣费。
|
||||
2. **私有存储**:ai-clinical-data 必须是 **Private**。这是医疗底线,不能为了省代码而裸奔。
|
||||
3. **流式处理**:代码里必须用 pipe / putStream。严禁 await file.toBuffer(),否则 2 人的小服务器一定会内存溢出崩掉。
|
||||
|
||||
## **6\. 成本预警 (Cost Watch)**
|
||||
|
||||
MVP 阶段不配置复杂的归档策略,但需关注一条警戒线:
|
||||
|
||||
* **警戒线**:当 OSS 月账单超过 **100 元** (约 500GB 存储) 时。
|
||||
* **行动**:此时再去研究“生命周期管理”和“归档存储”。在此之前,专注于业务代码。
|
||||
Reference in New Issue
Block a user