增加google退款相关功能

This commit is contained in:
CounterFire2023 2023-07-11 10:16:01 +08:00
parent e2ab4da847
commit e5dc33970c
12 changed files with 521 additions and 10 deletions

View File

@ -44,4 +44,5 @@ PAY_TRANSFER_CB_URL='http://127.0.0.1:3007/api/internal/update_task'
HASH_SALT='iG4Rpsa)6U31$H#^T85$^^3' HASH_SALT='iG4Rpsa)6U31$H#^T85$^^3'
# 游戏服, 支付上报地址 # 游戏服, 支付上报地址
GAME_PAY_CB_URL=https://game2006api-test.kingsome.cn/webapp/index.php?c=Shop&a=buyGoodsDirect GAME_PAY_CB_URL=https://game2006api-test.kingsome.cn/webapp/index.php
REDIS=redis://127.0.0.1:6379/14

13
.vscode/launch.json vendored
View File

@ -17,5 +17,18 @@
], ],
"type": "pwa-node" "type": "pwa-node"
}, },
{
"name": "Debug Monitor",
"request": "launch",
"runtimeArgs": [
"run-script",
"dev:monitor"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node"
},
] ]
} }

View File

@ -5,6 +5,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev:api": "ts-node -r tsconfig-paths/register src/api.ts", "dev:api": "ts-node -r tsconfig-paths/register src/api.ts",
"dev:monitor": "ts-node -r tsconfig-paths/register src/google.monitor.ts",
"build": "tsc", "build": "tsc",
"prod:api": "NODE_ENV=production NODE_PATH=./dist node dist/api.js", "prod:api": "NODE_ENV=production NODE_PATH=./dist node dist/api.js",
"lint": "eslint --ext .ts src/**", "lint": "eslint --ext .ts src/**",
@ -31,6 +32,7 @@
"mongoose-findorcreate": "^3.0.0", "mongoose-findorcreate": "^3.0.0",
"nanoid": "^3.1.23", "nanoid": "^3.1.23",
"node-schedule": "^2.1.1", "node-schedule": "^2.1.1",
"redis": "^3.1.2",
"tracer": "^1.1.6", "tracer": "^1.1.6",
"verify-apple-id-token": "^3.0.0" "verify-apple-id-token": "^3.0.0"
}, },
@ -38,6 +40,7 @@
"@typegoose/typegoose": "^9.12.1", "@typegoose/typegoose": "^9.12.1",
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
"@types/node-schedule": "^2.1.0", "@types/node-schedule": "^2.1.0",
"@types/redis": "^2.8.28",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/parser": "^5.40.1",
"eslint": "^8.25.0", "eslint": "^8.25.0",

View File

