增加一些排行榜相关方法
This commit is contained in:
parent
0252c39e18
commit
2f646863ae
@ -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",
|
||||
|
@ -1,4 +1,5 @@
|
||||
export class BaseConst{
|
||||
public static readonly COMPOUND = 'compound'
|
||||
public static readonly MISSION = 'mission'
|
||||
public static readonly RANK_SCORE = 'rank_score'
|
||||
}
|
||||
|
268
src/redis/RedisClient.ts
Normal file
268
src/redis/RedisClient.ts
Normal file
@ -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<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) {
|
||||
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<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 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
131
src/services/Rank.ts
Normal file
131
src/services/Rank.ts
Normal file
@ -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<void>}
|
||||
*/
|
||||
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<unknown>}
|
||||
*/
|
||||
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<unknown>}
|
||||
*/
|
||||
export async function getAccountRank(accountId: string, subKey?: string) {
|
||||
return await new RedisClient().zrevrank(generateRankKey(subKey), accountId)
|
||||
}
|
||||
/**
|
||||
* 获取指定用户的积分
|
||||
* @param {string} accountId
|
||||
* @param subKey
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
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<unknown>}
|
||||
*/
|
||||
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<unknown>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
export async function delRankList(subKey?: string) {
|
||||
await new RedisClient().del(generateRankKey(subKey))
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在游戏中的状态
|
||||
* @param {string} accountId
|
||||
* @param seconds
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
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<unknown>}
|
||||
*/
|
||||
export async function checkGameing(accountId: string) {
|
||||
return await new RedisClient().get('gameing_' + accountId)
|
||||
}
|
@ -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
|
||||
})
|
||||
|
36
yarn.lock
36
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user