redis.util.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import { createClient, RedisClientType } from 'redis';
  2. class RedisUtil {
  3. private client: RedisClientType | null = null;
  4. private static instance: RedisUtil;
  5. private constructor() {}
  6. public static getInstance(): RedisUtil {
  7. if (!RedisUtil.instance) {
  8. RedisUtil.instance = new RedisUtil();
  9. }
  10. return RedisUtil.instance;
  11. }
  12. async connect(): Promise<RedisClientType> {
  13. if (!this.client) {
  14. this.client = createClient({
  15. url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'
  16. });
  17. this.client.on('error', (err) => {
  18. console.error('Redis Client Error:', err);
  19. });
  20. await this.client.connect();
  21. }
  22. return this.client;
  23. }
  24. async disconnect(): Promise<void> {
  25. if (this.client) {
  26. await this.client.disconnect();
  27. this.client = null;
  28. }
  29. }
  30. async setSessionKey(userId: number, sessionKey: string, ttlSeconds: number = 7200): Promise<void> {
  31. const client = await this.connect();
  32. const key = `session_key:${userId}`;
  33. await client.set(key, sessionKey, {
  34. EX: ttlSeconds // 默认2小时过期,与微信session_key有效期一致
  35. });
  36. }
  37. async getSessionKey(userId: number): Promise<string | null> {
  38. const client = await this.connect();
  39. const key = `session_key:${userId}`;
  40. return await client.get(key);
  41. }
  42. async deleteSessionKey(userId: number): Promise<void> {
  43. const client = await this.connect();
  44. const key = `session_key:${userId}`;
  45. await client.del(key);
  46. }
  47. async isSessionKeyValid(userId: number): Promise<boolean> {
  48. const sessionKey = await this.getSessionKey(userId);
  49. return !!sessionKey;
  50. }
  51. /**
  52. * 设置系统配置缓存
  53. */
  54. async setSystemConfig(tenantId: number, configKey: string, configValue: string, ttlSeconds: number = 3600): Promise<void> {
  55. const client = await this.connect();
  56. const key = `system_config:${tenantId}:${configKey}`;
  57. await client.set(key, configValue, {
  58. EX: ttlSeconds // 默认1小时过期
  59. });
  60. }
  61. /**
  62. * 获取系统配置缓存
  63. */
  64. async getSystemConfig(tenantId: number, configKey: string): Promise<string | null> {
  65. const client = await this.connect();
  66. const key = `system_config:${tenantId}:${configKey}`;
  67. return await client.get(key);
  68. }
  69. /**
  70. * 删除系统配置缓存
  71. */
  72. async deleteSystemConfig(tenantId: number, configKey: string): Promise<void> {
  73. const client = await this.connect();
  74. const key = `system_config:${tenantId}:${configKey}`;
  75. await client.del(key);
  76. }
  77. /**
  78. * 批量获取系统配置缓存
  79. */
  80. async getSystemConfigs(tenantId: number, configKeys: string[]): Promise<Record<string, string | null>> {
  81. const client = await this.connect();
  82. const keys = configKeys.map(key => `system_config:${tenantId}:${key}`);
  83. const values = await client.mGet(keys);
  84. const result: Record<string, string | null> = {};
  85. configKeys.forEach((key, index) => {
  86. result[key] = values[index];
  87. });
  88. return result;
  89. }
  90. /**
  91. * 设置空值缓存(防止缓存穿透)
  92. */
  93. async setNullSystemConfig(tenantId: number, configKey: string, ttlSeconds: number = 300): Promise<void> {
  94. const client = await this.connect();
  95. const key = `system_config:${tenantId}:${configKey}`;
  96. await client.set(key, '__NULL__', {
  97. EX: ttlSeconds // 默认5分钟过期
  98. });
  99. }
  100. /**
  101. * 检查是否为空值缓存
  102. */
  103. isNullValue(value: string | null): boolean {
  104. return value === '__NULL__';
  105. }
  106. /**
  107. * 清除租户的所有系统配置缓存
  108. */
  109. async clearTenantSystemConfigs(tenantId: number): Promise<void> {
  110. const client = await this.connect();
  111. const pattern = `system_config:${tenantId}:*`;
  112. // 使用SCAN命令遍历匹配的键并删除
  113. let cursor = 0;
  114. do {
  115. const result = await client.scan(cursor, {
  116. MATCH: pattern,
  117. COUNT: 100
  118. });
  119. cursor = result.cursor;
  120. const keys = result.keys;
  121. if (keys.length > 0) {
  122. await client.del(keys);
  123. }
  124. } while (cursor !== 0);
  125. }
  126. /**
  127. * 格式化系统配置缓存键
  128. */
  129. formatSystemConfigKey(tenantId: number, configKey: string): string {
  130. return `system_config:${tenantId}:${configKey}`;
  131. }
  132. /**
  133. * 设置微信access_token缓存(支持租户隔离)
  134. * @param appId 微信小程序appId
  135. * @param accessToken access_token值
  136. * @param expiresIn 过期时间(秒),微信返回的expires_in,默认7100秒(比微信的7200秒少100秒,确保安全)
  137. * @param tenantId 租户ID,可选,用于多租户隔离
  138. */
  139. async setWechatAccessToken(appId: string, accessToken: string, expiresIn: number = 7100, tenantId?: number): Promise<void> {
  140. const client = await this.connect();
  141. const key = tenantId !== undefined
  142. ? `wechat_access_token:${tenantId}:${appId}`
  143. : `wechat_access_token:${appId}`;
  144. // 计算过期时间戳(毫秒)
  145. const expireAt = Date.now() + (expiresIn * 1000);
  146. // 存储包含 token 和过期时间戳的对象
  147. const tokenData = {
  148. token: accessToken,
  149. expireAt: expireAt
  150. };
  151. await client.set(key, JSON.stringify(tokenData), {
  152. PXAT: expireAt // 使用 PXAT 设置过期时间戳(毫秒)
  153. });
  154. console.debug(`微信access_token缓存设置成功,appId: ${appId}, 租户ID: ${tenantId || '无'}, 过期时间: ${expiresIn}秒, 过期时间戳: ${new Date(expireAt).toISOString()}`);
  155. }
  156. /**
  157. * 获取微信access_token缓存(支持租户隔离)
  158. * @param appId 微信小程序appId
  159. * @param tenantId 租户ID,可选,用于多租户隔离
  160. * @returns access_token值和过期时间戳,或null
  161. */
  162. async getWechatAccessToken(appId: string, tenantId?: number): Promise<{ token: string; expireAt: number } | null> {
  163. const client = await this.connect();
  164. const key = tenantId !== undefined
  165. ? `wechat_access_token:${tenantId}:${appId}`
  166. : `wechat_access_token:${appId}`;
  167. const tokenDataStr = await client.get(key);
  168. if (tokenDataStr) {
  169. try {
  170. const tokenData = JSON.parse(tokenDataStr);
  171. if (tokenData.token && tokenData.expireAt) {
  172. // 检查是否已过期(虽然Redis会自动删除,但这里做双重检查)
  173. if (Date.now() >= tokenData.expireAt) {
  174. console.debug(`微信access_token已过期,appId: ${appId}, 租户ID: ${tenantId || '无'}, 过期时间: ${new Date(tokenData.expireAt).toISOString()}`);
  175. await this.deleteWechatAccessToken(appId, tenantId);
  176. return null;
  177. }
  178. const remainingSeconds = Math.round((tokenData.expireAt - Date.now()) / 1000);
  179. console.debug(`从缓存获取微信access_token成功,appId: ${appId}, 租户ID: ${tenantId || '无'}, 剩余时间: ${remainingSeconds}秒`);
  180. return {
  181. token: tokenData.token,
  182. expireAt: tokenData.expireAt
  183. };
  184. } else {
  185. console.warn(`微信access_token缓存数据格式错误,appId: ${appId}, 租户ID: ${tenantId || '无'}, 数据: ${tokenDataStr}`);
  186. await this.deleteWechatAccessToken(appId, tenantId);
  187. return null;
  188. }
  189. } catch (error) {
  190. console.error(`解析微信access_token缓存数据失败,appId: ${appId}, 租户ID: ${tenantId || '无'}, 数据: ${tokenDataStr}`, error);
  191. await this.deleteWechatAccessToken(appId, tenantId);
  192. return null;
  193. }
  194. } else {
  195. console.debug(`缓存中未找到微信access_token,appId: ${appId}, 租户ID: ${tenantId || '无'}`);
  196. }
  197. return null;
  198. }
  199. /**
  200. * 删除微信access_token缓存(支持租户隔离)
  201. * @param appId 微信小程序appId
  202. * @param tenantId 租户ID,可选,用于多租户隔离
  203. */
  204. async deleteWechatAccessToken(appId: string, tenantId?: number): Promise<void> {
  205. const client = await this.connect();
  206. const key = tenantId !== undefined
  207. ? `wechat_access_token:${tenantId}:${appId}`
  208. : `wechat_access_token:${appId}`;
  209. await client.del(key);
  210. console.debug(`删除微信access_token缓存成功,appId: ${appId}, 租户ID: ${tenantId || '无'}`);
  211. }
  212. /**
  213. * 检查微信access_token缓存是否有效(支持租户隔离)
  214. * @param appId 微信小程序appId
  215. * @param tenantId 租户ID,可选,用于多租户隔离
  216. * @returns 是否有效
  217. */
  218. async isWechatAccessTokenValid(appId: string, tenantId?: number): Promise<boolean> {
  219. const token = await this.getWechatAccessToken(appId, tenantId);
  220. return !!token;
  221. }
  222. /**
  223. * 获取微信access_token缓存的剩余生存时间(支持租户隔离)
  224. * @param appId 微信小程序appId
  225. * @param tenantId 租户ID,可选,用于多租户隔离
  226. * @returns 剩余时间(秒),-1表示永不过期,-2表示键不存在
  227. */
  228. async getWechatAccessTokenTTL(appId: string, tenantId?: number): Promise<number> {
  229. const client = await this.connect();
  230. const key = tenantId !== undefined
  231. ? `wechat_access_token:${tenantId}:${appId}`
  232. : `wechat_access_token:${appId}`;
  233. // 首先获取存储的数据
  234. const tokenDataStr = await client.get(key);
  235. if (!tokenDataStr) {
  236. return -2; // 键不存在
  237. }
  238. try {
  239. const tokenData = JSON.parse(tokenDataStr);
  240. if (tokenData.expireAt) {
  241. // 根据存储的过期时间戳计算剩余时间(秒)
  242. const remainingMs = tokenData.expireAt - Date.now();
  243. if (remainingMs <= 0) {
  244. // 已过期,但Redis可能还没删除,返回0表示已过期
  245. return 0;
  246. }
  247. return Math.ceil(remainingMs / 1000); // 向上取整,确保至少1秒
  248. }
  249. } catch (error) {
  250. console.error(`解析微信access_token缓存数据失败,无法计算TTL,appId: ${appId}, 租户ID: ${tenantId || '无'}`, error);
  251. }
  252. // 如果无法解析或没有expireAt字段,回退到Redis的TTL
  253. return await client.ttl(key);
  254. }
  255. /**
  256. * 清除所有微信access_token缓存
  257. * @param tenantId 租户ID,可选,如果提供则只清除该租户的缓存
  258. */
  259. async clearAllWechatAccessTokens(tenantId?: number): Promise<void> {
  260. const client = await this.connect();
  261. const pattern = tenantId !== undefined
  262. ? `wechat_access_token:${tenantId}:*`
  263. : `wechat_access_token:*`;
  264. // 使用SCAN命令遍历匹配的键并删除
  265. let cursor = 0;
  266. do {
  267. const result = await client.scan(cursor, {
  268. MATCH: pattern,
  269. COUNT: 100
  270. });
  271. cursor = result.cursor;
  272. const keys = result.keys;
  273. if (keys.length > 0) {
  274. await client.del(keys);
  275. console.debug(`批量删除微信access_token缓存,数量: ${keys.length}, 租户ID: ${tenantId || '所有'}`);
  276. }
  277. } while (cursor !== 0);
  278. console.debug(`微信access_token缓存已清除,租户ID: ${tenantId || '所有'}`);
  279. }
  280. }
  281. export const redisUtil = RedisUtil.getInstance();