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 { 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 { if (this.client) { await this.client.disconnect(); this.client = null; } } async setSessionKey(userId: number, sessionKey: string, ttlSeconds: number = 7200): Promise { 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 { const client = await this.connect(); const key = `session_key:${userId}`; return await client.get(key); } async deleteSessionKey(userId: number): Promise { const client = await this.connect(); const key = `session_key:${userId}`; await client.del(key); } async isSessionKeyValid(userId: number): Promise { const sessionKey = await this.getSessionKey(userId); return !!sessionKey; } /** * 设置系统配置缓存 */ async setSystemConfig(tenantId: number, configKey: string, configValue: string, ttlSeconds: number = 3600): Promise { 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 { const client = await this.connect(); const key = `system_config:${tenantId}:${configKey}`; return await client.get(key); } /** * 删除系统配置缓存 */ async deleteSystemConfig(tenantId: number, configKey: string): Promise { const client = await this.connect(); const key = `system_config:${tenantId}:${configKey}`; await client.del(key); } /** * 批量获取系统配置缓存 */ async getSystemConfigs(tenantId: number, configKeys: string[]): Promise> { const client = await this.connect(); const keys = configKeys.map(key => `system_config:${tenantId}:${key}`); const values = await client.mGet(keys); const result: Record = {}; configKeys.forEach((key, index) => { result[key] = values[index]; }); return result; } /** * 设置空值缓存(防止缓存穿透) */ async setNullSystemConfig(tenantId: number, configKey: string, ttlSeconds: number = 300): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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();