feat(platform): Implement platform infrastructure with cloud-native support
- Add storage service (LocalAdapter + OSSAdapter stub) - Add database connection pool with graceful shutdown - Add logging system with winston (JSON format) - Add environment config management - Add async job queue (MemoryQueue + DatabaseQueue stub) - Add cache service (MemoryCache + RedisCache stub) - Add health check endpoints for SAE - Add monitoring metrics for DB, memory, API Key Features: - Zero-code switching between local and cloud environments - Adapter pattern for multi-environment support - Backward compatible with legacy modules - Ready for Aliyun Serverless deployment Related: Platform Infrastructure Planning (docs/09-鏋舵瀯瀹炴柦/04-骞冲彴鍩虹璁炬柦瑙勫垝.md)
This commit is contained in:
151
backend/src/common/storage/LocalAdapter.ts
Normal file
151
backend/src/common/storage/LocalAdapter.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { StorageAdapter } from './StorageAdapter.js'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { existsSync } from 'fs'
|
||||
|
||||
/**
|
||||
* 本地文件系统适配器
|
||||
*
|
||||
* 适用场景:
|
||||
* - 本地开发环境
|
||||
* - 私有化部署(数据不出内网)
|
||||
* - 单机版(100%本地化)
|
||||
*
|
||||
* 存储结构:
|
||||
* - 基础路径:backend/uploads/
|
||||
* - 示例:backend/uploads/literature/123.pdf
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const adapter = new LocalAdapter('/app/uploads')
|
||||
* await adapter.upload('literature/123.pdf', buffer)
|
||||
* ```
|
||||
*/
|
||||
export class LocalAdapter implements StorageAdapter {
|
||||
private readonly baseDir: string
|
||||
private readonly baseUrl: string
|
||||
|
||||
/**
|
||||
* @param baseDir 本地存储基础目录(绝对路径或相对路径)
|
||||
* @param baseUrl 访问URL前缀(默认:http://localhost:3001/uploads)
|
||||
*/
|
||||
constructor(
|
||||
baseDir: string = path.join(process.cwd(), 'uploads'),
|
||||
baseUrl: string = process.env.LOCAL_STORAGE_URL || 'http://localhost:3001/uploads'
|
||||
) {
|
||||
this.baseDir = baseDir
|
||||
this.baseUrl = baseUrl.replace(/\/$/, '') // 移除末尾的斜杠
|
||||
|
||||
// 确保基础目录存在
|
||||
this.ensureBaseDir()
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保基础目录存在
|
||||
*/
|
||||
private async ensureBaseDir(): Promise<void> {
|
||||
try {
|
||||
await fs.mkdir(this.baseDir, { recursive: true })
|
||||
} catch (error) {
|
||||
console.error(`[LocalAdapter] Failed to create base dir: ${this.baseDir}`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保文件所在目录存在
|
||||
*/
|
||||
private async ensureDir(filePath: string): Promise<void> {
|
||||
const dir = path.dirname(filePath)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的文件路径
|
||||
*/
|
||||
private getFullPath(key: string): string {
|
||||
// 规范化路径,移除开头的斜杠
|
||||
const normalizedKey = key.replace(/^\/+/, '')
|
||||
return path.join(this.baseDir, normalizedKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
async upload(key: string, buffer: Buffer): Promise<string> {
|
||||
try {
|
||||
const fullPath = this.getFullPath(key)
|
||||
|
||||
// 确保目录存在
|
||||
await this.ensureDir(fullPath)
|
||||
|
||||
// 写入文件
|
||||
await fs.writeFile(fullPath, buffer)
|
||||
|
||||
// 返回访问URL
|
||||
return this.getUrl(key)
|
||||
} catch (error) {
|
||||
console.error(`[LocalAdapter] Failed to upload file: ${key}`, error)
|
||||
throw new Error(`Failed to upload file: ${key}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
async download(key: string): Promise<Buffer> {
|
||||
try {
|
||||
const fullPath = this.getFullPath(key)
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!existsSync(fullPath)) {
|
||||
throw new Error(`File not found: ${key}`)
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
return await fs.readFile(fullPath)
|
||||
} catch (error) {
|
||||
console.error(`[LocalAdapter] Failed to download file: ${key}`, error)
|
||||
throw new Error(`Failed to download file: ${key}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
async delete(key: string): Promise<void> {
|
||||
try {
|
||||
const fullPath = this.getFullPath(key)
|
||||
|
||||
// 检查文件是否存在
|
||||
if (existsSync(fullPath)) {
|
||||
await fs.unlink(fullPath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[LocalAdapter] Failed to delete file: ${key}`, error)
|
||||
throw new Error(`Failed to delete file: ${key}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件访问URL
|
||||
*/
|
||||
getUrl(key: string): string {
|
||||
// 规范化路径,确保开头有斜杠
|
||||
const normalizedKey = key.replace(/^\/+/, '')
|
||||
return `${this.baseUrl}/${normalizedKey}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
async exists(key: string): Promise<boolean> {
|
||||
try {
|
||||
const fullPath = this.getFullPath(key)
|
||||
return existsSync(fullPath)
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
135
backend/src/common/storage/OSSAdapter.ts
Normal file
135
backend/src/common/storage/OSSAdapter.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { StorageAdapter } from './StorageAdapter.js'
|
||||
// import OSS from 'ali-oss' // ⚠️ 需要安装:npm install ali-oss
|
||||
|
||||
/**
|
||||
* 阿里云OSS适配器
|
||||
*
|
||||
* 适用场景:
|
||||
* - 云端SaaS部署(阿里云Serverless)
|
||||
* - 高可用、高并发场景
|
||||
* - 需要CDN加速
|
||||
*
|
||||
* 配置要求:
|
||||
* - OSS_REGION: OSS地域(如:oss-cn-hangzhou)
|
||||
* - OSS_BUCKET: OSS Bucket名称
|
||||
* - OSS_ACCESS_KEY_ID: AccessKey ID
|
||||
* - OSS_ACCESS_KEY_SECRET: AccessKey Secret
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const adapter = new OSSAdapter({
|
||||
* region: 'oss-cn-hangzhou',
|
||||
* bucket: 'aiclinical-prod',
|
||||
* accessKeyId: process.env.OSS_ACCESS_KEY_ID!,
|
||||
* accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!
|
||||
* })
|
||||
* await adapter.upload('literature/123.pdf', buffer)
|
||||
* ```
|
||||
*
|
||||
* ⚠️ 当前为预留实现,待云端部署时完善
|
||||
*/
|
||||
export class OSSAdapter implements StorageAdapter {
|
||||
// private readonly client: OSS
|
||||
private readonly bucket: string
|
||||
private readonly region: string
|
||||
|
||||
constructor(config: {
|
||||
region: string
|
||||
bucket: string
|
||||
accessKeyId: string
|
||||
accessKeySecret: string
|
||||
}) {
|
||||
this.region = config.region
|
||||
this.bucket = config.bucket
|
||||
|
||||
// ⚠️ TODO: 待安装 ali-oss 后取消注释
|
||||
// this.client = new OSS({
|
||||
// region: config.region,
|
||||
// bucket: config.bucket,
|
||||
// accessKeyId: config.accessKeyId,
|
||||
// accessKeySecret: config.accessKeySecret
|
||||
// })
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到OSS
|
||||
*/
|
||||
async upload(_key: string, _buffer: Buffer): Promise<string> {
|
||||
// ⚠️ TODO: 待实现
|
||||
// const result = await this.client.put(key, buffer)
|
||||
// return result.url
|
||||
|
||||
throw new Error('[OSSAdapter] Not implemented yet. Please install ali-oss and configure OSS.')
|
||||
}
|
||||
|
||||
/**
|
||||
* 从OSS下载文件
|
||||
*/
|
||||
async download(_key: string): Promise<Buffer> {
|
||||
// ⚠️ TODO: 待实现
|
||||
// const result = await this.client.get(key)
|
||||
// return result.content as Buffer
|
||||
|
||||
throw new Error('[OSSAdapter] Not implemented yet. Please install ali-oss and configure OSS.')
|
||||
}
|
||||
|
||||
/**
|
||||
* 从OSS删除文件
|
||||
*/
|
||||
async delete(_key: string): Promise<void> {
|
||||
// ⚠️ TODO: 待实现
|
||||
// await this.client.delete(key)
|
||||
|
||||
throw new Error('[OSSAdapter] Not implemented yet. Please install ali-oss and configure OSS.')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件访问URL
|
||||
*/
|
||||
getUrl(key: string): string {
|
||||
// 返回OSS公开访问URL
|
||||
// 格式:https://{bucket}.{region}.aliyuncs.com/{key}
|
||||
return `https://${this.bucket}.${this.region}.aliyuncs.com/${key}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
async exists(_key: string): Promise<boolean> {
|
||||
// ⚠️ TODO: 待实现
|
||||
// try {
|
||||
// await this.client.head(key)
|
||||
// return true
|
||||
// } catch (error) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
throw new Error('[OSSAdapter] Not implemented yet. Please install ali-oss and configure OSS.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ 实施说明:
|
||||
*
|
||||
* 1. 安装依赖:
|
||||
* npm install ali-oss
|
||||
* npm install -D @types/ali-oss
|
||||
*
|
||||
* 2. 取消注释代码:
|
||||
* - import OSS from 'ali-oss'
|
||||
* - new OSS({ ... })
|
||||
* - 所有方法的实现代码
|
||||
*
|
||||
* 3. 配置环境变量:
|
||||
* OSS_REGION=oss-cn-hangzhou
|
||||
* OSS_BUCKET=aiclinical-prod
|
||||
* OSS_ACCESS_KEY_ID=your-access-key-id
|
||||
* OSS_ACCESS_KEY_SECRET=your-access-key-secret
|
||||
*
|
||||
* 4. 测试:
|
||||
* - 上传小文件
|
||||
* - 下载文件
|
||||
* - 删除文件
|
||||
* - 检查文件是否存在
|
||||
*/
|
||||
|
||||
66
backend/src/common/storage/StorageAdapter.ts
Normal file
66
backend/src/common/storage/StorageAdapter.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 存储适配器接口
|
||||
*
|
||||
* 支持多种存储实现:
|
||||
* - LocalAdapter: 本地文件系统(开发环境)
|
||||
* - OSSAdapter: 阿里云OSS(生产环境)
|
||||
*
|
||||
* 使用场景:
|
||||
* - 上传PDF文献文件
|
||||
* - 上传Excel批量导入文件
|
||||
* - 上传用户头像等静态资源
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { storage } from '@/common/storage'
|
||||
*
|
||||
* // 上传文件
|
||||
* const url = await storage.upload('literature/123.pdf', buffer)
|
||||
*
|
||||
* // 下载文件
|
||||
* const buffer = await storage.download('literature/123.pdf')
|
||||
*
|
||||
* // 删除文件
|
||||
* await storage.delete('literature/123.pdf')
|
||||
*
|
||||
* // 获取URL
|
||||
* const url = storage.getUrl('literature/123.pdf')
|
||||
* ```
|
||||
*/
|
||||
export interface StorageAdapter {
|
||||
/**
|
||||
* 上传文件
|
||||
* @param key 文件存储路径(相对路径,如:literature/123.pdf)
|
||||
* @param buffer 文件内容(二进制数据)
|
||||
* @returns 文件访问URL
|
||||
*/
|
||||
upload(key: string, buffer: Buffer): Promise<string>
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param key 文件存储路径
|
||||
* @returns 文件内容(二进制数据)
|
||||
*/
|
||||
download(key: string): Promise<Buffer>
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param key 文件存储路径
|
||||
*/
|
||||
delete(key: string): Promise<void>
|
||||
|
||||
/**
|
||||
* 获取文件访问URL
|
||||
* @param key 文件存储路径
|
||||
* @returns 文件访问URL(本地:http://localhost:3001/uploads/xxx,OSS:https://xxx.oss-cn-hangzhou.aliyuncs.com/xxx)
|
||||
*/
|
||||
getUrl(key: string): string
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
* @param key 文件存储路径
|
||||
* @returns 是否存在
|
||||
*/
|
||||
exists(key: string): Promise<boolean>
|
||||
}
|
||||
|
||||
101
backend/src/common/storage/StorageFactory.ts
Normal file
101
backend/src/common/storage/StorageFactory.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { StorageAdapter } from './StorageAdapter.js'
|
||||
import { LocalAdapter } from './LocalAdapter.js'
|
||||
import { OSSAdapter } from './OSSAdapter.js'
|
||||
|
||||
/**
|
||||
* 存储工厂类
|
||||
*
|
||||
* 根据环境变量自动选择存储实现:
|
||||
* - STORAGE_TYPE=local: 使用LocalAdapter(本地文件系统)
|
||||
* - STORAGE_TYPE=oss: 使用OSSAdapter(阿里云OSS)
|
||||
*
|
||||
* 零代码切换:
|
||||
* - 本地开发:不配置STORAGE_TYPE,默认使用local
|
||||
* - 云端部署:配置STORAGE_TYPE=oss,自动切换到OSS
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { storage } from '@/common/storage'
|
||||
*
|
||||
* // 业务代码不关心是local还是oss
|
||||
* const url = await storage.upload('literature/123.pdf', buffer)
|
||||
* ```
|
||||
*/
|
||||
export class StorageFactory {
|
||||
private static instance: StorageAdapter | null = null
|
||||
|
||||
/**
|
||||
* 获取存储适配器实例(单例模式)
|
||||
*/
|
||||
static getInstance(): StorageAdapter {
|
||||
if (!this.instance) {
|
||||
this.instance = this.createAdapter()
|
||||
}
|
||||
return this.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建存储适配器
|
||||
*/
|
||||
private static createAdapter(): StorageAdapter {
|
||||
const storageType = process.env.STORAGE_TYPE || 'local'
|
||||
|
||||
switch (storageType) {
|
||||
case 'local':
|
||||
return this.createLocalAdapter()
|
||||
|
||||
case 'oss':
|
||||
return this.createOSSAdapter()
|
||||
|
||||
default:
|
||||
console.warn(`[StorageFactory] Unknown STORAGE_TYPE: ${storageType}, fallback to local`)
|
||||
return this.createLocalAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建本地适配器
|
||||
*/
|
||||
private static createLocalAdapter(): LocalAdapter {
|
||||
const baseDir = process.env.LOCAL_STORAGE_DIR || 'uploads'
|
||||
const baseUrl = process.env.LOCAL_STORAGE_URL || 'http://localhost:3001/uploads'
|
||||
|
||||
console.log(`[StorageFactory] Using LocalAdapter (baseDir: ${baseDir})`)
|
||||
|
||||
return new LocalAdapter(baseDir, baseUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建OSS适配器
|
||||
*/
|
||||
private static createOSSAdapter(): OSSAdapter {
|
||||
const region = process.env.OSS_REGION
|
||||
const bucket = process.env.OSS_BUCKET
|
||||
const accessKeyId = process.env.OSS_ACCESS_KEY_ID
|
||||
const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET
|
||||
|
||||
// 验证必需的环境变量
|
||||
if (!region || !bucket || !accessKeyId || !accessKeySecret) {
|
||||
throw new Error(
|
||||
'[StorageFactory] OSS configuration incomplete. Required: OSS_REGION, OSS_BUCKET, OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`[StorageFactory] Using OSSAdapter (region: ${region}, bucket: ${bucket})`)
|
||||
|
||||
return new OSSAdapter({
|
||||
region,
|
||||
bucket,
|
||||
accessKeyId,
|
||||
accessKeySecret
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置实例(用于测试)
|
||||
*/
|
||||
static reset(): void {
|
||||
this.instance = null
|
||||
}
|
||||
}
|
||||
|
||||
42
backend/src/common/storage/index.ts
Normal file
42
backend/src/common/storage/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 存储服务统一导出
|
||||
*
|
||||
* 提供平台级的文件存储能力,支持本地和云端无缝切换。
|
||||
*
|
||||
* @module storage
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 方式1:使用单例(推荐)
|
||||
* import { storage } from '@/common/storage'
|
||||
* const url = await storage.upload('literature/123.pdf', buffer)
|
||||
*
|
||||
* // 方式2:直接使用适配器
|
||||
* import { LocalAdapter } from '@/common/storage'
|
||||
* const adapter = new LocalAdapter()
|
||||
* const url = await adapter.upload('literature/123.pdf', buffer)
|
||||
*
|
||||
* // 方式3:使用工厂
|
||||
* import { StorageFactory } from '@/common/storage'
|
||||
* const storage = StorageFactory.getInstance()
|
||||
* const url = await storage.upload('literature/123.pdf', buffer)
|
||||
* ```
|
||||
*/
|
||||
|
||||
export { StorageAdapter } from './StorageAdapter.js'
|
||||
export { LocalAdapter } from './LocalAdapter.js'
|
||||
export { OSSAdapter } from './OSSAdapter.js'
|
||||
export { StorageFactory } from './StorageFactory.js'
|
||||
|
||||
// Import for usage below
|
||||
import { StorageFactory } from './StorageFactory.js'
|
||||
|
||||
/**
|
||||
* 全局存储实例(推荐使用)
|
||||
*
|
||||
* 自动根据环境变量选择存储实现:
|
||||
* - STORAGE_TYPE=local: 本地文件系统
|
||||
* - STORAGE_TYPE=oss: 阿里云OSS
|
||||
*/
|
||||
export const storage = StorageFactory.getInstance()
|
||||
|
||||
Reference in New Issue
Block a user