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,76 @@
/**
* 缓存适配器接口
*
* 支持多种缓存实现:
* - MemoryCacheAdapter: 内存缓存(开发环境)
* - RedisCacheAdapter: Redis缓存生产环境
*
* 使用场景:
* - LLM响应缓存减少API调用成本
* - 数据库查询结果缓存
* - Session缓存
* - API限流计数器
*
* @example
* ```typescript
* import { cache } from '@/common/cache'
*
* // 设置缓存30分钟过期
* await cache.set('user:123', userData, 30 * 60)
*
* // 获取缓存
* const user = await cache.get<User>('user:123')
*
* // 删除缓存
* await cache.delete('user:123')
* ```
*/
export interface CacheAdapter {
/**
* 获取缓存值
* @param key 缓存键
* @returns 缓存值不存在或已过期返回null
*/
get<T = any>(key: string): Promise<T | null>
/**
* 设置缓存值
* @param key 缓存键
* @param value 缓存值会自动序列化为JSON
* @param ttl 过期时间(秒),不传则永不过期
*/
set(key: string, value: any, ttl?: number): Promise<void>
/**
* 删除缓存
* @param key 缓存键
*/
delete(key: string): Promise<void>
/**
* 清空所有缓存
* ⚠️ 慎用,生产环境可能影响其他应用
*/
clear(): Promise<void>
/**
* 检查缓存是否存在
* @param key 缓存键
*/
has(key: string): Promise<boolean>
/**
* 批量获取缓存
* @param keys 缓存键数组
* @returns 缓存值数组按keys顺序不存在则为null
*/
mget<T = any>(keys: string[]): Promise<(T | null)[]>
/**
* 批量设置缓存
* @param entries 键值对数组
* @param ttl 过期时间(秒)
*/
mset(entries: Array<{ key: string; value: any }>, ttl?: number): Promise<void>
}

View File

@@ -0,0 +1,99 @@
import { CacheAdapter } from './CacheAdapter.js'
import { MemoryCacheAdapter } from './MemoryCacheAdapter.js'
import { RedisCacheAdapter } from './RedisCacheAdapter.js'
/**
* 缓存工厂类
*
* 根据环境变量自动选择缓存实现:
* - CACHE_TYPE=memory: 使用MemoryCacheAdapter内存缓存
* - CACHE_TYPE=redis: 使用RedisCacheAdapterRedis缓存
*
* 零代码切换:
* - 本地开发不配置CACHE_TYPE默认使用memory
* - 云端部署配置CACHE_TYPE=redis自动切换到Redis
*
* @example
* ```typescript
* import { cache } from '@/common/cache'
*
* // 业务代码不关心是memory还是redis
* await cache.set('user:123', userData, 60)
* const user = await cache.get('user:123')
* ```
*/
export class CacheFactory {
private static instance: CacheAdapter | null = null
/**
* 获取缓存适配器实例(单例模式)
*/
static getInstance(): CacheAdapter {
if (!this.instance) {
this.instance = this.createAdapter()
}
return this.instance
}
/**
* 创建缓存适配器
*/
private static createAdapter(): CacheAdapter {
const cacheType = process.env.CACHE_TYPE || 'memory'
switch (cacheType) {
case 'memory':
return this.createMemoryAdapter()
case 'redis':
return this.createRedisAdapter()
default:
console.warn(`[CacheFactory] Unknown CACHE_TYPE: ${cacheType}, fallback to memory`)
return this.createMemoryAdapter()
}
}
/**
* 创建内存缓存适配器
*/
private static createMemoryAdapter(): MemoryCacheAdapter {
console.log('[CacheFactory] Using MemoryCacheAdapter')
return new MemoryCacheAdapter()
}
/**
* 创建Redis缓存适配器
*/
private static createRedisAdapter(): RedisCacheAdapter {
const host = process.env.REDIS_HOST
const port = parseInt(process.env.REDIS_PORT || '6379', 10)
const password = process.env.REDIS_PASSWORD
const db = parseInt(process.env.REDIS_DB || '0', 10)
// 验证必需的环境变量
if (!host) {
throw new Error(
'[CacheFactory] Redis configuration incomplete. REDIS_HOST is required when CACHE_TYPE=redis'
)
}
console.log(`[CacheFactory] Using RedisCacheAdapter (host: ${host}:${port}, db: ${db})`)
return new RedisCacheAdapter({
host,
port,
password,
db,
keyPrefix: 'aiclinical:'
})
}
/**
* 重置实例(用于测试)
*/
static reset(): void {
this.instance = null
}
}

