修改task完成机制

This commit is contained in:
CounterFire2023 2024-01-03 19:10:05 +08:00
parent f1a7b1f7e9
commit e9b0f802c1
16 changed files with 368 additions and 134 deletions

View File

@ -93,7 +93,12 @@ SiweMessage说明: https://docs.login.xyz/sign-in-with-ethereum/quickstart-guide
{
"id": "任务id",
"title": "任务名",
"desc": "任务描述"
"desc": "任务描述",
"type": 1, //任务类型, 1: 一次性任务, 2: 日常任务
"pretasks": ["task id 1"], //前置任务
"score": 0, // 完成任务可获得的积分
"category": "", // 任务分类
"autoclaim": false // 任务完成后是否自动获取奖励
}
],
"startTime": 1702628292366, // 活动开始时间
@ -188,7 +193,36 @@ body:
}
```
### 6.\* 提交邀请码
###7.\* 获取任务奖励
#### Request
- URL`/api/tasks/claim`
- 方法:`GET`
- 头部:
- Authorization: Bearer JWT_token
body:
```js
{
"task": "TwitterFollow" // 任务id
}
```
#### Response
```json
{
"status": 1, // 任务状态, 0: 未开始, 1: 进行中, 2: 成功, 9: 失败
"id": "TwitterFollow", // 任务id
"timeStart": 1703150294051, // 任务开始时间
"timeFinish": 1703151338598
}
```
### 8.\* 提交邀请码
#### Request
@ -213,7 +247,7 @@ body:
{}
```
### 7. 积分排行榜
### 9. 积分排行榜
#### Request

View File

@ -6,16 +6,35 @@
"tasks": [{
"id": "TwitterConnect",
"title": "Connect Twitter",
"type": 1,
"desc": "",
"params": {}
"category": "",
"autoclaim": true,
"params": {"score": 100}
}, {
"id": "TwitterFollow",
"title": "Follow Twitter",
"type": 1,
"desc": "",
"params": {"time": 6, "failRate": 60}
"category": "",
"autoclaim": false,
"pretasks": ["TwitterConnect"],
"params": {"score": 100, "time": 6, "failRate": 60}
}, {
"id": "TwitterRetweet",
"title": "ReTwitt",
"type": 2,
"desc": "",
"category": "",
"autoclaim": false,
"pretasks": ["TwitterConnect"],
"params": {"score": 100, "time": 6, "failRate": 60}
}, {
"id": "UpdateScore",
"type": 1,
"show": false,
"autoclaim": false,
"pretasks": ["TwitterConnect", "TwitterFollow"],
"params": {"score": [100, 20]}
}],
"startTime": 1702628292366,

View File

