add api for leaderboard

This commit is contained in:
CounterFire2023 2023-12-21 17:55:52 +08:00
parent 5d2a3af490
commit bc17ea9f3a
6 changed files with 122 additions and 33 deletions

View File

@ -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]}

View File

@ -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;
}
}

View File

@ -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

32
src/models/ScoreRecord.ts Normal file
View File

@ -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'] })

View File

@ -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<string[]> {
return new Promise((resolve, reject) => {
this.pub.zrevrange(key, start, end, 'withscores', (err, data) => {
if (err) {

View File

@ -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
}