diff --git a/docs/api.md b/docs/api.md index 48d3da1..7c5fb8f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 +}, + +``` + diff --git a/src/common/Extend.ts b/src/common/Extend.ts index c20b111..5909bf9 100644 --- a/src/common/Extend.ts +++ b/src/common/Extend.ts @@ -580,6 +580,13 @@ interface Array { * @param arr */ difference?(arr: T[]): T[]; + + /** + * 转换成Map + * @param {string} key 用于生成map的key字段名 + * @return {Map} + */ + toMap?(key: string): Map; } Object.defineProperties(Array.prototype, { @@ -858,6 +865,18 @@ Object.defineProperties(Array.prototype, { return [...set1]; }, writable: true + }, + + toMap: { + value: function (this: T[], key: string) { + let result: Map = new Map() + for(const o of this) { + // @ts-ignore + result.set(o[key], o) + } + return result + }, + writable: true } }); diff --git a/src/controllers/AccountController.ts b/src/controllers/AccountController.ts index fd33d68..754b0a2 100644 --- a/src/controllers/AccountController.ts +++ b/src/controllers/AccountController.ts @@ -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 { diff --git a/src/controllers/RankController.ts b/src/controllers/RankController.ts index 2107237..a3384e6 100644 --- a/src/controllers/RankController.ts +++ b/src/controllers/RankController.ts @@ -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 = 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 = 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 = 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 = 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 + } } diff --git a/src/plugins/apiauth.ts b/src/plugins/apiauth.ts index 94a8a22..b70aea9 100644 --- a/src/plugins/apiauth.ts +++ b/src/plugins/apiauth.ts @@ -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' }); diff --git a/src/redis/RedisClient.ts b/src/redis/RedisClient.ts index ab0d6d4..e5e8170 100644 --- a/src/redis/RedisClient.ts +++ b/src/redis/RedisClient.ts @@ -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); diff --git a/src/service/rank.ts b/src/service/rank.ts index d090fa5..5ab0b51 100644 --- a/src/service/rank.ts +++ b/src/service/rank.ts @@ -3,26 +3,106 @@ import { BaseConst } from '../constants/BaseConst' const MAX_TIME = 5000000000000 +/** + * 更新排行榜数据 + * @param {string} accountid + * @param {number} score + * @return {Promise} + */ 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} + */ export async function usersByScore(min: number, max: number) { return await new RedisClient().zrangebyscore(BaseConst.RANK_SCORE, min, max) } +/** + * 设置在游戏中的状态 + * @param {string} accountid + * @return {Promise} + */ export async function setGameing(accountid: string) { await new RedisClient().setex('gameing_' + accountid, (Date.now() / 1000 | 0) + '', 30) } +/** + * 检查是否在游戏中 + * @param {string} accountid + * @return {Promise} + */ export async function checkGameing(accountid: string) { return await new RedisClient().get('gameing_' + accountid) } -export async function getRankScore(accountid: string) { +/** + * 获取指定用户的排名 + * @param {string} accountid + * @return {Promise} + */ +export async function getAccountRank(accountid: string) { return await new RedisClient().zrevrank(BaseConst.RANK_SCORE, accountid) } +/** + * 获取指定用户的积分 + * @param {string} accountid + * @return {Promise} + */ +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} + */ +export async function getRankList(start: number, end: number) { + return await new RedisClient().zrevrange(BaseConst.RANK_SCORE, start, end) +} + +/** + * 获取帐号附近的几个记录 + * @param {string} accountid + * @return {Promise} + */ +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 +}