diff --git a/package.json b/package.json index 93e2600..627e3d7 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,6 @@ "author": "", "license": "ISC", "dependencies": { - "@typegoose/auto-increment": "^0.4.1", - "@typegoose/typegoose": "^7.4.6", "alipay-sdk": "^3.1.4", "axios": "^0.21.1", "bson": "^4.0.4", @@ -40,6 +38,7 @@ "mongoose-findorcreate": "^3.0.0", "qrcode": "^1.4.4", "querystring": "^0.2.1", + "redis": "^3.1.2", "reflect-metadata": "^0.1.13", "svg-captcha": "^1.4.0", "tracer": "^1.0.3", @@ -52,6 +51,9 @@ "@types/dotenv": "^8.2.0", "@types/mongoose": "5.10.3", "@types/node": "^14.14.20", + "@typegoose/auto-increment": "^0.4.1", + "@typegoose/typegoose": "^7.4.6", + "@types/redis": "^2.8.28", "ts-node": "^9.1.1", "tsconfig-paths": "^3.9.0", "tslint": "^6.1.1", diff --git a/src/constants/BaseConst.ts b/src/constants/BaseConst.ts index de870d1..4ee7af9 100644 --- a/src/constants/BaseConst.ts +++ b/src/constants/BaseConst.ts @@ -1,4 +1,5 @@ export class BaseConst{ public static readonly COMPOUND = 'compound' public static readonly MISSION = 'mission' + public static readonly RANK_SCORE = 'rank_score' } diff --git a/src/redis/RedisClient.ts b/src/redis/RedisClient.ts new file mode 100644 index 0000000..1e52ffb --- /dev/null +++ b/src/redis/RedisClient.ts @@ -0,0 +1,268 @@ +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 { + 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) { + 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 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 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 { + 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/services/Rank.ts b/src/services/Rank.ts new file mode 100644 index 0000000..223f349 --- /dev/null +++ b/src/services/Rank.ts @@ -0,0 +1,131 @@ +import { RedisClient } from '../redis/RedisClient' +import { BaseConst } from '../constants/BaseConst' + +const MAX_TIME = 5000000000000 + +function generateRankKey(subKey?: string) { + if (subKey) { + return `${BaseConst.RANK_SCORE}_${subKey}` + } else { + return BaseConst.RANK_SCORE + } +} +/** + * 更新排行榜数据 + * @param subKey + * @param {string} accountId + * @param {number} score + * @return {Promise} + */ +export async function updateRank(accountId: string, score: number, subKey?: string) { + let scoreL = parseFloat(`${score | 0}.${MAX_TIME - Date.now()}`) + await new RedisClient().zadd(generateRankKey(subKey), scoreL, accountId) +} + +/** + * 获取指定分数段的玩家 + * @param {number} min + * @param {number} max + * @param subKey + * @return {Promise} + */ +export async function usersByScore(min: number, max: number, subKey?: string) { + return await new RedisClient().zrangebyscore(generateRankKey(subKey), min, max) +} + +/** + * 获取指定用户的排名 + * @param {string} accountId + * @param subKey + * @return {Promise} + */ +export async function getAccountRank(accountId: string, subKey?: string) { + return await new RedisClient().zrevrank(generateRankKey(subKey), accountId) +} +/** + * 获取指定用户的积分 + * @param {string} accountId + * @param subKey + * @return {Promise} + */ +export async function getAccountScore(accountId: string, subKey?: string) { + return await new RedisClient().zscore(generateRankKey(subKey), accountId) +} + +/** + * 获取从start到end的玩家列表 + * @param {number} start + * @param {number} end + * @param subKey + * @return {Promise} + */ +export async function getRankList(start: number, end: number, subKey?: string) { + return await new RedisClient().zrevrange(generateRankKey(subKey), start, end) +} + +/** + * 获取帐号附近的几个记录 + * @param {string} accountId + * @param subKey + * @return {Promise} + */ +export async function getRankNear(accountId: string, subKey?: string) { + let rank: any = await getAccountRank(accountId) + if (rank == null) { + return [] + } + let total: any = await new RedisClient().zcard(generateRankKey(subKey)) + let start = 0 + let count = 2 + let end = start + count + if (rank > 0 && rank < total - 1) { + start = rank - 1 + end = start + count + } else if (rank == total - 1) { + start = rank - 2 + end = total + } + start = start < 0? 0 : start + end = end > total - 1 ? total - 1 : end + let datas: any = await getRankList(start, end) + let results: any[] = [] + for (let i = 0, l = datas.length; i < l; i += 2) { + results.push({ + rank: start + (i / 2), + accountId: datas[i], + score: datas[i + 1] << 0 + }) + } + return results +} + +/** + * 移除某个榜单 + * @param {string} subKey + * @return {Promise} + */ +export async function delRankList(subKey?: string) { + await new RedisClient().del(generateRankKey(subKey)) +} + +/** + * 设置在游戏中的状态 + * @param {string} accountId + * @param seconds + * @return {Promise} + */ +export async function setGameing(accountId: string, seconds: number) { + await new RedisClient().setex('gameing_' + accountId, (Date.now() / 1000 | 0) + '', seconds) +} + +export async function setGameEnd(accountId: string) { + await new RedisClient().expire('gameing_' + accountId, 0) +} +/** + * 检查是否在游戏中 + * @param {string} accountId + * @return {Promise} + */ +export async function checkGameing(accountId: string) { + return await new RedisClient().get('gameing_' + accountId) +} diff --git a/src/services/WsSvr.ts b/src/services/WsSvr.ts index d729b9c..59279f4 100644 --- a/src/services/WsSvr.ts +++ b/src/services/WsSvr.ts @@ -18,7 +18,7 @@ export async function sendMsg(roomId, clientId, type, msg) { method: 'smsg', args: JSON.stringify(args) } - return axios.get(url, {params: data}) + return axios.post(url, {params: data}) .then(res => { return res.data }) @@ -38,7 +38,7 @@ export async function broadcast(roomId, type, msg) { type, msg } - return axios.get(url, {params: data}) + return axios.post(url, {params: data}) .then(res => { return res.data }) @@ -58,7 +58,7 @@ export async function kickClient(roomId, clientId) { method: '_forceClientDisconnect', args: JSON.stringify(args) } - return axios.get(url, {params: data}) + return axios.post(url, {params: data}) .then(res => { return res.data }) diff --git a/yarn.lock b/yarn.lock index 9dbaea5..83a40aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -137,6 +137,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.61.tgz#29f124eddd41c4c74281bd0b455d689109fc2a2d" integrity sha512-/aKAdg5c8n468cYLy2eQrcR5k6chlbNwZNGUj3TboyPa2hcO2QAJcfymlqPzMiRj8B6nYKXjzQz36minFE0RwQ== +"@types/redis@^2.8.28": + version "2.8.28" + resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.28.tgz#5862b2b64aa7f7cbc76dafd7e6f06992b52c55e3" + integrity sha512-8l2gr2OQ969ypa7hFOeKqtFoY70XkHxISV0pAwmQ2nm6CSPb1brmTmqJCGGrekCo+pAZyWlNXr+Kvo6L/1wijA== + dependencies: + "@types/node" "*" + abstract-logging@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" @@ -621,7 +628,7 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -denque@^1.4.1: +denque@^1.4.1, denque@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de" integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ== @@ -1943,6 +1950,33 @@ readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +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 sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + 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: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"