@ -171,8 +171,8 @@ export class ApiServer {
self.registerRouter() self.registerRouter()
self.setErrHandler() self.setErrHandler()
self.setFormatSend() self.setFormatSend()
self.initSchedules()
await self.initOtherServices() await self.initOtherServices()
self.initSchedules()
this.server.listen({ port: config.api.port, host: config.api.host }, (err: any, address: any) => { this.server.listen({ port: config.api.port, host: config.api.host }, (err: any, address: any) => {
if (err) { if (err) {
logger.log(err) logger.log(err)

View File

@ -3,6 +3,7 @@ import { ZError } from 'common/ZError'
import { router } from 'decorators/router' import { router } from 'decorators/router'
import logger from 'logger/logger' import logger from 'logger/logger'
import { GoogleInApp, GooglePayStatus } from 'modules/GoogleInApp' import { GoogleInApp, GooglePayStatus } from 'modules/GoogleInApp'
import { IPayResult, reportGooglePurchaseResult } from 'service/game.svr'
import { GooglePaySvr } from 'service/googlepay.svr' import { GooglePaySvr } from 'service/googlepay.svr'
class GooglePayController extends BaseController { class GooglePayController extends BaseController {
@ -14,7 +15,7 @@ class GooglePayController extends BaseController {
throw new ZError(10, 'purchase data is empty') throw new ZError(10, 'purchase data is empty')
} }
logger.info(`verify google purchase::list=${list}`) logger.info(`verify google purchase::list=${list}`)
let results = [] let results: IPayResult[] = []
for (let sub of list) { for (let sub of list) {
let infoRes = await new GooglePaySvr().fetchPurchaseData(sub.id, sub.token) let infoRes = await new GooglePaySvr().fetchPurchaseData(sub.id, sub.token)
// logger.log(JSON.stringify(infoRes)) // logger.log(JSON.stringify(infoRes))
@ -58,6 +59,16 @@ class GooglePayController extends BaseController {
} }
} }
//TODO:: 通知游戏服 //TODO:: 通知游戏服
if (results.length) {
setImmediate(async () => {
try {
await reportGooglePurchaseResult(results)
} catch (err) {
logger.error('report google voided purchases failed', err.message || err)
}
})
}
return results return results
} }
} }

23
src/google.monitor.ts Normal file
View File

@ -0,0 +1,23 @@
import * as dotenv from 'dotenv'
import logger from 'logger/logger'
import { RedisClient } from 'redis/RedisClient'
const envFile = process.env.NODE_ENV && process.env.NODE_ENV === 'production' ? `.env.production` : '.env.development'
dotenv.config({ path: envFile })
import 'common/Extend'
import { GooglePaySvr } from 'service/googlepay.svr'
import GooglePurchaseSchedule from 'schedule/googlepurchase.schedule'
async function main() {
let opts = { url: process.env.REDIS }
new RedisClient(opts)
logger.info('REDIS Connected')
await new GooglePaySvr().init()
logger.info('GooglePaySvr Connected')
setInterval(function () {
new GooglePurchaseSchedule().parseAllRecord()
}, 60 * 60 * 1000)
new GooglePurchaseSchedule().parseAllRecord()
}
main()

View File

@ -1,6 +1,8 @@
import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose' import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn' import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base' import { BaseModule } from './Base'
import logger from 'logger/logger'
import { IPayResult, reportGooglePurchaseResult } from 'service/game.svr'
export enum GooglePayStatus { export enum GooglePayStatus {
PENDING = 0, // 默认状态, 未支付 PENDING = 0, // 默认状态, 未支付
@ -80,6 +82,38 @@ export class GoogleInAppClass extends BaseModule {
public static async findByRecordId(this: ReturnModelType<typeof GoogleInAppClass>, outOrderId: string) { public static async findByRecordId(this: ReturnModelType<typeof GoogleInAppClass>, outOrderId: string) {
return this.findOne({ outOrderId }).exec() return this.findOne({ outOrderId }).exec()
} }
public static async parseVoidedRecords(this: ReturnModelType<typeof GoogleInAppClass>, voidedPurchases: any) {
if (!voidedPurchases || !voidedPurchases.length) {
return
}
let results: IPayResult[] = []
for (let sub of voidedPurchases) {
let { orderId, voidedTimeMillis, voidedSource, voidedReason } = sub
// orderId = 'GPA.3355-1172-9416-16839'
const record = await GoogleInApp.findOneAndUpdate(
{ outOrderId: orderId, status: GooglePayStatus.SUCCESS },
{ $set: { voidedTime: voidedTimeMillis, voidedSource, voidedReason, status: GooglePayStatus.VOIDED } },
{ returnDocument: 'after' },
)
if (record) {
results.push({
productId: record.productId,
gameOrderId: record.gameOrderId,
orderId: record.outOrderId,
status: record.status,
})
}
}
if (results.length) {
try {
await reportGooglePurchaseResult(results)
} catch (err) {
logger.error('report google voided purchases failed', err.message || err)
}
}
}
} }
export const GoogleInApp = getModelForClass(GoogleInAppClass, { existingConnection: GoogleInAppClass.db }) export const GoogleInApp = getModelForClass(GoogleInAppClass, { existingConnection: GoogleInAppClass.db })

306
src/redis/RedisClient.ts Normal file
View File

@ -0,0 +1,306 @@
import { resolveCname } from 'dns'
import redis from 'redis'
import { promisify } from 'util'
import { singleton } from '../decorators/singleton'
type Callback = (...args: any[]) => void
@singleton
export class RedisClient {
public pub: redis.RedisClient
public sub: redis.RedisClient
protected subscribeAsync: any
protected unsubscribeAsync: any
protected publishAsync: any
protected subscriptions: { [channel: string]: Callback[] } = {}
protected smembersAsync: any
protected sismemberAsync: any
protected hgetAsync: any
protected hlenAsync: any
protected pubsubAsync: any
protected incrAsync: any
protected decrAsync: any
constructor(opts?: redis.ClientOpts) {
this.sub = redis.createClient(opts)
this.pub = redis.createClient(opts)
// no listener limit
this.sub.setMaxListeners(0)
// create promisified pub/sub methods.
this.subscribeAsync = promisify(this.sub.subscribe).bind(this.sub)
this.unsubscribeAsync = promisify(this.sub.unsubscribe).bind(this.sub)
this.publishAsync = promisify(this.pub.publish).bind(this.pub)
// create promisified redis methods.
this.smembersAsync = promisify(this.pub.smembers).bind(this.pub)
this.sismemberAsync = promisify(this.pub.sismember).bind(this.pub)
this.hlenAsync = promisify(this.pub.hlen).bind(this.pub)
this.hgetAsync = promisify(this.pub.hget).bind(this.pub)
this.pubsubAsync = promisify(this.pub.pubsub).bind(this.pub)
this.decrAsync = promisify(this.pub.decr).bind(this.pub)
this.incrAsync = promisify(this.pub.incr).bind(this.pub)
}
public async subscribe(topic: string, callback: Callback) {
if (!this.subscriptions[topic]) {
this.subscriptions[topic] = []
}
this.subscriptions[topic].push(callback)
if (this.sub.listeners('message').length === 0) {
this.sub.addListener('message', this.handleSubscription)
}
await this.subscribeAsync(topic)
return this
}
public async unsubscribe(topic: string, callback?: Callback) {
if (callback) {
const index = this.subscriptions[topic].indexOf(callback)
this.subscriptions[topic].splice(index, 1)
} else {
this.subscriptions[topic] = []
}
if (this.subscriptions[topic].length === 0) {
await this.unsubscribeAsync(topic)
}
return this
}
public async publish(topic: string, data: any) {
if (data === undefined) {
data = false
}
await this.publishAsync(topic, JSON.stringify(data))
}
public async exists(roomId: string): Promise<boolean> {
return (await this.pubsubAsync('channels', roomId)).length > 0
}
public async setex(key: string, value: string, seconds: number) {
return new Promise(resolve => this.pub.setex(key, seconds, value, resolve))
}
public async expire(key: string, seconds: number) {
return new Promise(resolve => this.pub.expire(key, seconds, resolve))
}
public async get(key: string): Promise<string | null> {
return new Promise((resolve, reject) => {
this.pub.get(key, (err, data: string | null) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async set(key: string, val: string) {
return new Promise(resolve => {
this.pub.set(key, val, () => {
resolve && resolve('')
})
})
}
public async del(roomId: string) {
return new Promise(resolve => {
this.pub.del(roomId, resolve)
})
}
public async sadd(key: string, value: any) {
return new Promise(resolve => {
this.pub.sadd(key, value, resolve)
})
}
public async smembers(key: string): Promise<string[]> {
return await this.smembersAsync(key)
}
public async sismember(key: string, field: string): Promise<number> {
return await this.sismemberAsync(key, field)
}
public async srem(key: string, value: any) {
return new Promise(resolve => {
this.pub.srem(key, value, resolve)
})
}
public async scard(key: string) {
return new Promise((resolve, reject) => {
this.pub.scard(key, (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async srandmember(key: string) {
return new Promise((resolve, reject) => {
this.pub.srandmember(key, (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async sinter(...keys: string[]) {
return new Promise<string[]>((resolve, reject) => {
this.pub.sinter(...keys, (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async zadd(key: string, value: any, member: string) {
return new Promise(resolve => {
this.pub.zadd(key, value, member, resolve)
})
}
public async zrangebyscore(key: string, min: number, max: number) {
return new Promise((resolve, reject) => {
this.pub.zrangebyscore(key, min, max, 'withscores', (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async zcard(key: string) {
return new Promise((resolve, reject) => {
this.pub.zcard(key, (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async zcount(key: string, min: number, max: number) {
return new Promise((resolve, reject) => {
this.pub.zcount(key, min, max, (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async zrevrank(key: string, member: string) {
return new Promise((resolve, reject) => {
this.pub.zrevrank(key, member, (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async zscore(key: string, member: string) {
return new Promise((resolve, reject) => {
this.pub.zscore(key, member, (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async zrevrange(key: string, start: number, end: number) {
return new Promise((resolve, reject) => {
this.pub.zrevrange(key, start, end, 'withscores', (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}
public async hset(key: string, field: string, value: string) {
return new Promise(resolve => {
this.pub.hset(key, field, value, resolve)
})
}
public async hincrby(key: string, field: string, value: number) {
return new Promise(resolve => {
this.pub.hincrby(key, field, value, resolve)
})
}
public async hget(key: string, field: string) {
return await this.hgetAsync(key, field)
}
public async hgetall(key: string) {
return new Promise<{ [key: string]: string }>((resolve, reject) => {
this.pub.hgetall(key, (err, values) => {
if (err) {
return reject(err)
}
resolve(values)
})
})
}
public async hdel(key: string, field: string) {
return new Promise((resolve, reject) => {
this.pub.hdel(key, field, (err, ok) => {
if (err) {
return reject(err)
}
resolve(ok)
})
})
}
public async hlen(key: string): Promise<number> {
return await this.hlenAsync(key)
}
public async incr(key: string): Promise<number> {
return await this.incrAsync(key)
}
public async decr(key: string): Promise<number> {
return await this.decrAsync(key)
}
protected handleSubscription = (channel: string, message: string) => {
if (this.subscriptions[channel]) {
for (let i = 0, l = this.subscriptions[channel].length; i < l; i++) {
this.subscriptions[channel][i](JSON.parse(message))
}
}
}
}

View File

@ -0,0 +1,48 @@
import { singleton } from 'decorators/singleton'
import logger from 'logger/logger'
import { GoogleInApp } from 'modules/GoogleInApp'
import * as schedule from 'node-schedule'
import { RedisClient } from 'redis/RedisClient'
import { GooglePaySvr } from 'service/googlepay.svr'
/**
*
*
* * * * * *
day of week (0 - 7) (0 or 7 is Sun)
month (1 - 12)
day of month (1 - 31)
hour (0 - 23)
minute (0 - 59)
second (0 - 59, OPTIONAL)
*/
@singleton
export default class GooglePurchaseSchedule {
async parseAllRecord() {
const timeKey = 'google_inapp_voided_check_time'
let timeStr = await new RedisClient().get(timeKey)
let startTime = timeStr ? parseInt(timeStr) : Date.now() - 24 * 60 * 60 * 1000
let endTime = Date.now()
try {
let res = await new GooglePaySvr().queryVoidedPurchases(startTime, endTime)
if (res.status !== 200) {
logger.info('error check google voided purchase', res.status, res.statusText)
return
}
const { data } = res
await GoogleInApp.parseVoidedRecords(data.voidedPurchases)
await new RedisClient().set(timeKey, endTime + '')
logger.info(`success check google voided purchase:: voidedPurchases: ${data.voidedPurchases?.length || 0}`)
} catch (err) {
logger.info('error check google voided purchase', err.message || err)
}
}
scheduleAll() {
const job = schedule.scheduleJob('1 * * * *', async () => {
await this.parseAllRecord()
})
this.parseAllRecord()
}
}

View File

@ -2,22 +2,31 @@ import axios from 'axios'
import { PayRecordClass } from 'modules/PayRecord' import { PayRecordClass } from 'modules/PayRecord'
import { DocumentType } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose'
import { hmacsha256 } from 'utils/security.util' import { hmacsha256 } from 'utils/security.util'
import { NetClient } from 'net/NetClient'
import logger from 'logger/logger'
export interface IPayResult {
productId: string
gameOrderId: string
orderId: string
status: number
}
export async function reportPayResult(data: DocumentType<PayRecordClass>) { export async function reportPayResult(data: DocumentType<PayRecordClass>) {
let repData = { const repData = {
account_id: data.gameAccountId, account_id: data.gameAccountId,
order_id: data.gameOrderId, order_id: data.gameOrderId,
status: data.status, status: data.status,
id: data.id, id: data.id,
txhash: data.txHash, txhash: data.txHash,
} }
let signStr = Object.keys(repData) const signStr = Object.keys(repData)
.sort() .sort()
.map(key => `${key}=${encodeURIComponent(repData[key])}`) .map(key => `${key}=${encodeURIComponent(repData[key])}`)
.join('&') .join('&')
const sign = hmacsha256(signStr, process.env.HASH_SALT) const sign = hmacsha256(signStr, process.env.HASH_SALT)
let url = `${process.env.GAME_PAY_CB_URL}&${signStr}&sign=${sign}` const url = `${process.env.GAME_PAY_CB_URL}?c=Shop&a=buyGoodsDirect&${signStr}&sign=${sign}`
let reqConfig: any = { const reqConfig: any = {
method: 'get', method: 'get',
url, url,
headers: { headers: {
@ -26,3 +35,27 @@ export async function reportPayResult(data: DocumentType<PayRecordClass>) {
} }
return axios(reqConfig) return axios(reqConfig)
} }
// 上报google支付结果
export async function reportGooglePurchaseResult(records: IPayResult[]) {
const url = `${process.env.GAME_PAY_CB_URL}?c=Shop&a=inappPurchaseDiamonds`
let reportData: any = {
channel: 'google',
records,
}
let signStr = 'channel=google&'
signStr += records
.map(record =>
Object.keys(record)
.sort()
.map(key => `${key}=${record[key]}`)
.join('&'),
)
.join('&')
const sign = hmacsha256(signStr, process.env.HASH_SALT)
reportData.sign = sign
const reqData = {
url,
data: JSON.stringify(reportData),
}
return new NetClient().httpPost(reqData)
}

View File

@ -43,12 +43,12 @@ export class GooglePaySvr {
* @param startTime * @param startTime
* @param endTime * @param endTime
*/ */
public async queryVoidedPurchases(startTime: string, endTime: string) { public async queryVoidedPurchases(startTime: number, endTime: number) {
return this.androidpublisher.purchases.voidedpurchases.list({ return this.androidpublisher.purchases.voidedpurchases.list({
packageName: PAGEAGE_NAME, packageName: PAGEAGE_NAME,
type: 0, type: 0,
startTime, startTime: startTime + '',
endTime, endTime: endTime + '',
}) })
} }
} }

View File

@ -272,6 +272,13 @@
resolved "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" resolved "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/redis@^2.8.28":
version "2.8.32"
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11"
integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==
dependencies:
"@types/node" "*"
"@types/semver@^7.3.12": "@types/semver@^7.3.12":
version "7.3.12" version "7.3.12"
resolved "https://registry.npmmirror.com/@types/semver/-/semver-7.3.12.tgz#920447fdd78d76b19de0438b7f60df3c4a80bf1c" resolved "https://registry.npmmirror.com/@types/semver/-/semver-7.3.12.tgz#920447fdd78d76b19de0438b7f60df3c4a80bf1c"
@ -737,6 +744,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
denque@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf"
integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==
denque@^2.1.0: denque@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" resolved "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
@ -2055,6 +2067,33 @@ real-require@^0.2.0:
resolved "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" resolved "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
redis-commands@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
redis-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
dependencies:
redis-errors "^1.0.0"
redis@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c"
integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==
dependencies:
denque "^1.5.0"
redis-commands "^1.7.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
reflect-metadata@^0.1.13: reflect-metadata@^0.1.13:
version "0.1.13" version "0.1.13"
resolved "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" resolved "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"