@ -19,15 +19,7 @@ export default class ActivityController extends BaseController {
if (!activity) {
throw new ZError(12, 'activity not found')
}
let tasks = []
for (let task of activity.tasks) {
if (task.show) {
tasks.push({id: task.id, title: task.title, desc: task.desc})
}
}
let result = activity.toJson()
result.tasks = tasks
return result;
return activity.toJson()
}
/**

View File

@ -1,10 +1,10 @@
import { ZError } from "common/ZError";
import BaseController, { ROLE_ANON } from "common/base.controller";
import { role, router } from "decorators/router";
import { all } from "deepmerge";
import { ActivityInfo, TaskCfg } from "models/ActivityInfo";
import { router } from "decorators/router";
import { ActivityInfo, TaskCfg, TaskTypeEnum } from "models/ActivityInfo";
import { ActivityUser, TaskStatus, TaskStatusEnum } from "models/ActivityUser";
import { join } from 'path'
import { formatDate } from "utils/date.util";
const fs = require('fs')
const prod = process.env.NODE_ENV === 'production'
@ -25,82 +25,46 @@ 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;
for (let taskData of user.taskProgress) {
if (taskData.id === task) {
currentTask = taskData;
break;
}
if (taskData.status !== TaskStatusEnum.SUCCESS && !currentTask) {
preEnd = false;
}
}
if (!preEnd) {
throw new ZError(12, 'task previous not end')
}
if (!currentTask) {
throw new ZError(13, 'task not found')
}
return {preEnd, currentTask}
}
const parseNextTask = async (
user: typeof ActivityUser,
activity: typeof ActivityInfo,
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 {
@router('post /api/tasks/progress')
async taskProgress(req) {
let user = req.user;
let activity = req.activity;
if (!user.taskProgress || user.taskProgress.length === 0) {
for (let task of activity.tasks) {
if (allTasks.has(task.id)) {
const dateTag = formatDate(new Date());
let taskAddedSet = new Set();
for (let task of user.taskProgress) {
if (task.dateTag) {
taskAddedSet.add(task.id + ':' + task.dateTag)
} else {
taskAddedSet.add(task.id)
}
}
let modified = false;
let visibleTasks = new Set();
for (let task of activity.tasks) {
if (!allTasks.has(task.id)) {
continue;
}
if (task.type === TaskTypeEnum.DAILY ) {
let id = `${task.id}:${dateTag}`
if (!taskAddedSet.has(id)) {
modified = true;
user.taskProgress.push({id, dateTag: dateTag, status: TaskStatusEnum.NOT_START})
}
if (task.show) visibleTasks.add(id);
} else if (task.type === TaskTypeEnum.ONCE ) {
if (!taskAddedSet.has(task.id)) {
modified = true;
user.taskProgress.push({id: task.id, status: TaskStatusEnum.NOT_START})
}
if (task.show) visibleTasks.add(task.id);
}
}
if (modified) {
await user.save();
}
let visibleTasks = new Set();
activity.tasks.forEach((t:TaskCfg) => {
if (t.show) {
visibleTasks.add(t.id)
}
})
return user.taskProgress.filter((t: TaskStatus) => visibleTasks.has(t.id));
}
@ -108,35 +72,93 @@ export default class TasksController extends BaseController {
@router('post /api/tasks/begin_task')
async beginTask(req) {
let user = req.user;
let activity = req.activity;
let { task } = req.params;
let { currentTask } = parseCurrentTask(user, task);
currentTask.timeStart = Date.now();
currentTask.status = 1;
const [taskId, dateTag] = task.split(':');
const currentDateTag = formatDate(new Date());
if (currentDateTag !== dateTag) {
throw new ZError(11, 'task date not match')
}
let cfg = activity.tasks.find((t: TaskCfg) => t.id === taskId);
if (!cfg) {
throw new ZError(12, 'task not found')
}
if (dateTag && cfg.type !== TaskTypeEnum.DAILY) {
throw new ZError(13, 'task is not daily task')
}
if (cfg.pretasks && cfg.pretasks.length > 0) {
for (let preTask of cfg.pretasks) {
let preTaskData = user.taskProgress.find((t: TaskStatus) => {
if (preTask.type === TaskTypeEnum.DAILY) {
return t.id === `${preTask}:${formatDate(new Date())}`
}
return t.id === preTask
});
if (!preTaskData || (preTaskData.status === TaskStatusEnum.NOT_START || preTaskData.status === TaskStatusEnum.RUNNING)) {
throw new ZError(14, 'task previous not end')
}
}
}
let currentTask = user.taskProgress.find((t: TaskStatus) => t.id === task);
if (currentTask.status === TaskStatusEnum.SUCCESS || currentTask.status === TaskStatusEnum.CLAIMED) {
throw new ZError(15, 'task already end')
}
if (currentTask.status === TaskStatusEnum.NOT_START) {
currentTask.timeStart = Date.now();
currentTask.status = TaskStatusEnum.RUNNING;
}
await user.save();
return currentTask
}
@router('post /api/tasks/check_task')
async checkTask(req) {
let user = req.user;
let activity = req.activity;
let { task } = req.params;
let { currentTask } = parseCurrentTask(user, task);
if (currentTask.status === TaskStatusEnum.SUCCESS) {
const user = req.user;
const activity = req.activity;
const { task } = req.params;
const [taskId, dateTag] = task.split(':');
const currentDateTag = formatDate(new Date());
if (currentDateTag !== dateTag) {
throw new ZError(11, 'task date not match')
}
let currentTask = user.taskProgress.find((t: TaskStatus) => t.id === task);
if (!currentTask) {
throw new ZError(11, 'task not found')
}
if (currentTask.status === TaskStatusEnum.SUCCESS || currentTask.status === TaskStatusEnum.CLAIMED) {
return currentTask;
}
if (currentTask.status !== TaskStatusEnum.RUNNING) {
if (currentTask.status === TaskStatusEnum.NOT_START) {
throw new ZError(11, 'task not begin');
}
if (currentTask.status !== TaskStatusEnum.SUCCESS) {
let Task = require('../tasks/' + task);
if (currentTask.status === TaskStatusEnum.RUNNING) {
let Task = require('../tasks/' + taskId);
let taskInstance = new Task.default({user, activity});
let result = await taskInstance.execute({});
if (result) {
await parseNextTask(user, activity, task);
}
await taskInstance.execute({task: currentTask});
}
return currentTask
}
@router('post /api/tasks/claim')
async claimTask(req) {
const user = req.user;
const activity = req.activity;
const { task } = req.params;
let currentTask = user.taskProgress.find((t: TaskStatus) => t.id === task);
if (!currentTask) {
throw new ZError(11, 'task not found')
}
if (currentTask.status === TaskStatusEnum.CLAIMED) {
throw new ZError(12, 'task already claimed')
}
if (currentTask.status !== TaskStatusEnum.SUCCESS) {
throw new ZError(13, 'task not end')
}
const Task = require('../tasks/' + task);
const taskInstance = new Task.default({user, activity});
await taskInstance.claimReward(currentTask);
return currentTask
}
}

View File

@ -4,15 +4,30 @@ import { dbconn } from 'decorators/dbconn'
import findOrCreate from 'mongoose-findorcreate'
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
import { BaseModule } from './Base'
import exp from 'constants'
export enum TaskTypeEnum {
ONCE = 1,
DAILY = 2,
}
@modelOptions({ schemaOptions: { _id: false }, options: { allowMixed: Severity.ALLOW }, })
export class TaskCfg {
@prop()
id: string
@prop()
title: string
@prop({ enum: TaskTypeEnum, default: TaskTypeEnum.ONCE })
type: TaskTypeEnum
@prop()
desc: string
@prop({ type: mongoose.Schema.Types.Mixed })
category: any
@prop({default: false})
autoclaim: boolean
@prop({ type: () => [String], default: [] })
pretasks: string[]
@prop()
score: number
@prop({default: true})
show: boolean
@prop({ type: mongoose.Schema.Types.Mixed })
@ -45,6 +60,27 @@ class ActivityInfoClass extends BaseModule {
@prop()
public comment?: string
public toJson() {
let result = super.toJson()
let tasks = []
for (let task of this.tasks) {
if (task.show) {
tasks.push({
id: task.id,
title: task.title,
desc: task.desc,
type: task.type,
pretasks: task.pretasks,
score: task.score,
category: task.category,
autoclaim: task.autoclaim,
})
}
}
result.tasks = tasks
return result
}
}

View File

@ -12,6 +12,7 @@ export enum TaskStatusEnum {
NOT_START = 0,
RUNNING = 1,
SUCCESS = 2,
CLAIMED = 3,
}
@modelOptions({ schemaOptions: { _id: false }, options: { allowMixed: Severity.ALLOW }})
@ -22,9 +23,13 @@ export class TaskStatus {
@prop({ enum: TaskStatusEnum, default: TaskStatusEnum.NOT_START })
status: TaskStatusEnum
@prop()
dateTag: string
@prop()
timeStart: number
@prop()
timeFinish: number
@prop()
timeClaim: number
@prop({ type: mongoose.Schema.Types.Mixed })
data: any
}
@ -73,7 +78,7 @@ class ActivityUserClass extends BaseModule {
@prop()
public twitterId?: string
@prop()
public discordId: string
public discordId?: string
@prop()
public lastLogin?: Date

View File

@ -2,9 +2,6 @@ import { Severity, getModelForClass, index, modelOptions, mongoose, prop } from
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
export enum ScoreTypeEnum {
INVITE = 1,
}
@dbconn()
@index({ user: 1 }, { unique: false })
@ -20,8 +17,8 @@ class ScoreRecordClass extends BaseModule {
@prop()
public score: number
@prop({ enum: ScoreTypeEnum })
public type: ScoreTypeEnum
@prop()
public type: string
@prop({ type: mongoose.Schema.Types.Mixed })
public data: any

43
src/services/rank.svr.ts Normal file
View File

@ -0,0 +1,43 @@
import { ScoreRecord } from "models/ScoreRecord";
import { RedisClient } from "redis/RedisClient";
/**
*
* @param param0
* user: 用户id
* score: 分数
* activity: 活动id
* scoreType: 分数类型
* scoreParams: 额外的参数
*/
export const updateRankScore = async ({
user,
score,
activity,
scoreType,
scoreParams
}: {
user: string,
score: number,
activity: string,
scoreType: string,
scoreParams: any
}) => {
let record = new ScoreRecord({
user: user,
activity: activity,
score,
type: scoreType,
data: scoreParams
})
await record.save();
const key = `${activity}:score`
let scoreSaved = await new RedisClient().zscore(key, user) + '';
if (scoreSaved) {
scoreSaved = scoreSaved.substring(0, scoreSaved.indexOf('.'))
}
let scoreOld = parseInt(scoreSaved || '0');
score = score + scoreOld;
const scoreToSave = score + 1 - (Date.now() / 1000 / 10000000000)
await new RedisClient().zadd(key, scoreToSave, user);
}

