diff --git a/package-lock.json b/package-lock.json index bb19bf9..fc8dd12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@nestjs/throttler": "^6.4.0", "@openrouter/sdk": "^0.1.27", "@parse/node-apn": "^5.0.0", + "@types/ioredis": "^4.28.10", "@types/jsonwebtoken": "^9.0.9", "@types/uuid": "^10.0.0", "apns2": "^12.2.0", @@ -31,6 +32,7 @@ "crypto-js": "^4.2.0", "dayjs": "^1.11.18", "fs": "^0.0.1-security", + "ioredis": "^5.8.2", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.2.0", "mysql2": "^3.14.0", @@ -1362,6 +1364,12 @@ } } }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://mirrors.tencent.com/npm/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3427,6 +3435,15 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "license": "MIT" }, + "node_modules/@types/ioredis": { + "version": "4.28.10", + "resolved": "https://mirrors.tencent.com/npm/@types/ioredis/-/ioredis-4.28.10.tgz", + "integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -5422,6 +5439,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://mirrors.tencent.com/npm/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -8072,6 +8097,30 @@ "kind-of": "^6.0.2" } }, + "node_modules/ioredis": { + "version": "5.8.2", + "resolved": "https://mirrors.tencent.com/npm/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://mirrors.tencent.com/npm/ip-address/-/ip-address-9.0.5.tgz", @@ -9357,12 +9406,23 @@ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://mirrors.tencent.com/npm/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://mirrors.tencent.com/npm/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://mirrors.tencent.com/npm/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://mirrors.tencent.com/npm/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -11266,6 +11326,26 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://mirrors.tencent.com/npm/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://mirrors.tencent.com/npm/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -12203,6 +12283,12 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://mirrors.tencent.com/npm/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index cbec5fe..3fac396 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@nestjs/throttler": "^6.4.0", "@openrouter/sdk": "^0.1.27", "@parse/node-apn": "^5.0.0", + "@types/ioredis": "^4.28.10", "@types/jsonwebtoken": "^9.0.9", "@types/uuid": "^10.0.0", "apns2": "^12.2.0", @@ -49,6 +50,7 @@ "crypto-js": "^4.2.0", "dayjs": "^1.11.18", "fs": "^0.0.1-security", + "ioredis": "^5.8.2", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.2.0", "mysql2": "^3.14.0", diff --git a/src/app.module.ts b/src/app.module.ts index 15ee142..c140eb7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { LoggerModule } from './common/logger/logger.module'; +import { RedisModule, ThrottlerStorageRedisService } from './redis'; import { CheckinsModule } from './checkins/checkins.module'; import { AiCoachModule } from './ai-coach/ai-coach.module'; import { TrainingPlansModule } from './training-plans/training-plans.module'; @@ -33,10 +34,18 @@ import { HealthProfilesModule } from './health-profiles/health-profiles.module'; envFilePath: '.env', }), ScheduleModule.forRoot(), - ThrottlerModule.forRoot([{ - ttl: 60000, // 时间窗口:60秒 - limit: 100, // 每个时间窗口最多100个请求 - }]), + // 限流模块必须在 RedisModule 之后导入,以确保 Redis 连接可用 + RedisModule, + ThrottlerModule.forRootAsync({ + useFactory: (throttlerStorage: ThrottlerStorageRedisService) => ({ + throttlers: [{ + ttl: 60000, // 时间窗口:60秒 + limit: 100, // 每个时间窗口最多100个请求 + }], + storage: throttlerStorage, + }), + inject: [ThrottlerStorageRedisService], + }), LoggerModule, DatabaseModule, UsersModule, diff --git a/src/medications/services/medication-reminder.service.ts b/src/medications/services/medication-reminder.service.ts index c729277..fc8aa5c 100644 --- a/src/medications/services/medication-reminder.service.ts +++ b/src/medications/services/medication-reminder.service.ts @@ -17,7 +17,7 @@ import * as dayjs from 'dayjs'; @Injectable() export class MedicationReminderService { private readonly logger = new Logger(MedicationReminderService.name); - private readonly REMINDER_MINUTES_BEFORE = 2; // 提前5分钟提醒 + private readonly REMINDER_MINUTES_BEFORE = 5; // 提前5分钟提醒 private readonly OVERDUE_HOURS_THRESHOLD = 1; // 超过1小时后发送超时提醒 private readonly EXPIRY_ONE_MONTH_DAYS = 30; // 提前一个月预警 private readonly EXPIRY_ONE_WEEK_DAYS = 7; // 提前一周预警 diff --git a/src/redis/index.ts b/src/redis/index.ts new file mode 100644 index 0000000..808c767 --- /dev/null +++ b/src/redis/index.ts @@ -0,0 +1,3 @@ +export * from './redis.module'; +export * from './redis.service'; +export * from './throttler-storage-redis.service'; diff --git a/src/redis/redis.module.ts b/src/redis/redis.module.ts new file mode 100644 index 0000000..6c28c59 --- /dev/null +++ b/src/redis/redis.module.ts @@ -0,0 +1,46 @@ +import { Global, Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { RedisService } from './redis.service'; +import { ThrottlerStorageRedisService } from './throttler-storage-redis.service'; + +@Global() +@Module({ + imports: [ConfigModule], + providers: [ + { + provide: 'REDIS_CLIENT', + useFactory: async (configService: ConfigService) => { + const Redis = (await import('ioredis')).default; + const client = new Redis({ + host: configService.get('REDIS_HOST', '127.0.0.1'), + port: configService.get('REDIS_PORT', 6379), + password: configService.get('REDIS_PASSWORD', ''), + db: configService.get('REDIS_DB', 0), + keyPrefix: configService.get('REDIS_PREFIX', 'pilates:'), + retryStrategy: (times: number) => { + if (times > 3) { + return null; // 停止重试 + } + return Math.min(times * 200, 2000); + }, + maxRetriesPerRequest: 3, + }); + + client.on('connect', () => { + console.log('Redis client connected'); + }); + + client.on('error', (err) => { + console.error('Redis client error:', err); + }); + + return client; + }, + inject: [ConfigService], + }, + RedisService, + ThrottlerStorageRedisService, + ], + exports: ['REDIS_CLIENT', RedisService, ThrottlerStorageRedisService], +}) +export class RedisModule {} diff --git a/src/redis/redis.service.ts b/src/redis/redis.service.ts new file mode 100644 index 0000000..f1a7c58 --- /dev/null +++ b/src/redis/redis.service.ts @@ -0,0 +1,315 @@ +import { Injectable, Inject, OnModuleDestroy, Logger } from '@nestjs/common'; +import Redis from 'ioredis'; + +@Injectable() +export class RedisService implements OnModuleDestroy { + private readonly logger = new Logger(RedisService.name); + + constructor( + @Inject('REDIS_CLIENT') + private readonly redis: Redis, + ) {} + + async onModuleDestroy() { + await this.redis.quit(); + this.logger.log('Redis connection closed'); + } + + /** + * 获取原始 Redis 客户端(用于高级操作) + */ + getClient(): Redis { + return this.redis; + } + + // ==================== 基础操作 ==================== + + /** + * 设置键值 + * @param key 键 + * @param value 值 + * @param ttlSeconds 过期时间(秒),可选 + */ + async set(key: string, value: string, ttlSeconds?: number): Promise { + if (ttlSeconds) { + await this.redis.setex(key, ttlSeconds, value); + } else { + await this.redis.set(key, value); + } + } + + /** + * 获取键值 + */ + async get(key: string): Promise { + return this.redis.get(key); + } + + /** + * 删除键 + */ + async del(...keys: string[]): Promise { + return this.redis.del(...keys); + } + + /** + * 检查键是否存在 + */ + async exists(key: string): Promise { + const result = await this.redis.exists(key); + return result === 1; + } + + /** + * 设置过期时间 + */ + async expire(key: string, seconds: number): Promise { + const result = await this.redis.expire(key, seconds); + return result === 1; + } + + /** + * 获取剩余过期时间 + */ + async ttl(key: string): Promise { + return this.redis.ttl(key); + } + + // ==================== JSON 操作 ==================== + + /** + * 设置 JSON 对象 + */ + async setJson(key: string, value: T, ttlSeconds?: number): Promise { + const jsonString = JSON.stringify(value); + await this.set(key, jsonString, ttlSeconds); + } + + /** + * 获取 JSON 对象 + */ + async getJson(key: string): Promise { + const value = await this.get(key); + if (!value) return null; + try { + return JSON.parse(value) as T; + } catch { + return null; + } + } + + // ==================== Hash 操作 ==================== + + /** + * 设置 Hash 字段 + */ + async hset(key: string, field: string, value: string): Promise { + return this.redis.hset(key, field, value); + } + + /** + * 获取 Hash 字段 + */ + async hget(key: string, field: string): Promise { + return this.redis.hget(key, field); + } + + /** + * 获取所有 Hash 字段 + */ + async hgetall(key: string): Promise> { + return this.redis.hgetall(key); + } + + /** + * 删除 Hash 字段 + */ + async hdel(key: string, ...fields: string[]): Promise { + return this.redis.hdel(key, ...fields); + } + + // ==================== List 操作 ==================== + + /** + * 从左侧推入列表 + */ + async lpush(key: string, ...values: string[]): Promise { + return this.redis.lpush(key, ...values); + } + + /** + * 从右侧推入列表 + */ + async rpush(key: string, ...values: string[]): Promise { + return this.redis.rpush(key, ...values); + } + + /** + * 获取列表范围 + */ + async lrange(key: string, start: number, stop: number): Promise { + return this.redis.lrange(key, start, stop); + } + + /** + * 获取列表长度 + */ + async llen(key: string): Promise { + return this.redis.llen(key); + } + + // ==================== Set 操作 ==================== + + /** + * 添加 Set 成员 + */ + async sadd(key: string, ...members: string[]): Promise { + return this.redis.sadd(key, ...members); + } + + /** + * 获取所有 Set 成员 + */ + async smembers(key: string): Promise { + return this.redis.smembers(key); + } + + /** + * 检查是否是 Set 成员 + */ + async sismember(key: string, member: string): Promise { + const result = await this.redis.sismember(key, member); + return result === 1; + } + + /** + * 移除 Set 成员 + */ + async srem(key: string, ...members: string[]): Promise { + return this.redis.srem(key, ...members); + } + + // ==================== 计数器操作 ==================== + + /** + * 自增 + */ + async incr(key: string): Promise { + return this.redis.incr(key); + } + + /** + * 自增指定值 + */ + async incrby(key: string, increment: number): Promise { + return this.redis.incrby(key, increment); + } + + /** + * 自减 + */ + async decr(key: string): Promise { + return this.redis.decr(key); + } + + // ==================== 分布式锁 ==================== + + /** + * 获取分布式锁 + * @param lockKey 锁的键名 + * @param ttlSeconds 锁的过期时间(秒) + * @param retryTimes 重试次数 + * @param retryDelay 重试间隔(毫秒) + * @returns 锁的唯一标识,获取失败返回 null + */ + async acquireLock( + lockKey: string, + ttlSeconds: number = 10, + retryTimes: number = 3, + retryDelay: number = 100, + ): Promise { + const lockValue = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + + for (let i = 0; i < retryTimes; i++) { + const result = await this.redis.set( + `lock:${lockKey}`, + lockValue, + 'EX', + ttlSeconds, + 'NX', + ); + + if (result === 'OK') { + return lockValue; + } + + if (i < retryTimes - 1) { + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + } + } + + return null; + } + + /** + * 释放分布式锁 + * @param lockKey 锁的键名 + * @param lockValue 锁的唯一标识 + */ + async releaseLock(lockKey: string, lockValue: string): Promise { + const script = ` + if redis.call("get", KEYS[1]) == ARGV[1] then + return redis.call("del", KEYS[1]) + else + return 0 + end + `; + const result = await this.redis.eval(script, 1, `lock:${lockKey}`, lockValue); + return result === 1; + } + + // ==================== 缓存辅助方法 ==================== + + /** + * 带缓存的数据获取 + * 如果缓存存在则返回缓存,否则执行 factory 函数获取数据并缓存 + */ + async getOrSet( + key: string, + factory: () => Promise, + ttlSeconds: number = 300, + ): Promise { + const cached = await this.getJson(key); + if (cached !== null) { + return cached; + } + + const value = await factory(); + await this.setJson(key, value, ttlSeconds); + return value; + } + + /** + * 批量删除匹配模式的键 + * @param pattern 匹配模式,如 "user:*" + */ + async delByPattern(pattern: string): Promise { + const keys = await this.redis.keys(pattern); + if (keys.length === 0) return 0; + return this.redis.del(...keys); + } + + // ==================== 健康检查 ==================== + + /** + * 检查 Redis 连接状态 + */ + async ping(): Promise { + try { + const result = await this.redis.ping(); + return result === 'PONG'; + } catch { + return false; + } + } +} diff --git a/src/redis/throttler-storage-redis.service.ts b/src/redis/throttler-storage-redis.service.ts new file mode 100644 index 0000000..fa406d7 --- /dev/null +++ b/src/redis/throttler-storage-redis.service.ts @@ -0,0 +1,93 @@ +import { Injectable, Inject, OnModuleDestroy } from '@nestjs/common'; +import { ThrottlerStorage } from '@nestjs/throttler'; +import Redis from 'ioredis'; + +export interface ThrottlerStorageRecord { + totalHits: number; + timeToExpire: number; + isBlocked: boolean; + timeToBlockExpire: number; +} + +@Injectable() +export class ThrottlerStorageRedisService + implements ThrottlerStorage, OnModuleDestroy +{ + private readonly prefix = 'throttler:'; + + constructor( + @Inject('REDIS_CLIENT') + private readonly redis: Redis, + ) {} + + async onModuleDestroy() { + // Redis 连接由 RedisModule 管理,这里不需要关闭 + } + + /** + * 增加指定 key 的请求计数 + * @param key 限流 key(通常是 IP 或用户标识) + * @param ttl 过期时间(毫秒) + * @param limit 限制次数 + * @param blockDuration 封禁时长(毫秒) + * @param throttlerName 限流器名称 + */ + async increment( + key: string, + ttl: number, + limit: number, + blockDuration: number, + throttlerName: string, + ): Promise { + const redisKey = `${this.prefix}${throttlerName}:${key}`; + const blockKey = `${this.prefix}${throttlerName}:block:${key}`; + + // 检查是否被封禁 + const blockTtl = await this.redis.pttl(blockKey); + if (blockTtl > 0) { + return { + totalHits: limit + 1, + timeToExpire: ttl, + isBlocked: true, + timeToBlockExpire: blockTtl, + }; + } + + // 使用 Lua 脚本保证原子性操作 + const luaScript = ` + local current = redis.call('INCR', KEYS[1]) + if current == 1 then + redis.call('PEXPIRE', KEYS[1], ARGV[1]) + end + local pttl = redis.call('PTTL', KEYS[1]) + return {current, pttl} + `; + + const result = (await this.redis.eval( + luaScript, + 1, + redisKey, + ttl.toString(), + )) as [number, number]; + + const totalHits = result[0]; + const timeToExpire = result[1] > 0 ? result[1] : ttl; + + // 如果超过限制且设置了封禁时长,则设置封禁 + let isBlocked = false; + let timeToBlockExpire = 0; + + if (totalHits > limit && blockDuration > 0) { + await this.redis.set(blockKey, '1', 'PX', blockDuration); + isBlocked = true; + timeToBlockExpire = blockDuration; + } + + return { + totalHits, + timeToExpire, + isBlocked, + timeToBlockExpire, + }; + } +} diff --git a/yarn.lock b/yarn.lock index 9064754..40a61f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -599,6 +599,11 @@ resolved "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz" integrity sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg== +"@ioredis/commands@1.4.0": + version "1.4.0" + resolved "https://mirrors.tencent.com/npm/@ioredis/commands/-/commands-1.4.0.tgz" + integrity sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" @@ -1526,6 +1531,13 @@ resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== +"@types/ioredis@^4.28.10": + version "4.28.10" + resolved "https://mirrors.tencent.com/npm/@types/ioredis/-/ioredis-4.28.10.tgz" + integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ== + dependencies: + "@types/node" "*" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" @@ -2740,6 +2752,11 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://mirrors.tencent.com/npm/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" @@ -4316,6 +4333,21 @@ inspect-with-kind@^1.0.5: dependencies: kind-of "^6.0.2" +ioredis@^5.8.2: + version "5.8.2" + resolved "https://mirrors.tencent.com/npm/ioredis/-/ioredis-5.8.2.tgz" + integrity sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q== + dependencies: + "@ioredis/commands" "1.4.0" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip-address@^9.0.5: version "9.0.5" resolved "https://mirrors.tencent.com/npm/ip-address/-/ip-address-9.0.5.tgz" @@ -5138,11 +5170,21 @@ lodash.clonedeep@^4.5.0: resolved "https://mirrors.tencent.com/npm/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://mirrors.tencent.com/npm/lodash.defaults/-/lodash.defaults-4.2.0.tgz" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://mirrors.tencent.com/npm/lodash.includes/-/lodash.includes-4.3.0.tgz" integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://mirrors.tencent.com/npm/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://mirrors.tencent.com/npm/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" @@ -6213,6 +6255,18 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://mirrors.tencent.com/npm/redis-errors/-/redis-errors-1.2.0.tgz" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://mirrors.tencent.com/npm/redis-parser/-/redis-parser-3.0.0.tgz" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + reflect-metadata@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz" @@ -6716,6 +6770,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://mirrors.tencent.com/npm/standard-as-callback/-/standard-as-callback-2.1.0.tgz" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + statuses@2.0.1, statuses@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"