From bc17ea9f3a1a218ec4e620bffbc10bf4a82f0f1c Mon Sep 17 00:00:00 2001 From: CounterFire2023 <136581895+CounterFire2023@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:55:52 +0800 Subject: [PATCH] add api for leaderboard --- initdatas/activity_info.json | 9 ------ src/controllers/activity.controller.ts | 20 ++++++++++-- src/controllers/tasks.controller.ts | 39 ++++++++++++++++------ src/models/ScoreRecord.ts | 32 ++++++++++++++++++ src/redis/RedisClient.ts | 10 ++++-- src/tasks/UpdateScore.ts | 45 ++++++++++++++++++++------ 6 files changed, 122 insertions(+), 33 deletions(-) create mode 100644 src/models/ScoreRecord.ts diff --git a/initdatas/activity_info.json b/initdatas/activity_info.json index 51c1050..7a4e360 100644 --- a/initdatas/activity_info.json +++ b/initdatas/activity_info.json @@ -9,15 +9,6 @@ }, { "task": "TwitterFollow", "params": {"time": 6, "failRate": 60} - }, { - "task": "TwitterRetweet", - "params": {"time": 6, "failRate": 60} - }, { - "task": "DiscordJoin", - "params": {} - }, { - "task": "DiscordRole", - "params": {"time": 6, "failRate": 60} }, { "task": "UpdateScore", "params": {"score": [100, 20]} diff --git a/src/controllers/activity.controller.ts b/src/controllers/activity.controller.ts index 620935d..6dfc7e8 100644 --- a/src/controllers/activity.controller.ts +++ b/src/controllers/activity.controller.ts @@ -3,6 +3,7 @@ import BaseController, { ROLE_ANON } from "common/base.controller"; import { role, router } from "decorators/router"; import { ActivityInfo } from "models/ActivityInfo"; import { ActivityUser } from "models/ActivityUser"; +import { RedisClient } from "redis/RedisClient"; export default class ActivityController extends BaseController { @@ -37,9 +38,24 @@ export default class ActivityController extends BaseController { throw new ZError(12, 'invalid invite code') } user.inviteUser = inviteUser.id + await user.save() return {} } - - + @router('get /api/activity/leaderboard') + async inviteCode(req) { + let user = req.user; + let records = await new RedisClient().zrevrange(`${user.activity}:score`, 0, 9) + let results: any = [] + for (let i = 0; i < records.length; i+=2) { + let id = records[i] + let score = parseInt(records[i + 1]) + let user = await ActivityUser.findById(id) + results.push({ + address: user.address, + score + }) + } + return results; + } } \ No newline at end of file diff --git a/src/controllers/tasks.controller.ts b/src/controllers/tasks.controller.ts index be6bb72..479491d 100644 --- a/src/controllers/tasks.controller.ts +++ b/src/controllers/tasks.controller.ts @@ -25,23 +25,34 @@ const initTasks = () => { } const allTasks = initTasks(); +const findNextTask = (user: typeof ActivityUser, task: string) => { + let currentTask = null; + for (let taskData of user.taskProgress) { + if (taskData.id === task) { + currentTask = taskData; + continue; + } + if (currentTask && currentTask.status === TaskStatusEnum.SUCCESS) { + return taskData; + } + } + return null; +} + const parseCurrentTask = (user: typeof ActivityUser, task: string) => { if (!allTasks.has(task)) { throw new ZError(11, 'invalid task') } let preEnd = true; let currentTask = null; - let nextTask = null; for (let taskData of user.taskProgress) { if (taskData.id === task) { currentTask = taskData; + break; } - if (taskData.status !== 2 && !currentTask) { + if (taskData.status !== TaskStatusEnum.SUCCESS && !currentTask) { preEnd = false; } - if (currentTask && !nextTask) { - nextTask = taskData; - } } if (!preEnd) { throw new ZError(12, 'task previous not end') @@ -49,19 +60,24 @@ const parseCurrentTask = (user: typeof ActivityUser, task: string) => { if (!currentTask) { throw new ZError(13, 'task not found') } - return {preEnd, currentTask, nextTask} + return {preEnd, currentTask} } const parseNextTask = async ( user: typeof ActivityUser, activity: typeof ActivityInfo, - task: TaskStatus) => { - let Task = require('../tasks/' + task.id); + task: string) => { + let nextTask = findNextTask(user, task); + if (!nextTask) { + return true + } + let Task = require('../tasks/' + nextTask.id); if (!Task.default.auto) { return true } let taskInstance = new Task.default({user, activity}); let result = await taskInstance.execute({}); + await parseNextTask(user, activity, nextTask.id); return result } export default class TasksController extends BaseController { @@ -110,7 +126,10 @@ export default class TasksController extends BaseController { let user = req.user; let activity = req.activity; let { task } = req.params; - let { currentTask, nextTask } = parseCurrentTask(user, task); + let { currentTask } = parseCurrentTask(user, task); + if (currentTask.status === TaskStatusEnum.SUCCESS) { + return currentTask; + } if (currentTask.status !== TaskStatusEnum.RUNNING) { throw new ZError(11, 'task not begin'); } @@ -119,7 +138,7 @@ export default class TasksController extends BaseController { let taskInstance = new Task.default({user, activity}); let result = await taskInstance.execute({}); if (result) { - await parseNextTask(user, activity, nextTask); + await parseNextTask(user, activity, task); } } return currentTask diff --git a/src/models/ScoreRecord.ts b/src/models/ScoreRecord.ts new file mode 100644 index 0000000..03bf720 --- /dev/null +++ b/src/models/ScoreRecord.ts @@ -0,0 +1,32 @@ +import { Severity, getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from './Base' + +export enum ScoreTypeEnum { + INVITE = 1, +} + +@dbconn() +@index({ user: 1 }, { unique: false }) +@index({ activity: 1 }, { unique: false }) +@modelOptions({ schemaOptions: { collection: 'score_record', timestamps: true }, options: { allowMixed: Severity.ALLOW } }) +class ScoreRecordClass extends BaseModule { + @prop({ required: true}) + public user: string + + @prop({ required: true}) + public activity: string + + @prop() + public score: number + + @prop({ enum: ScoreTypeEnum }) + public type: ScoreTypeEnum + + @prop({ type: mongoose.Schema.Types.Mixed }) + public data: any + +} + +export const ScoreRecord = getModelForClass(ScoreRecordClass, { existingConnection: ScoreRecordClass['db'] }) + diff --git a/src/redis/RedisClient.ts b/src/redis/RedisClient.ts index 2eee25c..e6245b2 100644 --- a/src/redis/RedisClient.ts +++ b/src/redis/RedisClient.ts @@ -1,4 +1,3 @@ -import { resolveCname } from 'dns' import redis from 'redis' import { promisify } from 'util' import { singleton } from '../decorators/singleton' @@ -180,6 +179,13 @@ export class RedisClient { this.pub.zadd(key, value, member, resolve) }) } + + public async zincrby(key: string, value: any, member: string) { + return new Promise(resolve => { + this.pub.zincrby(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) => { @@ -235,7 +241,7 @@ export class RedisClient { }) } - public async zrevrange(key: string, start: number, end: number) { + public async zrevrange(key: string, start: number, end: number): Promise { return new Promise((resolve, reject) => { this.pub.zrevrange(key, start, end, 'withscores', (err, data) => { if (err) { diff --git a/src/tasks/UpdateScore.ts b/src/tasks/UpdateScore.ts index 736e197..90008bc 100644 --- a/src/tasks/UpdateScore.ts +++ b/src/tasks/UpdateScore.ts @@ -1,6 +1,33 @@ -import { ActivityUser, TaskStatus } from "models/ActivityUser"; +import { ActivityUser, TaskStatus, TaskStatusEnum } from "models/ActivityUser"; import { ITask } from "./base/ITask"; import { TaskCfg } from "models/ActivityInfo"; +import { ScoreRecord, ScoreTypeEnum } from "models/ScoreRecord"; +import { RedisClient } from "redis/RedisClient"; + +const updateInviteScore = async (user: typeof ActivityUser, scores: number[], level: number) => { + if (!user.inviteUser || scores.length <= level) { + return; + } + let userSup = await ActivityUser.findById(user.inviteUser) + if (!userSup) { + return; + } + let record = new ScoreRecord({ + user: userSup.id, + activity: user.activity, + score: scores[level], + type: ScoreTypeEnum.INVITE, + data: { + user: user.id, + level + } + }) + await record.save(); + const key = `${user.activity}:score` + const score = scores[level] + 1 - (Date.now() / 1000 / 10000000000) + await new RedisClient().zincrby(key, score, userSup.id); + await updateInviteScore(userSup, scores, level + 1) +} export default class UpdateScore extends ITask { static desc = 'update invite score' @@ -9,15 +36,13 @@ export default class UpdateScore extends ITask { async execute(data: any) { let task = this.params.user.taskProgress.find((t: TaskStatus) => t.id === this.constructor.name) let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.task === this.constructor.name) - let scores = cfg.params.scores; - for (let i = 0, l = scores.length; i < l; i++) { - let score = scores[i] - let user = await ActivityUser.findById(this.params.user.inviteUser) - if (user) { - - } - - } + let scores = cfg.params.score; + task.status = TaskStatusEnum.SUCCESS + task.timeFinish = Date.now() + task.data = {} + await this.params.user.save(); + // According to configuration, add score to user who invite current user + await updateInviteScore(this.params.user, scores, 0) return true }