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:
2026-01-22 22:02:20 +08:00
parent 483c62fb6f
commit 9c96f75c52
309 changed files with 4583 additions and 172 deletions

163
backend/scripts/test-oss.ts Normal file
View File

@@ -0,0 +1,163 @@
/**
* OSS存储适配器测试脚本
*
* 使用方法:
* 1. 配置环境变量(.env 文件)
* 2. 运行npx tsx scripts/test-oss.ts
*
* 测试项:
* - 上传文件(按 MVP 目录结构)
* - 下载文件
* - 检查文件存在
* - 获取签名URL
* - 【可选】删除文件
*/
import dotenv from 'dotenv'
import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'
import { randomUUID } from 'crypto'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// 加载环境变量
dotenv.config({ path: path.join(__dirname, '../.env') })
import { StorageFactory } from '../src/common/storage/StorageFactory.js'
/**
* 生成存储 Key按 MVP 目录结构)
*
* 格式tenants/{tenantId}/users/{userId}/{module}/{uuid}.{ext}
*/
function generateStorageKey(
tenantId: string,
userId: string | null,
module: string,
filename: string
): string {
const uuid = randomUUID().replace(/-/g, '').substring(0, 16)
const ext = path.extname(filename)
if (userId) {
// 用户私有数据
return `tenants/${tenantId}/users/${userId}/${module}/${uuid}${ext}`
} else {
// 租户共享数据
return `tenants/${tenantId}/shared/${module}/${uuid}${ext}`
}
}
async function main() {
console.log('='.repeat(60))
console.log('OSS 存储适配器测试 - MVP 目录结构验证')
console.log('='.repeat(60))
// 检查环境变量
const storageType = process.env.STORAGE_TYPE || 'local'
console.log(`\n📦 存储类型: ${storageType}`)
if (storageType === 'oss') {
console.log(` Region: ${process.env.OSS_REGION}`)
console.log(` Bucket: ${process.env.OSS_BUCKET}`)
console.log(` Internal: ${process.env.OSS_INTERNAL}`)
}
// 获取存储实例
const storage = StorageFactory.getInstance()
console.log(`\n✅ 存储实例创建成功`)
// ============================================================
// 测试文件:真实 PDF 文献
// ============================================================
const testPdfPath = path.join(__dirname, '../../docs/06-测试文档/Ihl 2011.pdf')
if (!fs.existsSync(testPdfPath)) {
console.error(`\n❌ 测试文件不存在: ${testPdfPath}`)
process.exit(1)
}
const pdfBuffer = fs.readFileSync(testPdfPath)
console.log(`\n📄 测试文件: Ihl 2011.pdf`)
console.log(` 大小: ${(pdfBuffer.length / 1024).toFixed(2)} KB`)
// ============================================================
// 按 MVP 目录结构生成 Key
// ============================================================
// 模拟:租户 yizhengxun用户 test-user-001模块 pkb
const tenantId = 'yizhengxun'
const userId = 'test-user-001'
const module = 'pkb'
const storageKey = generateStorageKey(tenantId, userId, module, 'Ihl 2011.pdf')
console.log(`\n📁 目录结构验证:`)
console.log(` 租户ID: ${tenantId}`)
console.log(` 用户ID: ${userId}`)
console.log(` 模块: ${module}`)
console.log(` 生成Key: ${storageKey}`)
try {
// 1. 上传测试
console.log(`\n📤 上传文件到 OSS...`)
const uploadUrl = await storage.upload(storageKey, pdfBuffer)
console.log(` ✅ 上传成功!`)
console.log(` 签名URL: ${uploadUrl.substring(0, 80)}...`)
// 2. 检查存在
console.log(`\n🔍 验证文件存在...`)
const exists = await storage.exists(storageKey)
console.log(` 存在: ${exists ? '✅ 是' : '❌ 否'}`)
// 3. 下载验证
console.log(`\n📥 下载验证...`)
const downloadBuffer = await storage.download(storageKey)
const sizeMatch = downloadBuffer.length === pdfBuffer.length
console.log(` 下载大小: ${(downloadBuffer.length / 1024).toFixed(2)} KB`)
console.log(` 大小匹配: ${sizeMatch ? '✅ 是' : '❌ 否'}`)
// 4. 获取URL不带原始文件名
console.log(`\n🔗 获取访问URL...`)
const url = storage.getUrl(storageKey)
console.log(` URL: ${url.substring(0, 80)}...`)
// 5. 获取URL带原始文件名 - 下载时恢复文件名)
console.log(`\n📎 获取带原始文件名的URL...`)
// 类型断言访问 OSSAdapter 的 getSignedUrl 方法
const ossAdapter = storage as any
if (typeof ossAdapter.getSignedUrl === 'function') {
const urlWithFilename = ossAdapter.getSignedUrl(storageKey, 3600, 'Ihl 2011.pdf')
console.log(` 原始文件名: Ihl 2011.pdf`)
console.log(` URL: ${urlWithFilename.substring(0, 80)}...`)
console.log(` ✅ 下载此URL时浏览器会保存为 "Ihl 2011.pdf"`)
}
// ============================================================
// 🔴 不删除文件!保留在 OSS 中供验证
// ============================================================
console.log(`\n⚠ 文件已保留在 OSS 中,不删除!`)
console.log(` 请登录阿里云 OSS 控制台查看:`)
console.log(` https://oss.console.aliyun.com/bucket/oss-cn-beijing/ai-clinical-data-dev/object`)
console.log(`\n 文件路径: ${storageKey}`)
// 测试完成
console.log('\n' + '='.repeat(60))
console.log('🎉 测试完成!请到 OSS 控制台验证目录结构')
console.log('='.repeat(60))
// 输出完整信息供验证
console.log(`\n📋 验证信息:`)
console.log(` Bucket: ai-clinical-data-dev`)
console.log(` Key: ${storageKey}`)
console.log(` 预期目录: tenants/yizhengxun/users/test-user-001/pkb/`)
} catch (error) {
console.error('\n❌ 测试失败:', error)
process.exit(1)
}
}
// 运行测试
main().catch(console.error)