View File

@@ -0,0 +1,180 @@
import { CacheAdapter } from './CacheAdapter.js'
/**
* 缓存条目
*/
interface CacheEntry {
value: any
expiresAt: number | null // null表示永不过期
}
/**
* 内存缓存适配器
*
* 适用场景:
* - 本地开发环境
* - 单实例部署
* - 非关键缓存数据
*
* 特点:
* - ✅ 简单易用,无需外部依赖
* - ✅ 性能极高
* - ⚠️ 进程重启后数据丢失
* - ⚠️ 不支持多实例共享
* - ⚠️ 内存占用需要控制
*
* @example
* ```typescript
* const cache = new MemoryCacheAdapter()
* await cache.set('key', 'value', 60) // 60秒过期
* const value = await cache.get('key')
* ```
*/
export class MemoryCacheAdapter implements CacheAdapter {
private cache: Map<string, CacheEntry> = new Map()
private cleanupTimer: NodeJS.Timeout | null = null
constructor() {
// 每分钟清理一次过期缓存
this.startCleanupTimer()
}
/**
* 启动定期清理过期缓存
*/
private startCleanupTimer(): void {
if (process.env.NODE_ENV !== 'test') {
this.cleanupTimer = setInterval(() => {
this.cleanupExpired()
}, 60 * 1000) // 每分钟
}
}
/**
* 停止清理定时器
*/
destroy(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer)
this.cleanupTimer = null
}
}
/**
* 清理过期缓存
*/
private cleanupExpired(): void {
const now = Date.now()
let removed = 0
for (const [key, entry] of this.cache) {
if (entry.expiresAt !== null && entry.expiresAt <= now) {
this.cache.delete(key)
removed++
}
}
if (removed > 0) {
console.log(`[MemoryCacheAdapter] Cleanup: removed ${removed} expired entries`)
}
}
/**
* 检查条目是否过期
*/
private isExpired(entry: CacheEntry): boolean {
if (entry.expiresAt === null) return false
return entry.expiresAt <= Date.now()
}
/**
* 获取缓存值
*/
async get<T = any>(key: string): Promise<T | null> {
const entry = this.cache.get(key)
if (!entry) return null
// 检查是否过期
if (this.isExpired(entry)) {
this.cache.delete(key)
return null
}
return entry.value as T
}
/**
* 设置缓存值
*/
async set(key: string, value: any, ttl?: number): Promise<void> {
const entry: CacheEntry = {
value,
expiresAt: ttl ? Date.now() + ttl * 1000 : null
}
this.cache.set(key, entry)
}
/**
* 删除缓存
*/
async delete(key: string): Promise<void> {
this.cache.delete(key)
}
/**
* 清空所有缓存
*/
async clear(): Promise<void> {
this.cache.clear()
console.log('[MemoryCacheAdapter] Cache cleared')
}
/**
* 检查缓存是否存在
*/
async has(key: string): Promise<boolean> {
const entry = this.cache.get(key)
if (!entry) return false
if (this.isExpired(entry)) {
this.cache.delete(key)
return false
}
return true
}
/**
* 批量获取缓存
*/
async mget<T = any>(keys: string[]): Promise<(T | null)[]> {
return Promise.all(keys.map(key => this.get<T>(key)))
}
/**
* 批量设置缓存
*/
async mset(entries: Array<{ key: string; value: any }>, ttl?: number): Promise<void> {
await Promise.all(entries.map(({ key, value }) => this.set(key, value, ttl)))
}
/**
* 获取缓存统计信息
*/
getStats() {
let expired = 0
for (const entry of this.cache.values()) {
if (this.isExpired(entry)) {
expired++
}
}
return {
total: this.cache.size,
active: this.cache.size - expired,
expired
}
}
}

