| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- import { createClient, RedisClientType } from 'redis';
- class RedisUtil {
- private client: RedisClientType | null = null;
- private static instance: RedisUtil;
- private constructor() {}
- public static getInstance(): RedisUtil {
- if (!RedisUtil.instance) {
- RedisUtil.instance = new RedisUtil();
- }
- return RedisUtil.instance;
- }
- async connect(): Promise<RedisClientType> {
- if (!this.client) {
- this.client = createClient({
- url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'
- });
- this.client.on('error', (err) => {
- console.error('Redis Client Error:', err);
- });
- await this.client.connect();
- }
- return this.client;
- }
- async disconnect(): Promise<void> {
- if (this.client) {
- await this.client.disconnect();
- this.client = null;
- }
- }
- async setSessionKey(userId: number, sessionKey: string, ttlSeconds: number = 7200): Promise<void> {
- const client = await this.connect();
- const key = `session_key:${userId}`;
- await client.set(key, sessionKey, {
- EX: ttlSeconds // 默认2小时过期,与微信session_key有效期一致
- });
- }
- async getSessionKey(userId: number): Promise<string | null> {
- const client = await this.connect();
- const key = `session_key:${userId}`;
- return await client.get(key);
- }
- async deleteSessionKey(userId: number): Promise<void> {
- const client = await this.connect();
- const key = `session_key:${userId}`;
- await client.del(key);
- }
- async isSessionKeyValid(userId: number): Promise<boolean> {
- const sessionKey = await this.getSessionKey(userId);
- return !!sessionKey;
- }
- /**
- * 设置系统配置缓存
- */
- async setSystemConfig(tenantId: number, configKey: string, configValue: string, ttlSeconds: number = 3600): Promise<void> {
- const client = await this.connect();
- const key = `system_config:${tenantId}:${configKey}`;
- await client.set(key, configValue, {
- EX: ttlSeconds // 默认1小时过期
- });
- }
- /**
- * 获取系统配置缓存
- */
- async getSystemConfig(tenantId: number, configKey: string): Promise<string | null> {
- const client = await this.connect();
- const key = `system_config:${tenantId}:${configKey}`;
- return await client.get(key);
- }
- /**
- * 删除系统配置缓存
- */
- async deleteSystemConfig(tenantId: number, configKey: string): Promise<void> {
- const client = await this.connect();
- const key = `system_config:${tenantId}:${configKey}`;
- await client.del(key);
- }
- /**
- * 批量获取系统配置缓存
- */
- async getSystemConfigs(tenantId: number, configKeys: string[]): Promise<Record<string, string | null>> {
- const client = await this.connect();
- const keys = configKeys.map(key => `system_config:${tenantId}:${key}`);
- const values = await client.mGet(keys);
- const result: Record<string, string | null> = {};
- configKeys.forEach((key, index) => {
- result[key] = values[index];
- });
- return result;
- }
- /**
- * 设置空值缓存(防止缓存穿透)
- */
- async setNullSystemConfig(tenantId: number, configKey: string, ttlSeconds: number = 300): Promise<void> {
- const client = await this.connect();
- const key = `system_config:${tenantId}:${configKey}`;
- await client.set(key, '__NULL__', {
- EX: ttlSeconds // 默认5分钟过期
- });
- }
- /**
- * 检查是否为空值缓存
- */
- isNullValue(value: string | null): boolean {
- return value === '__NULL__';
- }
- /**
- * 清除租户的所有系统配置缓存
- */
- async clearTenantSystemConfigs(tenantId: number): Promise<void> {
- const client = await this.connect();
- const pattern = `system_config:${tenantId}:*`;
- // 使用SCAN命令遍历匹配的键并删除
- let cursor = 0;
- do {
- const result = await client.scan(cursor, {
- MATCH: pattern,
- COUNT: 100
- });
- cursor = result.cursor;
- const keys = result.keys;
- if (keys.length > 0) {
- await client.del(keys);
- }
- } while (cursor !== 0);
- }
- /**
- * 格式化系统配置缓存键
- */
- formatSystemConfigKey(tenantId: number, configKey: string): string {
- return `system_config:${tenantId}:${configKey}`;
- }
- /**
- * 设置微信access_token缓存(支持租户隔离)
- * @param appId 微信小程序appId
- * @param accessToken access_token值
- * @param expiresIn 过期时间(秒),微信返回的expires_in,默认7100秒(比微信的7200秒少100秒,确保安全)
- * @param tenantId 租户ID,可选,用于多租户隔离
- */
- async setWechatAccessToken(appId: string, accessToken: string, expiresIn: number = 7100, tenantId?: number): Promise<void> {
- const client = await this.connect();
- const key = tenantId !== undefined
- ? `wechat_access_token:${tenantId}:${appId}`
- : `wechat_access_token:${appId}`;
- // 计算过期时间戳(毫秒)
- const expireAt = Date.now() + (expiresIn * 1000);
- // 存储包含 token 和过期时间戳的对象
- const tokenData = {
- token: accessToken,
- expireAt: expireAt
- };
- await client.set(key, JSON.stringify(tokenData), {
- PXAT: expireAt // 使用 PXAT 设置过期时间戳(毫秒)
- });
- console.debug(`微信access_token缓存设置成功,appId: ${appId}, 租户ID: ${tenantId || '无'}, 过期时间: ${expiresIn}秒, 过期时间戳: ${new Date(expireAt).toISOString()}`);
- }
- /**
- * 获取微信access_token缓存(支持租户隔离)
- * @param appId 微信小程序appId
- * @param tenantId 租户ID,可选,用于多租户隔离
- * @returns access_token值和过期时间戳,或null
- */
- async getWechatAccessToken(appId: string, tenantId?: number): Promise<{ token: string; expireAt: number } | null> {
- const client = await this.connect();
- const key = tenantId !== undefined
- ? `wechat_access_token:${tenantId}:${appId}`
- : `wechat_access_token:${appId}`;
- const tokenDataStr = await client.get(key);
- if (tokenDataStr) {
- try {
- const tokenData = JSON.parse(tokenDataStr);
- if (tokenData.token && tokenData.expireAt) {
- // 检查是否已过期(虽然Redis会自动删除,但这里做双重检查)
- if (Date.now() >= tokenData.expireAt) {
- console.debug(`微信access_token已过期,appId: ${appId}, 租户ID: ${tenantId || '无'}, 过期时间: ${new Date(tokenData.expireAt).toISOString()}`);
- await this.deleteWechatAccessToken(appId, tenantId);
- return null;
- }
- const remainingSeconds = Math.round((tokenData.expireAt - Date.now()) / 1000);
- console.debug(`从缓存获取微信access_token成功,appId: ${appId}, 租户ID: ${tenantId || '无'}, 剩余时间: ${remainingSeconds}秒`);
- return {
- token: tokenData.token,
- expireAt: tokenData.expireAt
- };
- } else {
- console.warn(`微信access_token缓存数据格式错误,appId: ${appId}, 租户ID: ${tenantId || '无'}, 数据: ${tokenDataStr}`);
- await this.deleteWechatAccessToken(appId, tenantId);
- return null;
- }
- } catch (error) {
- console.error(`解析微信access_token缓存数据失败,appId: ${appId}, 租户ID: ${tenantId || '无'}, 数据: ${tokenDataStr}`, error);
- await this.deleteWechatAccessToken(appId, tenantId);
- return null;
- }
- } else {
- console.debug(`缓存中未找到微信access_token,appId: ${appId}, 租户ID: ${tenantId || '无'}`);
- }
- return null;
- }
- /**
- * 删除微信access_token缓存(支持租户隔离)
- * @param appId 微信小程序appId
- * @param tenantId 租户ID,可选,用于多租户隔离
- */
- async deleteWechatAccessToken(appId: string, tenantId?: number): Promise<void> {
- const client = await this.connect();
- const key = tenantId !== undefined
- ? `wechat_access_token:${tenantId}:${appId}`
- : `wechat_access_token:${appId}`;
- await client.del(key);
- console.debug(`删除微信access_token缓存成功,appId: ${appId}, 租户ID: ${tenantId || '无'}`);
- }
- /**
- * 检查微信access_token缓存是否有效(支持租户隔离)
- * @param appId 微信小程序appId
- * @param tenantId 租户ID,可选,用于多租户隔离
- * @returns 是否有效
- */
- async isWechatAccessTokenValid(appId: string, tenantId?: number): Promise<boolean> {
- const token = await this.getWechatAccessToken(appId, tenantId);
- return !!token;
- }
- /**
- * 获取微信access_token缓存的剩余生存时间(支持租户隔离)
- * @param appId 微信小程序appId
- * @param tenantId 租户ID,可选,用于多租户隔离
- * @returns 剩余时间(秒),-1表示永不过期,-2表示键不存在
- */
- async getWechatAccessTokenTTL(appId: string, tenantId?: number): Promise<number> {
- const client = await this.connect();
- const key = tenantId !== undefined
- ? `wechat_access_token:${tenantId}:${appId}`
- : `wechat_access_token:${appId}`;
- // 首先获取存储的数据
- const tokenDataStr = await client.get(key);
- if (!tokenDataStr) {
- return -2; // 键不存在
- }
- try {
- const tokenData = JSON.parse(tokenDataStr);
- if (tokenData.expireAt) {
- // 根据存储的过期时间戳计算剩余时间(秒)
- const remainingMs = tokenData.expireAt - Date.now();
- if (remainingMs <= 0) {
- // 已过期,但Redis可能还没删除,返回0表示已过期
- return 0;
- }
- return Math.ceil(remainingMs / 1000); // 向上取整,确保至少1秒
- }
- } catch (error) {
- console.error(`解析微信access_token缓存数据失败,无法计算TTL,appId: ${appId}, 租户ID: ${tenantId || '无'}`, error);
- }
- // 如果无法解析或没有expireAt字段,回退到Redis的TTL
- return await client.ttl(key);
- }
- /**
- * 清除所有微信access_token缓存
- * @param tenantId 租户ID,可选,如果提供则只清除该租户的缓存
- */
- async clearAllWechatAccessTokens(tenantId?: number): Promise<void> {
- const client = await this.connect();
- const pattern = tenantId !== undefined
- ? `wechat_access_token:${tenantId}:*`
- : `wechat_access_token:*`;
- // 使用SCAN命令遍历匹配的键并删除
- let cursor = 0;
- do {
- const result = await client.scan(cursor, {
- MATCH: pattern,
- COUNT: 100
- });
- cursor = result.cursor;
- const keys = result.keys;
- if (keys.length > 0) {
- await client.del(keys);
- console.debug(`批量删除微信access_token缓存,数量: ${keys.length}, 租户ID: ${tenantId || '所有'}`);
- }
- } while (cursor !== 0);
- console.debug(`微信access_token缓存已清除,租户ID: ${tenantId || '所有'}`);
- }
- }
- export const redisUtil = RedisUtil.getInstance();
|