View File

@ -2,23 +2,24 @@ import { checkDiscord } from "services/oauth.svr";
import { ITask } from "./base/ITask";
import { ZError } from "common/ZError";
import { TaskStatusEnum } from "models/ActivityUser";
import { TaskCfg } from "models/ActivityInfo";
export default class DiscordJoin extends ITask {
static desc = 'join discord'
static show: boolean = true
async execute(data: any) {
let { address } = this.params.user
let res = await checkDiscord(address)
console.log(res);
const { address } = this.params.user
const res = await checkDiscord(address)
let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === this.constructor.name)
if (res.status !== 200) {
throw new ZError(11, 'discord check failed')
}
if (res.data.errcode) {
throw new ZError(res.data.errcode, res.data.errmsg)
}
let task = this.params.user.taskProgress.find(t => t.id === this.constructor.name)
if (res.data.data.userid && task.status !== TaskStatusEnum.SUCCESS) {
const { task } = data
if (res.data.data.userid && task.status === TaskStatusEnum.RUNNING) {
task.status = TaskStatusEnum.SUCCESS
task.timeFinish = Date.now()
task.data = res.data.data
@ -28,7 +29,13 @@ export default class DiscordJoin extends ITask {
} catch(err) {
throw new ZError(100, 'discord already binded')
}
if (cfg.autoclaim) {
try {
await this.claimReward(task);
} catch(err) {
console.log(err)
}
}
}
return true
}

View File

@ -1,13 +1,14 @@
import { ZError } from "common/ZError";
import { ITask } from "./base/ITask";
import { TaskStatusEnum } from "models/ActivityUser";
import { TaskCfg } from "models/ActivityInfo";
export default class DiscordRole extends ITask {
static desc = 'acquire discord role'
static show: boolean = true
async execute(data: any) {
let task = this.params.user.taskProgress.find(t => t.id === this.constructor.name)
let cfg = this.params.activity.tasks.find(t => t.id === this.constructor.name)
const { task } = data
let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === this.constructor.name)
let time = cfg.params.time;
if (Date.now() - task.timeStart < time * 1000) {
throw new ZError(11, 'check discord role failed')
@ -19,7 +20,18 @@ export default class DiscordRole extends ITask {
task.status = TaskStatusEnum.SUCCESS
task.timeFinish = Date.now()
task.data = {}
await this.params.user.save()
try {
await this.params.user.save()
} catch(err) {
throw new ZError(100, 'already acquired discord role')
}
if (cfg.autoclaim) {
try {
await this.claimReward(task);
} catch(err) {
console.log(err)
}
}
return true
}

View File

@ -1,10 +1,9 @@
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";
import { updateRankScore } from "services/rank.svr";
const updateInviteScore = async (user: typeof ActivityUser, scores: number[], level: number) => {
const updateInviteScore = async (user: typeof ActivityUser, scores: number[], level: number, reason: string) => {
if (!user.inviteUser || scores.length <= level) {
return;
}
@ -12,27 +11,23 @@ const updateInviteScore = async (user: typeof ActivityUser, scores: number[], le
if (!userSup) {
return;
}
let record = new ScoreRecord({
await updateRankScore({
user: userSup.id,
activity: user.activity,
score: scores[level],
type: ScoreTypeEnum.INVITE,
data: {
activity: user.activity,
scoreType: reason,
scoreParams: {
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)
await updateInviteScore(userSup, scores, level + 1, reason)
}
export default class UpdateScore extends ITask {
export default class ShareCode extends ITask {
static desc = 'update invite score'
static show: boolean = false
static auto: boolean = true
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.id === this.constructor.name)
@ -42,8 +37,19 @@ export default class UpdateScore extends ITask {
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)
if (cfg.autoclaim) {
try {
await updateInviteScore(this.params.user, scores, 0, this.constructor.name)
} catch(err) {
console.log(err)
}
}
return true
}
public async claimReward(task: TaskStatus) {
let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === this.constructor.name)
let scores = cfg.params.score;
await updateInviteScore(this.params.user, scores, 0, this.constructor.name)
}
}

View File

@ -2,6 +2,7 @@ import { checkTwitter } from "services/oauth.svr";
import { ITask } from "./base/ITask";
import { ActivityUser, TaskStatusEnum } from "models/ActivityUser";
import { ZError } from "common/ZError";
import { TaskCfg } from "models/ActivityInfo";
export default class TwitterConnect extends ITask {
static desc = 'twitter connect'
@ -10,15 +11,14 @@ export default class TwitterConnect extends ITask {
async execute(data: any) {
let { address } = this.params.user
let res = await checkTwitter(address)
console.log(res);
if (res.status !== 200) {
throw new ZError(11, 'twitter check failed')
}
if (res.data.errcode) {
throw new ZError(res.data.errcode, res.data.errmsg)
}
let task = this.params.user.taskProgress.find(t => t.id === this.constructor.name)
if (res.data.data.userid && task.status !== TaskStatusEnum.SUCCESS) {
const { task } = data
if (res.data.data.userid && task.status === TaskStatusEnum.RUNNING) {
task.status = TaskStatusEnum.SUCCESS
task.timeFinish = Date.now()
task.data = res.data.data
@ -28,6 +28,14 @@ export default class TwitterConnect extends ITask {
} catch(err) {
throw new ZError(100, 'twitter already binded')
}
let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === this.constructor.name)
if (cfg.autoclaim) {
try {
await this.claimReward(task);
} catch(err) {
console.log(err)
}
}
}
return true

View File

@ -1,13 +1,14 @@
import { ITask } from "./base/ITask";
import { TaskStatusEnum } from "models/ActivityUser";
import { ZError } from "common/ZError";
import { TaskCfg } from "models/ActivityInfo";
export default class TwitterFollow extends ITask {
static desc = 'twitter follow'
static show: boolean = true
async execute(data: any) {
let task = this.params.user.taskProgress.find(t => t.id === this.constructor.name)
let cfg = this.params.activity.tasks.find(t => t.id === this.constructor.name)
const { task } = data
let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === this.constructor.name)
let time = cfg.params.time;
if (Date.now() - task.timeStart < time * 1000) {
throw new ZError(11, 'follow failed')
@ -19,7 +20,18 @@ export default class TwitterFollow extends ITask {
task.status = TaskStatusEnum.SUCCESS
task.timeFinish = Date.now()
task.data = {}
await this.params.user.save()
try {
await this.params.user.save()
} catch(err) {
throw new ZError(100, 'save failed')
}
if (cfg.autoclaim) {
try {
await this.claimReward(task);
} catch(err) {
console.log(err)
}
}
return true
}

View File

@ -6,7 +6,7 @@ export default class TwitterRetweet extends ITask {
static desc = 'twitter retweet'
static show: boolean = true
async execute(data: any) {
let task = this.params.user.taskProgress.find(t => t.id === this.constructor.name)
const { task } = data
let cfg = this.params.activity.tasks.find(t => t.id === this.constructor.name)
let time = cfg.params.time;
if (Date.now() - task.timeStart < time * 1000) {
@ -19,7 +19,18 @@ export default class TwitterRetweet extends ITask {
task.status = TaskStatusEnum.SUCCESS
task.timeFinish = Date.now()
task.data = {}
await this.params.user.save()
try {
await this.params.user.save()
} catch(err) {
throw new ZError(100, 'save failed')
}
if (cfg.autoclaim) {
try {
await this.claimReward(task);
} catch(err) {
console.log(err)
}
}
return true
}

View File

@ -1,12 +1,35 @@
import { TaskCfg } from "models/ActivityInfo"
import { TaskStatus, TaskStatusEnum } from "models/ActivityUser"
import { updateRankScore } from "services/rank.svr"
export abstract class ITask {
static desc: string
static show: boolean = true
static auto: boolean = false
params: any
constructor(params: any) {
// do nothing
this.params = params
}
abstract execute(data: any): Promise<boolean>
public async claimReward(task: TaskStatus) {
const user = this.params.user
const [taskId, dateTag] = task.id.split(':');
const cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === taskId)
await updateRankScore({
user: user.id,
score: cfg.score,
activity: user.activity,
scoreType: taskId,
scoreParams: {
date: dateTag,
taskId: task.id
}
})
task.status = TaskStatusEnum.CLAIMED
task.timeClaim = Date.now()
await user.save()
}
}

7
src/utils/date.util.ts Normal file
View File

@ -0,0 +1,7 @@
// format the date to the format we want
export const formatDate = (date: Date): string => {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}${month}${day}`;
};