View File

@@ -0,0 +1,211 @@
import { CacheAdapter } from './CacheAdapter.js'
// import Redis from 'ioredis' // ⚠️ 需要安装npm install ioredis
/**
* Redis缓存适配器
*
* 适用场景:
* - 云端SaaS部署多实例共享
* - 需要持久化的缓存
* - 高并发场景
*
* 配置要求:
* - REDIS_HOST: Redis主机r-***.redis.aliyuncs.com
* - REDIS_PORT: Redis端口默认6379
* - REDIS_PASSWORD: Redis密码可选
* - REDIS_DB: Redis数据库索引默认0
*
* @example
* ```typescript
* const cache = new RedisCacheAdapter({
* host: 'localhost',
* port: 6379,
* password: 'your-password'
* })
* await cache.set('key', 'value', 60)
* ```
*
* ⚠️ 当前为预留实现,待云端部署时完善
*/
export class RedisCacheAdapter implements CacheAdapter {
// private readonly client: Redis
private readonly keyPrefix: string
constructor(config: {
host: string
port: number
password?: string
db?: number
keyPrefix?: string
}) {
this.keyPrefix = config.keyPrefix || 'aiclinical:'
// ⚠️ TODO: 待安装 ioredis 后取消注释
// this.client = new Redis({
// host: config.host,
// port: config.port,
// password: config.password,
// db: config.db || 0,
// retryStrategy: (times) => {
// const delay = Math.min(times * 50, 2000)
// return delay
// }
// })
// this.client.on('error', (err) => {
// console.error('[RedisCacheAdapter] Redis error:', err)
// })
// this.client.on('connect', () => {
// console.log('[RedisCacheAdapter] Connected to Redis')
// })
}
/**
* 获取完整的key带前缀
*/
private getFullKey(_key: string): string {
return `${this.keyPrefix}${_key}`
}
/**
* 获取缓存值
*/
async get<T = any>(_key: string): Promise<T | null> {
// ⚠️ TODO: 待实现
// const value = await this.client.get(this.getFullKey(key))
// if (!value) return null
// try {
// return JSON.parse(value) as T
// } catch {
// return value as T
// }
throw new Error('[RedisCacheAdapter] Not implemented yet. Please install ioredis and configure Redis.')
}
/**
* 设置缓存值
*/
async set(_key: string, _value: any, _ttl?: number): Promise<void> {
// ⚠️ TODO: 待实现
// const serialized = JSON.stringify(value)
// const fullKey = this.getFullKey(key)
// if (ttl) {
// await this.client.setex(fullKey, ttl, serialized)
// } else {
// await this.client.set(fullKey, serialized)
// }
throw new Error('[RedisCacheAdapter] Not implemented yet. Please install ioredis and configure Redis.')
}
/**
* 删除缓存
*/
async delete(_key: string): Promise<void> {
// ⚠️ TODO: 待实现
// await this.client.del(this.getFullKey(key))
throw new Error('[RedisCacheAdapter] Not implemented yet. Please install ioredis and configure Redis.')
}
/**
* 清空所有缓存仅清空带前缀的key
*/
async clear(): Promise<void> {
// ⚠️ TODO: 待实现
// const keys = await this.client.keys(`${this.keyPrefix}*`)
// if (keys.length > 0) {
// await this.client.del(...keys)
// }
throw new Error('[RedisCacheAdapter] Not implemented yet. Please install ioredis and configure Redis.')
}
/**
* 检查缓存是否存在
*/
async has(_key: string): Promise<boolean> {
// ⚠️ TODO: 待实现
// const exists = await this.client.exists(this.getFullKey(key))
// return exists === 1
throw new Error('[RedisCacheAdapter] Not implemented yet. Please install ioredis and configure Redis.')
}
/**
* 批量获取缓存
*/
async mget<T = any>(_keys: string[]): Promise<(T | null)[]> {
// ⚠️ TODO: 待实现
// const fullKeys = keys.map(k => this.getFullKey(k))
// const values = await this.client.mget(...fullKeys)
// return values.map(v => {
// if (!v) return null
// try {
// return JSON.parse(v) as T
// } catch {
// return v as T
// }
// })
throw new Error('[RedisCacheAdapter] Not implemented yet. Please install ioredis and configure Redis.')
}
/**
* 批量设置缓存
*/
async mset(_entries: Array<{ key: string; value: any }>, _ttl?: number): Promise<void> {
// ⚠️ TODO: 待实现
// if (ttl) {
// // 有TTL时需要单独设置每个key
// await Promise.all(entries.map(({ key, value }) => this.set(key, value, ttl)))
// } else {
// // 无TTL时可以批量设置
// const pairs: string[] = []
// for (const { key, value } of entries) {
// pairs.push(this.getFullKey(key), JSON.stringify(value))
// }
// await this.client.mset(...pairs)
// }
throw new Error('[RedisCacheAdapter] Not implemented yet. Please install ioredis and configure Redis.')
}
/**
* 关闭Redis连接
*/
async disconnect(): Promise<void> {
// ⚠️ TODO: 待实现
// await this.client.quit()
}
}
/**
* ⚠️ 实施说明:
*
* 1. 安装依赖:
* npm install ioredis
* npm install -D @types/ioredis
*
* 2. 取消注释代码:
* - import Redis from 'ioredis'
* - new Redis({ ... })
* - 所有方法的实现代码
*
* 3. 配置环境变量:
* CACHE_TYPE=redis
* REDIS_HOST=r-***.redis.aliyuncs.com
* REDIS_PORT=6379
* REDIS_PASSWORD=your-password
* REDIS_DB=0
*
* 4. 测试:
* - 连接Redis
* - 设置/获取缓存
* - 批量操作
* - TTL过期
*/

