From b69953fd512e784b69e41dfcd84b3b4d3f2d8da7 Mon Sep 17 00:00:00 2001 From: zhl Date: Thu, 28 Jan 2021 16:32:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0redis=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api.md | 35 +++++ package-lock.json | 34 +++++ package.json | 2 + src/api.server.ts | 13 ++ src/constants/BaseConst.ts | 9 ++ src/controllers/AccountController.ts | 20 +++ src/redis/RedisClient.ts | 212 +++++++++++++++++++++++++++ src/service/jcfw.ts | 51 ++++++- src/utils/security.util.ts | 10 ++ 9 files changed, 381 insertions(+), 5 deletions(-) create mode 100644 src/redis/RedisClient.ts create mode 100644 src/utils/security.util.ts diff --git a/docs/api.md b/docs/api.md index 494054b..2d618cb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -650,6 +650,41 @@ }] ``` +### 4. 获取用户的简要信息 +1. Method: GET +2. URI: /svr/:accountid/uinfo + +| 字段 | 说明 | +| -------- | -------------------------------------- | +| accountid | 帐号id | + +3. Response: JSON + + + +```js +{ + nickname: '阿三', // 英雄id + avatar: true, // 头像 +} +``` + +### 5. 随机获取一个机器人信息 +1. Method: GET +2. URI: /svr/randomrobot + + + + +3. Response: JSON + + +```js +{ + nickname: '阿三', // 英雄id + avatar: true, // 头像 +} +``` diff --git a/package-lock.json b/package-lock.json index 88d6407..21094c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==" }, + "@types/redis": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.28.tgz", + "integrity": "sha512-8l2gr2OQ969ypa7hFOeKqtFoY70XkHxISV0pAwmQ2nm6CSPb1brmTmqJCGGrekCo+pAZyWlNXr+Kvo6L/1wijA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -305,6 +314,11 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -970,6 +984,26 @@ "util-deprecate": "^1.0.1" } }, + "redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", + "requires": { + "double-ended-queue": "^2.1.0-0", + "redis-commands": "^1.2.0", + "redis-parser": "^2.6.0" + } + }, + "redis-commands": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", + "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==" + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", diff --git a/package.json b/package.json index 394922a..f78f908 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "mime-types": "^2.1.28", "mongoose": "5.10.3", "mongoose-findorcreate": "^3.0.0", + "redis": "^2.8.0", "tracer": "^1.1.4", "urlencode": "^1.1.0" }, @@ -35,6 +36,7 @@ "@types/debug": "4.1.5", "@types/mongoose": "5.10.3", "@types/node": "^14.14.20", + "@types/redis": "^2.8.28", "ts-node": "^9.1.1", "tsconfig-paths": "^3.9.0", "typescript": "^4.1.3" diff --git a/src/api.server.ts b/src/api.server.ts index fe594a4..468a0cb 100644 --- a/src/api.server.ts +++ b/src/api.server.ts @@ -11,6 +11,9 @@ import {mongoose} from "@typegoose/typegoose"; import logger from './logger/logger'; import {Config} from "./cfg/Config"; import {initData} from "./common/GConfig"; +import { RedisClient } from './redis/RedisClient' +import { initRobotCache } from './service/jcfw' +import { error } from './common/Debug' const zReqParserPlugin = require('./plugins/zReqParser'); const apiAuthPlugin = require('./plugins/apiauth'); @@ -84,6 +87,8 @@ export class ApiServer { } catch (err) { logger.log(`DB Connection Error: ${err.message}`); } + let opts = {url: config.redis} + new RedisClient(opts) } private setErrHandler() { this.server.setNotFoundHandler(function(request: any, reply: { send: (arg0: { errcode: number; errmsg: string; }) => void; }){ @@ -124,10 +129,18 @@ export class ApiServer { return payload }) } + private async initCache() { + try { + await initRobotCache(100) + } catch (err) { + error('error init robot info', err) + } + } public async start() { let self = this; return new Promise(async (resolve, reject) => { await self.connectDB(); + await self.initCache(); self.initControllers(); self.registerRouter(); self.setErrHandler(); diff --git a/src/constants/BaseConst.ts b/src/constants/BaseConst.ts index 27d418d..b9e886b 100644 --- a/src/constants/BaseConst.ts +++ b/src/constants/BaseConst.ts @@ -72,4 +72,13 @@ export class BaseConst { public static readonly ITEMCARD = "itemcard"; public static readonly MATCH = "match"; public static readonly ITEMFUNC = "itemfunc"; + /** + * 缓存的机器人信息在redis中的key + */ + public static readonly ROBOT_INFO = 'robot_info' + /** + * 缓存的机器人信息昵称和头像的分割字符 + */ + public static readonly ROBOT_INFO_SEP = '|||' + } diff --git a/src/controllers/AccountController.ts b/src/controllers/AccountController.ts index 2392676..886aced 100644 --- a/src/controllers/AccountController.ts +++ b/src/controllers/AccountController.ts @@ -7,6 +7,7 @@ import { BaseConst } from '../constants/BaseConst' import { Hero } from '../models/subdoc/Hero' import { BagItem, ItemType } from '../models/BagItem' import { addHeroDefaultCardGroup } from '../dao/CardGroupDao' +import { RedisClient } from '../redis/RedisClient' export default class AccountController extends BaseController { @role('anon') @@ -75,6 +76,25 @@ export default class AccountController extends BaseController { result.match_score = account.getMatchScore() return result } + @router('get /svr/:accountid/uinfo') + async simpleInfo(req: any) { + let account = req.user + return { + nickname: account.nickname, + avatar: account.avatar + } + } + + @router('get /svr/randomrobot') + async randomRobot(req: any) { + // @ts-ignore + let str: string = await new RedisClient().srandmember(BaseConst.ROBOT_INFO) + let arr = str.split(BaseConst.ROBOT_INFO_SEP) + return { + nickname: arr[0], + avatar: arr[1] + } + } @router('post /api/:accountid/season_data') async seasonData(req: any) { diff --git a/src/redis/RedisClient.ts b/src/redis/RedisClient.ts new file mode 100644 index 0000000..c819978 --- /dev/null +++ b/src/redis/RedisClient.ts @@ -0,0 +1,212 @@ +import redis from 'redis'; +import { promisify } from 'util'; +import { singleton } from '../decorators/singleton.decorator' + +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 { + 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 get(key: string) { + return new Promise((resolve, reject) => { + this.pub.get(key, (err, data) => { + if (err) { return reject(err); } + resolve(data); + }); + }); + } + + 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 { + return await this.smembersAsync(key); + } + + public async sismember(key: string, field: string): Promise { + 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((resolve, reject) => { + this.pub.sinter(...keys, (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 { + return await this.hlenAsync(key); + } + + public async incr(key: string): Promise { + return await this.incrAsync(key); + } + + public async decr(key: string): Promise { + 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)); + } + } + } +} diff --git a/src/service/jcfw.ts b/src/service/jcfw.ts index 74b1936..7a4f551 100644 --- a/src/service/jcfw.ts +++ b/src/service/jcfw.ts @@ -1,19 +1,60 @@ -import { generateKeyValStr } from '../utils/string.util' import axios from 'axios' +import { createSign } from '../utils/security.util' +import { RedisClient } from '../redis/RedisClient' +import { BaseConst } from '../constants/BaseConst' const CONFIG_URL = 'https://center.kingsome.cn/api/cfg_list' - /** * 获取jcfw中该游戏的配置 * @param {string} gameid * @param {string} channel */ export function getGameConfig(gameid: string, channel: string) { - let url = `${ CONFIG_URL }?game_id=${gameid}&channel_id=${channel}` + let url = `${ CONFIG_URL }?game_id=${ gameid }&channel_id=${ channel }` return axios.get(url).then((res: any) => { - if (res.data.errorcode && res.data.result) { - throw new Error(`error get game cfg, code: ${res.errorcode}, msg: ${res.errmsg}`) + if (res.data.errorcode || !res.data.result) { + throw new Error(`error get game cfg, code: ${ res.errorcode }, msg: ${ res.errmsg }`) } return JSON.parse(res.data.result) }) } + +/** + * 获取随机数量的机器人数据 + * @param {string | undefined} accountId + * @param {number} num + * @return {Promise>} + */ +export function randomUser(accountId: string | undefined, num: number) { + const excludeAccountids = accountId ? `[${ accountId }]` : '[]' + const timestamp = new Date().getTime() + const url = 'https://service.kingsome.cn/webapp/index.php?c=Voodoo&a=getRobotList' + const paramStr = `exclude_accountids=${ excludeAccountids }&exclude_names=[]&num=${ num }` + const signStr = createSign('70e32abc60367adccaa9eb7b56ed821b', paramStr, timestamp) + const link = `${ url }&${ paramStr }&sign=${ signStr }×tamp=${ timestamp }` + return axios.get(link).then((res: any) => { + if (res.data.errorcode || !res.data.robot_list) { + throw new Error(`error get robo list, code: ${ res.errorcode }, msg: ${ res.errmsg }`) + } + return res.data.robot_list + }) +} + +/** + * 更新redis中机器人数据 + * @param {number} count + * @param {boolean} force + */ +export async function initRobotCache(count: number, force?: boolean) { + const redisClient = new RedisClient() + const countExists = await redisClient.scard(BaseConst.ROBOT_INFO) + if (countExists > count && !force) { + return + } + let robots = await randomUser('', 100) + if (robots && Array.isArray(robots)) { + for(const robot of robots) { + await redisClient.sadd(BaseConst.ROBOT_INFO, `${robot.nickname}${BaseConst.ROBOT_INFO_SEP}${robot.avatar_url}`) + } + } +} diff --git a/src/utils/security.util.ts b/src/utils/security.util.ts new file mode 100644 index 0000000..5cb8e4e --- /dev/null +++ b/src/utils/security.util.ts @@ -0,0 +1,10 @@ + +import crypto from 'crypto' + +// 生成签名字段 +// paramStr 为key1=val1&key2=val2, key1, key2按字母升序 + +export function createSign(secretKey: string, paramStr: string, timestamp: number) { + paramStr = `${paramStr}:${timestamp}${secretKey}`; + return crypto.createHash('md5').update(paramStr, 'utf8').digest('hex'); +}