Files
Edu/services/config-service/src/shared/cache/config-cache.service.ts
SpecialX 1a5fa78fa6 feat(config-service): split config-service from iam for plugin/layout config
- new NestJS service on port 3011/gRPC 50059 (ADR-026)
- owns 6 config_ tables (plugin/role-mapping/role-layout/layout-tpl/user-override/outbox)
- GraphQL Federation 2 subgraph with DataLoader + RouterAuthGuard
- gRPC ConfigService + admin REST CRUD + user REST API
- three-layer merge: registry.defaultProps + roleMapping.widget_props + userOverride.props
- Redis cache with 5min TTL
- registered in apollo-router supergraph + docker-compose + port-allocation

Implements M3 of v2.1 migration plan.
2026-07-15 02:13:03 +08:00

109 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string | null> {
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<void> {
const redis = getRedis();
await redis.set(
ConfigCacheService.buildPluginKey(pluginId),
json,
"EX",
CACHE_TTL_SECONDS,
);
}
async invalidatePlugin(pluginId: string, reason = "manual"): Promise<void> {
const redis = getRedis();
await redis.del(ConfigCacheService.buildPluginKey(pluginId));
cacheMetrics.recordInvalidation("plugin", reason);
}
/**
* 批量失效所有 plugin 缓存admin 全量更新时调用)。
* 通过 SCAN 匹配 config:plugin:* 模式删除。
*/
async invalidateAllPlugins(reason = "admin-update"): Promise<void> {
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<string | null> {
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<void> {
const redis = getRedis();
await redis.set(
ConfigCacheService.buildUserLayoutKey(userId),
json,
"EX",
CACHE_TTL_SECONDS,
);
}
async invalidateUserLayout(userId: string, reason = "manual"): Promise<void> {
const redis = getRedis();
await redis.del(ConfigCacheService.buildUserLayoutKey(userId));
cacheMetrics.recordInvalidation("user-layout", reason);
}
}