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:
76
backend/src/common/cache/CacheAdapter.ts
vendored
Normal file
76
backend/src/common/cache/CacheAdapter.ts
vendored
Normal 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>
|
||||
}
|
||||
|
||||
99
backend/src/common/cache/CacheFactory.ts
vendored
Normal file
99
backend/src/common/cache/CacheFactory.ts
vendored
Normal 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: 使用RedisCacheAdapter(Redis缓存)
|
||||
*
|
||||
* 零代码切换:
|
||||
* - 本地开发:不配置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
|
||||
}
|
||||
}
|
||||
|
||||
180
backend/src/common/cache/MemoryCacheAdapter.ts
vendored
Normal file
180
backend/src/common/cache/MemoryCacheAdapter.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
211
backend/src/common/cache/RedisCacheAdapter.ts
vendored
Normal file
211
backend/src/common/cache/RedisCacheAdapter.ts
vendored
Normal 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
51
backend/src/common/cache/index.ts
vendored
Normal 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()
|
||||
|
||||
Reference in New Issue
Block a user