import { Injectable } from "@nestjs/common"; import { getRedis } from "../../config/redis.js"; import { cacheMetrics } from "../observability/metrics.js"; const CACHE_TTL_SECONDS = 300; // 5 分钟 /** * config-service Redis 缓存服务。 * * 缓存策略: * - Plugin registry: `config:plugin:{pluginId}` → JSON PluginRegistryItem * - User layout override: `config:user-layout:{userId}` → JSON UserLayoutOverride * - TTL: 5 分钟,超时自动失效重新从 DB 加载 * - 失效:admin 修改配置或用户更新布局时主动 del * * 使用 ioredis 单例(config/redis.ts 管理),不重复创建连接。 */ @Injectable() export class ConfigCacheService { private static buildPluginKey(pluginId: string): string { return `config:plugin:${pluginId}`; } private static buildUserLayoutKey(userId: string): string { return `config:user-layout:${userId}`; } // ============ Plugin Registry ============ async getPlugin(pluginId: string): Promise { const redis = getRedis(); const raw = await redis.get(ConfigCacheService.buildPluginKey(pluginId)); if (!raw) { cacheMetrics.recordMiss("plugin"); return null; } cacheMetrics.recordHit("plugin"); return raw; } async setPlugin(pluginId: string, json: string): Promise { const redis = getRedis(); await redis.set( ConfigCacheService.buildPluginKey(pluginId), json, "EX", CACHE_TTL_SECONDS, ); } async invalidatePlugin(pluginId: string, reason = "manual"): Promise { const redis = getRedis(); await redis.del(ConfigCacheService.buildPluginKey(pluginId)); cacheMetrics.recordInvalidation("plugin", reason); } /** * 批量失效所有 plugin 缓存(admin 全量更新时调用)。 * 通过 SCAN 匹配 config:plugin:* 模式删除。 */ async invalidateAllPlugins(reason = "admin-update"): Promise { const redis = getRedis(); let cursor = "0"; do { const [next, keys] = await redis.scan( cursor, "MATCH", "config:plugin:*", "COUNT", 100, ); cursor = next; if (keys.length > 0) { await redis.del(...keys); } } while (cursor !== "0"); cacheMetrics.recordInvalidation("plugin", reason); } // ============ User Layout Override ============ async getUserLayout(userId: string): Promise { const redis = getRedis(); const raw = await redis.get(ConfigCacheService.buildUserLayoutKey(userId)); if (!raw) { cacheMetrics.recordMiss("user-layout"); return null; } cacheMetrics.recordHit("user-layout"); return raw; } async setUserLayout(userId: string, json: string): Promise { const redis = getRedis(); await redis.set( ConfigCacheService.buildUserLayoutKey(userId), json, "EX", CACHE_TTL_SECONDS, ); } async invalidateUserLayout(userId: string, reason = "manual"): Promise { const redis = getRedis(); await redis.del(ConfigCacheService.buildUserLayoutKey(userId)); cacheMetrics.recordInvalidation("user-layout", reason); } }