51
backend/src/common/cache/index.ts vendored Normal file
View File

@@ -0,0 +1,51 @@
/**
* 缓存服务统一导出
*
* 提供平台级的缓存能力支持内存和Redis无缝切换。
*
* @module cache
*
* @example
* ```typescript
* // 方式1使用全局缓存实例推荐
* import { cache } from '@/common/cache'
*
* // 缓存用户数据
* await cache.set('user:123', { id: 123, name: 'Alice' }, 60 * 5) // 5分钟
* const user = await cache.get<User>('user:123')
*
* // 缓存LLM响应
* const cacheKey = `llm:${model}:${hash(prompt)}`
* const cached = await cache.get(cacheKey)
* if (cached) return cached
*
* const response = await llm.chat(prompt)
* await cache.set(cacheKey, response, 60 * 60) // 1小时
*
* // 方式2直接使用适配器
* import { MemoryCacheAdapter } from '@/common/cache'
* const cache = new MemoryCacheAdapter()
*
* // 方式3使用工厂
* import { CacheFactory } from '@/common/cache'
* const cache = CacheFactory.getInstance()
* ```
*/
export type { CacheAdapter } from './CacheAdapter.js'
export { MemoryCacheAdapter } from './MemoryCacheAdapter.js'
export { RedisCacheAdapter } from './RedisCacheAdapter.js'
export { CacheFactory } from './CacheFactory.js'
// Import for usage below
import { CacheFactory } from './CacheFactory.js'
/**
* 全局缓存实例(推荐使用)
*
* 自动根据环境变量选择缓存实现:
* - CACHE_TYPE=memory: 内存缓存(本地开发)
* - CACHE_TYPE=redis: Redis缓存生产环境
*/
export const cache = CacheFactory.getInstance()