添加排行榜相关接口

This commit is contained in:
zhl 2021-01-29 12:12:38 +08:00
parent 41ca21e2ca
commit b7f2fd0d95
7 changed files with 321 additions and 61 deletions

View File

@ -570,6 +570,66 @@
]
```
### 22. 排行榜
1. Method: POST
2. URI: /api/:accountid/rank
| 字段 | 说明 |
| -------- | -------------------------------------- |
| accountid | 帐号id |
> POST参数
| 字段 |说明 |
| -------- | -------------------------------------- |
|limit |获取的数据数量 |
|skip |skip数量 |
3. Response: JSON
```js
{"records": [
{
"rank": 0,
"accountid": "6000_3200_QcYw2AAK6Vf95WNfRmHYly340J455zZR",
"nickname": "Sunny",
"avatar": "https://resource.kingsome.cn/matchvs_cdn/1.0.0.1/avatar/10_2.jpg",
"score": 1000
},
],
"userRank": 0, //当前玩家的排名
"userScore": 1000 //当前玩家的积分
}
```
### 23. 排行榜-附近的玩家
1. Method: POST
2. URI: /api/:accountid/rank/nearMe
| 字段 | 说明 |
| -------- | -------------------------------------- |
| accountid | 帐号id |
3. Response: JSON
```js
{
"rank": 0,
"accountid": "6000_3200_QcYw2AAK6Vf95WNfRmHYly340J455zZR",
"nickname": "Sunny",
"avatar": "https://resource.kingsome.cn/matchvs_cdn/1.0.0.1/avatar/10_2.jpg",
"score": 1000
},
```

View File

@ -580,6 +580,13 @@ interface Array<T> {
* @param arr
*/
difference?<T>(arr: T[]): T[];
/**
* Map
* @param {string} key map的key字段名
* @return {Map<any, T>}
*/
toMap?<T>(key: string): Map<any, T>;
}
Object.defineProperties(Array.prototype, {
@ -858,6 +865,18 @@ Object.defineProperties(Array.prototype, {
return [...set1];
},
writable: true
},
toMap: {
value: function<T> (this: T[], key: string) {
let result: Map<any, T> = new Map()
for(const o of this) {
// @ts-ignore
result.set(o[key], o)
}
return result
},
writable: true
}
});

View File

@ -6,7 +6,7 @@ import { BagItem, ItemType } from '../models/BagItem'
import { RedisClient } from '../redis/RedisClient'
import {
checkGameing,
getRankScore,
getAccountRank,
setGameing,
usersByScore
} from '../service/rank'
@ -37,7 +37,7 @@ export default class AccountController extends BaseController {
result.season_score = account.season_score
result.season_data = account.season_data
result.match_score = account.getMatchScore()
let rank = await getRankScore(accountid)
let rank = await getAccountRank(accountid)
if (typeof rank === 'string') {
result.rank = parseInt(rank)
} else {

View File

@ -1,5 +1,77 @@
import BaseController from '../common/base.controller'
import { router } from '../decorators/router'
import {
getAccountRank,
getAccountScore,
getRankList,
getRankNear
} from '../service/rank'
import { User } from '../models/User'
export default class RankController extends BaseController {
@router('post /api/games/rank')
@router('post /api/:accountid/rank')
async rank(req: any) {
let { gameId, channelId, limit, skip, accountid } = req.params
let start = skip || 0
let end = start + limit
let datas: any = await getRankList(start, end)
let accountIds: string[] = []
let scoreMap: Map<any, number> = new Map()
for (let i = 0, l = datas.length; i < l; i += 2) {
accountIds.push(datas[i])
scoreMap.set(datas[i], datas[i + 1] << 0)
}
const accounts = await User.find({_id : {$in: accountIds}})
const accountMap: Map<string, any> = accounts.toMap('_id')
let results: any = []
let i = 0
for (let aid of accountIds) {
const account = accountMap.get(aid)
results.push({
rank: start + i ++,
accountid: aid,
nickname: account.nickname,
avatar: account.avatar,
score: scoreMap.get(aid)
})
}
let userRank = await getAccountRank(accountid)
// @ts-ignore
let userScore = (await getAccountScore(accountid)) << 0
return {
records: results,
userRank,
userScore
}
}
@router('post /api/:accountid/rank/nearMe')
@router('post /games/rank/nearMe')
async nearMe(req: any) {
let { gameId, channelId, score, accountid } = req.params
let datas = await getRankNear(accountid)
let accountIds: string[] = []
let scoreMap: Map<any, any> = new Map()
for (let data of datas) {
accountIds.push(data.accountid)
scoreMap.set(data.accountid, data)
}
const accounts = await User.find({_id : {$in: accountIds}})
const accountMap: Map<string, any> = accounts.toMap('_id')
let results: any = []
for (let aid of accountIds) {
const account = accountMap.get(aid)
results.push({
rank: scoreMap.get(aid).rank,
accountid: aid,
nickname: account.nickname,
avatar: account.avatar,
score: scoreMap.get(aid).score
})
}
return results
}
}

View File

@ -1,72 +1,73 @@
import {
FastifyInstance,
FastifyPluginAsync, FastifyReply,
FastifyRequest,
} from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import {User} from '../models/User';
import {ZError} from "../common/ZError";
FastifyInstance,
FastifyPluginAsync,
FastifyReply,
FastifyRequest
} from 'fastify'
import fastifyPlugin from 'fastify-plugin'
import { User } from '../models/User'
import { ZError } from '../common/ZError'
declare module 'fastify' {
interface FastifyInstance {
apiAuth: (request: FastifyRequest, reply: FastifyReply) => {};
}
interface FastifyInstance {
apiAuth: (request: FastifyRequest, reply: FastifyReply) => {};
}
interface FastifyRequest {
session?: {[key: string]: any};
roles?: any,
user?: any
}
interface FastifyRequest {
session?: { [key: string]: any };
roles?: any,
user?: any
}
}
const apiAuthPlugin: FastifyPluginAsync = async function(
fastify: FastifyInstance,
options?: any
const apiAuthPlugin: FastifyPluginAsync = async function (
fastify: FastifyInstance,
options?: any
) {
// 只有路由配置的role为anon才不需要过滤
fastify.decorate("apiAuth", async function(request: FastifyRequest, reply: FastifyReply) {
if (request.url.startsWith('/svr')) {
// @ts-ignore
let { accountid } = request.params;
if (accountid) {
request.user = await User.findById(accountid);
}
} else {
if (!request.roles || request.roles.indexOf('anon') == -1) {
try {
// @ts-ignore
let { accountid, sessionid } = request.params;
//TODO: 增加sessionid的校验
// if (!accountid || !sessionid) {
// return reply.send({code: 11, msg: 'need accountid and sessionid'});
// }
if (!accountid) {
return reply.send({code: 2, msg: 'need accountid and sessionid'});
}
// const data = this.jwt.verify(request.token);
// if (!data || !data.id) {
// return reply.send({code: 10, msg: 'need login'});
// }
let account = await User.findById(accountid);
if (!account) {
return reply.send({code: 5, msg: 'account not found'});
}
if (account.locked) {
return reply.send({code: 4, msg: 'account locked'});
}
request.user = account;
} catch (err) {
return reply.send({code: 401, msg: 'need auth'})
}
}
// 只有路由配置的role为anon才不需要过滤
fastify.decorate('apiAuth', async function (request: FastifyRequest, reply: FastifyReply) {
if (request.url.startsWith('/svr')) {
// @ts-ignore
let { accountid } = request.params
if (accountid) {
request.user = await User.findById(accountid)
}
} else {
if (!request.roles || request.roles.indexOf('anon') == -1) {
try {
// @ts-ignore
let { accountid, sessionid } = request.params
//TODO: 增加sessionid的校验
// if (!accountid || !sessionid) {
// return reply.send({code: 11, msg: 'need accountid and sessionid'});
// }
if (!accountid) {
throw new ZError(2, 'need accountid and sessionid')
}
// const data = this.jwt.verify(request.token);
// if (!data || !data.id) {
// return reply.send({code: 10, msg: 'need login'});
// }
let account = await User.findById(accountid)
if (!account) {
throw new ZError(5, 'account not found')
}
if (account.locked) {
throw new ZError(4, 'account locked')
}
request.user = account
} catch (err) {
throw err
}
}
}
})
};
})
}
export = fastifyPlugin(apiAuthPlugin, {
fastify: '>=3.0.0',
name: 'apiauth',
fastify: '>=3.0.0',
name: 'apiauth'
});

View File

@ -170,6 +170,15 @@ export class RedisClient {
});
}
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) => {
@ -179,6 +188,25 @@ export class RedisClient {
});
}
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);

View File

@ -3,26 +3,106 @@ import { BaseConst } from '../constants/BaseConst'
const MAX_TIME = 5000000000000
/**
*
* @param {string} accountid
* @param {number} score
* @return {Promise<void>}
*/
export async function updateRank(accountid: string, score: number) {
let scoreL = parseFloat(`${score | 0}.${MAX_TIME - Date.now()}`)
await new RedisClient().zadd(BaseConst.RANK_SCORE, scoreL, accountid)
}
/**
*
* @param {number} min
* @param {number} max
* @return {Promise<unknown>}
*/
export async function usersByScore(min: number, max: number) {
return await new RedisClient().zrangebyscore(BaseConst.RANK_SCORE, min, max)
}
/**
*
* @param {string} accountid
* @return {Promise<void>}
*/
export async function setGameing(accountid: string) {
await new RedisClient().setex('gameing_' + accountid, (Date.now() / 1000 | 0) + '', 30)
}
/**
*
* @param {string} accountid
* @return {Promise<unknown>}
*/
export async function checkGameing(accountid: string) {
return await new RedisClient().get('gameing_' + accountid)
}
export async function getRankScore(accountid: string) {
/**
*
* @param {string} accountid
* @return {Promise<unknown>}
*/
export async function getAccountRank(accountid: string) {
return await new RedisClient().zrevrank(BaseConst.RANK_SCORE, accountid)
}
/**
*
* @param {string} accountid
* @return {Promise<unknown>}
*/
export async function getAccountScore(accountid: string) {
return await new RedisClient().zscore(BaseConst.RANK_SCORE, accountid)
}
/**
* start到end的玩家列表
* @param {number} start
* @param {number} end
* @return {Promise<unknown>}
*/
export async function getRankList(start: number, end: number) {
return await new RedisClient().zrevrange(BaseConst.RANK_SCORE, start, end)
}
/**
*
* @param {string} accountid
* @return {Promise<unknown>}
*/
export async function getRankNear(accountid: string) {
let rank: any = await getAccountRank(accountid)
if (rank == null) {
return []
}
let total: any = await new RedisClient().zcard(BaseConst.RANK_SCORE)
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
}