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:
2025-11-17 08:31:23 +08:00
parent a79abf88db
commit 8bba33ac89
28 changed files with 3716 additions and 51 deletions

View 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
}
}
}

View 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. 测试:
* - 上传小文件
* - 下载文件
* - 删除文件
* - 检查文件是否存在
*/

View 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/xxxOSShttps://xxx.oss-cn-hangzhou.aliyuncs.com/xxx
*/
getUrl(key: string): string
/**
* 检查文件是否存在
* @param key 文件存储路径
* @returns 是否存在
*/
exists(key: string): Promise<boolean>
}

View 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
}
}

View 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()