From 544048bcf3792c054eb41121f3d961fb1a021dd2 Mon Sep 17 00:00:00 2001 From: CounterFire2023 <136581895+CounterFire2023@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:17:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8A=BD=E5=A5=96=E7=9B=B8?= =?UTF-8?q?=E5=85=B3api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api.md | 127 +++++++++++++++++ initdatas/activity_info.json | 37 ++++- src/common/Constants.ts | 3 + src/common/LotteryCache.ts | 23 +++ src/configs/fusion.ts | 28 ++++ src/configs/items.ts | 40 ++++++ src/configs/lottery.ts | 43 ++++++ src/controllers/activity.controller.ts | 2 +- src/controllers/lottery.controller.ts | 190 +++++++++++++++++++++++++ src/controllers/tasks.controller.ts | 30 ++++ src/models/ActivityInfo.ts | 46 +++++- src/models/ActivityItem.ts | 31 ++++ src/models/LotteryRecord.ts | 33 +++++ src/models/LotteryStats.ts | 41 ++++++ src/models/chain/CheckIn.ts | 57 ++++++++ src/models/chain/NftHolder.ts | 54 +++++++ src/models/chain/NftTransferEvent.ts | 66 +++++++++ src/services/chain.svr.ts | 8 ++ src/tasks/BurnNft.ts | 14 +- src/tasks/DailyCheckIn.ts | 29 ++-- src/tasks/DiscordConnect.ts | 10 +- src/tasks/DiscordJoin.ts | 4 +- src/tasks/DiscordRole.ts | 4 +- src/tasks/OkxLogin.ts | 8 +- src/tasks/ShareCode.ts | 38 +++-- src/tasks/TwitterConnect.ts | 10 +- src/tasks/TwitterFollow.ts | 4 +- src/tasks/TwitterRetweet.ts | 4 +- src/tasks/YoutubeFollow.ts | 4 +- src/tasks/YoutubePost.ts | 4 +- src/tasks/base/ITask.ts | 30 +++- 31 files changed, 956 insertions(+), 66 deletions(-) create mode 100644 src/common/LotteryCache.ts create mode 100644 src/configs/fusion.ts create mode 100644 src/configs/items.ts create mode 100644 src/configs/lottery.ts create mode 100644 src/controllers/lottery.controller.ts create mode 100644 src/models/ActivityItem.ts create mode 100644 src/models/LotteryRecord.ts create mode 100644 src/models/LotteryStats.ts create mode 100644 src/models/chain/CheckIn.ts create mode 100644 src/models/chain/NftHolder.ts create mode 100644 src/models/chain/NftTransferEvent.ts diff --git a/docs/api.md b/docs/api.md index 6da0f55..646f676 100644 --- a/docs/api.md +++ b/docs/api.md @@ -110,6 +110,7 @@ SiweMessage的nonce说明(具体参考例子): "score": 0, // 完成任务可获得的积分 "category": "", // 任务分类 "cfg": {}, // 其他一些任务相关配置参数, 比如icon, 或者其他未考虑的参数 + "end": false, // 是否已经结束 "autoclaim": false // 任务完成后是否自动获取奖励 } ], @@ -361,4 +362,130 @@ query param "count": 0 //连签天数 }, ] +``` + +### 13.\* 抽奖状态 + +#### Request + +- URL:`/api/lottery/stats` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + + +#### Response + +```json +{ + "amount": 1, // 今日可抽数量 + "daily": 0, // 今日通过每日签到获得的抽奖机会 + "share": 0, // 今日通过邀请获得的抽奖机会 + "gacha": 0, // 今日通过gacha获得的抽奖机会 + "used": 0, // 今日已抽数量 + "day": "20240110", + "items": [{ + "id": "thunder", + "amount": 1, //数量 + }] + } +``` + +### 14.\* 活动配置 + +#### Request + +- URL:`/api/lottery/items` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + + +#### Response + +```json +{ + "start": "活动开始时间", + "end": "活动结束时间", + "items": [ + { + "id": "物品id", + "name": "物品名称", + "amount": 1 //物品数量 + }, + ] +} + +``` + +### 15.\* 抽奖 + +#### Request + +- URL:`/api/lottery/draw` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + + +#### Response + +```json +{ + "id": "物品id 或 empty(未获得任何奖励)", + "amount": "数量" +} +``` + +### 16.\* 合成 + +#### Request + +- URL:`/api/lottery/fusion` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token + + +#### Response + +```json +{ + "id": "物品id", + "amount": "数量" +} +``` + +### 17.\* 查询抽奖历史记录 + +#### Request + +- URL:`/api/lottery/history` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token + +body: + +```json +{ + "day": "20240111", // 按日查询, 传格式化后的日期(yyyyMMDD), "all"表示查询所有 + "page": 1, // 分页, 从1开始, 可为空, 默认1 + "limit": 10, // 分页记录数, 可为空, 默认10 +} +``` + +#### Response + +```json +{ + "count": 100, //记录总数 + "page": 1, // 分页 + "limit": 10, // 分页记录数 + "records":[{ + "id": "物品id", + "day": "20240111", + "amount": "数量" + }] +} ``` \ No newline at end of file diff --git a/initdatas/activity_info.json b/initdatas/activity_info.json index 5dddd21..b511266 100644 --- a/initdatas/activity_info.json +++ b/initdatas/activity_info.json @@ -13,6 +13,8 @@ "category": "", "autoclaim": false, "cfg": {"icon": "twitter"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {} }, { "id": "e2fclylj30vwcpe0szl", @@ -25,6 +27,8 @@ "autoclaim": false, "pretasks": ["e2yhq2lj30vwcpedv7p"], "cfg": {"account": "@_CounterFire", "icon": "twitter"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2feyflj30vwcpe0sjy", @@ -37,6 +41,8 @@ "autoclaim": false, "pretasks": ["e2yhq2lj30vwcpedv7p"], "cfg": {"icon": "twitter"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2fuah0j30vwcpe0my7", @@ -49,6 +55,8 @@ "autoclaim": false, "pretasks": ["e2yhq2lj30vwcpedv7p"], "cfg": {"icon": "twitter", "content": "Just joined Counter Fire! 🎮 Excited about the endless opportunities ahead. 🔥 Let's team up and conquer together! Come in with me #CounterFire #GamingAdventures"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2fuah0j30vwcpe1my7", @@ -61,6 +69,8 @@ "autoclaim": false, "pretasks": ["e2yhq2lj30vwcpedv7p"], "cfg": {"icon": "twitter", "content": "输入框自动生成,活动开始时添加内容)"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2far3lj30vwcpe0mh7", @@ -73,6 +83,8 @@ "autoclaim": false, "pretasks": [], "cfg": {"icon": "discord"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {} }, { "id": "e2far3lj30vwcpe0mf8", @@ -85,6 +97,8 @@ "autoclaim": false, "pretasks": [], "cfg": {"icon": "discord"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2fak2lj30vwcpe0awc", @@ -97,6 +111,8 @@ "autoclaim": false, "pretasks": ["e2far3lj30vwcpe0mf8"], "cfg": {"icon": "discord"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2feyflj30vwcpe0sjx", @@ -109,6 +125,8 @@ "autoclaim": false, "pretasks": [], "cfg": {"icon": "youtube"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2feyflj30vwcpe0sjz", @@ -121,6 +139,8 @@ "autoclaim": false, "pretasks": [], "cfg": {"icon": "youtube"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2fuah0j30vwcpe2my7", @@ -133,6 +153,8 @@ "autoclaim": false, "pretasks": ["e2yhq2lj30vwcpedv7p"], "cfg": {"icon": "twitter", "content": "Just scored xx Flame on @_CounterFire! 🔥 Join me in the action-packed fun and let's play to earn! #GamingAdventures #CounterFire"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2fuah0j30vwcpe2my7", @@ -145,6 +167,8 @@ "autoclaim": false, "pretasks": ["e2yhq2lj30vwcpedv7p"], "cfg": {"icon": "twitter", "content": "Just scored xx Flame on @_CounterFire! 🔥 Join me in the action-packed fun and let's play to earn! #GamingAdventures #CounterFire"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"time": 6, "failRate": 60} }, { "id": "e2f7fplj30vwcpe0l97", @@ -157,6 +181,8 @@ "autoclaim": false, "pretasks": [], "cfg": {}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"days": 3} }, { "id": "e2f7fplj30vwcpe0l98", @@ -169,7 +195,9 @@ "autoclaim": false, "pretasks": [], "cfg": {"account": "okx", "icon": "okx"}, - "params": {} + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", + "params": {"item": {"lottery_ticket": 1}} }, { "id": "e2f7fplj30vwcpe0l96", "task": "DailyCheckIn", @@ -181,6 +209,8 @@ "autoclaim": false, "pretasks": [], "cfg": {"score": [0, 15, 20, 20, 40, 40, 60]}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", "params": {"days": 1, "score": [0, 15, 20, 20, 40, 40, 60]} }, { "id": "e2f7t4lj30vwcpe0ldr", @@ -188,6 +218,7 @@ "title": "", "desc": "Click here if your are referred by a friend!", "type": 1, + "category": "Social Tasks", "show": true, "autoclaim": false, "pretasks": [], @@ -205,9 +236,9 @@ "category": "CF Pal", "autoclaim": false, "pretasks": [], - "cfg": {"address": "0x59e751c2037B710090035B6ea928e0cce80aC03f"}, + "cfg": {"address": "0xCD4bb3402f1a444a1AF10F31946Ed37DaC0eaC4d"}, "score": 200, - "params": {"address": "0x59e751c2037B710090035B6ea928e0cce80aC03f"} + "params": {"address": "0xCD4bb3402f1a444a1AF10F31946Ed37DaC0eaC4d"} }, { "id": "e2f7t4lj32vwcpe0ldr", "task": "BurnNft", diff --git a/src/common/Constants.ts b/src/common/Constants.ts index a3dfa70..a5b214a 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -4,6 +4,9 @@ export const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000 export const MAX_BATCH_REQ_COUNT = 50 +export const EMPTY_REWARD = 'empty' +export const ITEM_FRAME = 'flame' + export const CONFIRM_MAIL_HTML = `

有东西需要你确认

{{title}}

diff --git a/src/common/LotteryCache.ts b/src/common/LotteryCache.ts new file mode 100644 index 0000000..fa9ce6a --- /dev/null +++ b/src/common/LotteryCache.ts @@ -0,0 +1,23 @@ +import { singleton } from "decorators/singleton"; +import { LotteryStats } from "models/LotteryStats"; +import { formatDate } from "utils/date.util"; + +@singleton +export class LotteryCache { + map: Map = new Map(); + + public async getData(user: string, activity: string) { + const dateTag = formatDate(new Date()); + if (!this.map.has(user+dateTag)) { + const record = await LotteryStats.insertOrUpdate({user, activity, dateTag}, {}) + this.map.set(user+dateTag, record); + } + return this.map.get(user+dateTag); + } + + public async flush() { + for (let record of this.map.values()) { + await record.save(); + } + } +} \ No newline at end of file diff --git a/src/configs/fusion.ts b/src/configs/fusion.ts new file mode 100644 index 0000000..2cab3f3 --- /dev/null +++ b/src/configs/fusion.ts @@ -0,0 +1,28 @@ +export const FUSION_CFG = { + target: { + id: 'torch', + amount: 1, + }, + source: [ + { + id: 'thunder', + amount: 1, + }, + { + id: 'light', + amount: 1, + }, + { + id: 'spark', + amount: 1, + }, + { + id: 'blaze', + amount: 1, + }, + { + id: 'kindle', + amount: 1, + }, + ] +} \ No newline at end of file diff --git a/src/configs/items.ts b/src/configs/items.ts new file mode 100644 index 0000000..eca16e7 --- /dev/null +++ b/src/configs/items.ts @@ -0,0 +1,40 @@ +export interface IItem { + id: string + name: string + score?: number +} +export const ALL_ITEMS: IItem[] = [ + { + id: 'usdt', + name: 'USDT', + }, + { + id: 'flame', + name: 'Flame', + score: 1, + }, + { + id: 'thunder', + name: 'Thunder', + }, + { + id: 'light', + name: 'Light', + }, + { + id: 'spark', + name: 'Spark', + }, + { + id: 'blaze', + name: 'Blaze', + }, + { + id: 'kindle', + name: 'Kindle', + }, + { + id: 'torch', + name: 'Torch' + }, +] diff --git a/src/configs/lottery.ts b/src/configs/lottery.ts new file mode 100644 index 0000000..145b8d7 --- /dev/null +++ b/src/configs/lottery.ts @@ -0,0 +1,43 @@ + +export const LOTTERY_CFG = { + start: '2024-01-01 00:00:00', + end: '2025-01-01 00:00:00', + rewards: [ + { + item: 'usdt', + amount: 20, + probability: 10000 + }, + { + item: 'flame', + amount: 30, + probability: 300000 + }, + { + item: 'thunder', + amount: 1, + probability: 170000 + }, + { + item: 'light', + amount: 1, + probability: 180000 + }, + { + item: 'spark', + amount: 1, + probability: 130000 + }, + { + item: 'blaze', + amount: 1, + probability: 100000 + }, + { + item: 'kindle', + amount: 1, + probability: 100000 + }, + + ] +} \ No newline at end of file diff --git a/src/controllers/activity.controller.ts b/src/controllers/activity.controller.ts index a72f34f..689bca3 100644 --- a/src/controllers/activity.controller.ts +++ b/src/controllers/activity.controller.ts @@ -62,7 +62,7 @@ export default class ActivityController extends BaseController { let score = parseInt(records[i + 1]) const user = await ActivityUser.findById(id) let invite = '' - if (user.inviteUser) { + if (user?.inviteUser) { const inviteUser = await ActivityUser.findById(user.inviteUser) if (inviteUser) { invite = inviteUser.address diff --git a/src/controllers/lottery.controller.ts b/src/controllers/lottery.controller.ts new file mode 100644 index 0000000..1af683a --- /dev/null +++ b/src/controllers/lottery.controller.ts @@ -0,0 +1,190 @@ +import { EMPTY_REWARD, ITEM_FRAME } from "common/Constants"; +import { LotteryCache } from "common/LotteryCache"; +import { ZError } from "common/ZError"; +import BaseController from "common/base.controller"; +import { FUSION_CFG } from "configs/fusion"; +import { ALL_ITEMS } from "configs/items"; +import { LOTTERY_CFG } from "configs/lottery"; +import { router } from "decorators/router"; +import { ActivityItem } from "models/ActivityItem"; +import { LotteryRecord } from "models/LotteryRecord"; +import { updateRankScore } from "services/rank.svr"; +import { formatDate } from "utils/date.util"; + +const ROUND = 1000000; + +// Get random prizes according to the set probability +const draw = (rewards: {probability: number}[]) => { + let total = 0; + let random = Math.floor(Math.random() * ROUND); + let reward = null; + for (let r of rewards) { + total += r.probability; + if (random < total) { + reward = r; + break; + } + } + return {id: reward?.item || EMPTY_REWARD, amount: reward?.amount || 1}; +} + +export default class LotteryController extends BaseController { + @router('get /api/lottery/stats') + async userStats(req) { + let user = req.user; + let record = await new LotteryCache().getData(user.id, user.activity); + let result:any = record.toJson(); + let items = await ActivityItem.find({user: user.id, activity: user.activity}); + result.items = items.map((i) => i.toJson()); + return result; + } + + @router('post /api/lottery/history') + async userLotteryHistory(req) { + let user = req.user; + let { day, page, limit } = req.params; + const query: any = {user: user.id, activity: user.activity}; + if (day !== 'all') { + query.dateTag = day; + } + page = +page || 1 + limit = +limit || 10 + let start = page * limit || 0 + + + let historys = await LotteryRecord.find(query).skip(start).limit(limit); + let total = await LotteryRecord.countDocuments(query); + return { + count: total, + page: +page, + limit, + records: historys.map((h) => h.toJson()) + } + } + + @router('get /api/lottery/items') + async items(req) { + const items = ALL_ITEMS; + const cfgs = LOTTERY_CFG; + const itemMap = new Map(); + for (let item of items) { + itemMap.set(item.id, item); + } + let result = []; + for (let cfg of cfgs.rewards) { + result.push({ + id: cfg.item, + amount: cfg.amount, + name: itemMap.get(cfg.item).name, + }) + } + return {items: result, start: cfgs.start, end: cfgs.end}; + } + + + @router('get /api/lottery/draw') + async draw(req) { + let user = req.user; + const { start, end, rewards } = LOTTERY_CFG; + const startTime = new Date(start).getTime(); + const endTime = new Date(end).getTime(); + const now = Date.now(); + if (now < startTime) { + throw new ZError(10, 'lottery not start'); + } + if (now > endTime) { + throw new ZError(11, 'lottery end'); + } + let record = await new LotteryCache().getData(user.id, user.activity); + if (record.amount <= 0) { + throw new ZError(12, 'no chance'); + } + record.amount -= 1; + record.used += 1; + let reward = draw(rewards); + const dateTag = formatDate(new Date()); + let history = new LotteryRecord({ + user: user.id, + activity: user.activity, + dateTag, + reward: reward?.id || EMPTY_REWARD, + amount: reward.amount || 0, + }); + await history.save(); + const items = ALL_ITEMS; + const itemMap = new Map(); + for (let item of items) { + itemMap.set(item.id, item); + } + if (itemMap.get(reward.id)?.score) { + let score = (reward?.amount || 0) * itemMap.get(reward.id).score; + if (user.boost > 1 && Date.now() < user.boostExpire) { + score = Math.floor(score * user.boost) + } + await updateRankScore({ + user: user.id, + score, + activity: user.activity, + scoreType: "draw", + scoreParams: { + date: dateTag + } + }) + } + await ActivityItem.insertOrUpdate({ + user: user.id, + activity: user.activity, + item: reward.id + }, + { + $inc: {amount: reward.amount}, + last: Date.now() + }) + return reward; + } + + @router('get /api/lottery/fusion') + async fusion(req) { + let user = req.user; + let items = await ActivityItem.find({user: user.id, activity: user.activity}); + let itemCountMap = new Map(); + for (let item of items) { + itemCountMap.set(item.item, item.amount); + } + for (let item of FUSION_CFG.source) { + if (!itemCountMap.has(item.id)) { + throw new ZError(13, 'no enough item'); + } + if (itemCountMap.get(item.id) < item.amount) { + throw new ZError(14, 'no enough item'); + } + } + for (let item of FUSION_CFG.source) { + await ActivityItem.insertOrUpdate({ + user: user.id, + activity: user.activity, + item: item.id + }, + { + $inc: {amount: -item.amount}, + last: Date.now() + }) + } + + await ActivityItem.insertOrUpdate({ + user: user.id, + activity: user.activity, + item: FUSION_CFG.target.id + }, + { + $inc: {amount: FUSION_CFG.target.amount}, + last: Date.now() + }) + + return { + id: FUSION_CFG.target.id, + amount: FUSION_CFG.target.amount, + } + } + +} \ No newline at end of file diff --git a/src/controllers/tasks.controller.ts b/src/controllers/tasks.controller.ts index a411b07..03df86e 100644 --- a/src/controllers/tasks.controller.ts +++ b/src/controllers/tasks.controller.ts @@ -47,6 +47,9 @@ export default class TasksController extends BaseController { if (!allTasks.has(task.task)) { continue; } + if (!task.isVaild()) { + continue; + } if (task.type === TaskTypeEnum.DAILY ) { let id = `${task.id}:${dateTag}` if (!taskAddedSet.has(id)) { @@ -79,10 +82,16 @@ export default class TasksController extends BaseController { if (dateTag && currentDateTag !== dateTag) { throw new ZError(11, 'task date not match') } + if (!activity.isVaild()) { + throw new ZError(15, 'activity not start or end') + } let cfg = activity.tasks.find((t: TaskCfg) => t.id === taskId); if (!cfg) { throw new ZError(12, 'task not found') } + if (!cfg.isVaild()) { + throw new ZError(16, 'task not start or end') + } if (dateTag && cfg.type !== TaskTypeEnum.DAILY) { throw new ZError(13, 'task is not daily task') } @@ -122,6 +131,16 @@ export default class TasksController extends BaseController { if (dateTag && currentDateTag !== dateTag) { throw new ZError(11, 'task date not match') } + if (!activity.isVaild()) { + throw new ZError(15, 'activity not start or end') + } + let cfg = activity.tasks.find((t: TaskCfg) => t.id === taskId); + if (!cfg) { + throw new ZError(12, 'task not found') + } + if (!cfg.isVaild()) { + throw new ZError(16, 'task not start or end') + } let currentTask = user.taskProgress.find((t: TaskStatus) => t.id === task); if (!currentTask) { throw new ZError(11, 'task not found') @@ -145,6 +164,17 @@ export default class TasksController extends BaseController { const user = req.user; const activity = req.activity; const { task } = req.params; + const [taskId, dateTag] = task.split(':'); + if (!activity.isVaild()) { + throw new ZError(15, 'activity not start or end') + } + let cfg = activity.tasks.find((t: TaskCfg) => t.id === taskId); + if (!cfg) { + throw new ZError(14, 'task not found') + } + if (!cfg.isVaild()) { + throw new ZError(16, 'task not start or end') + } let currentTask = user.taskProgress.find((t: TaskStatus) => t.id === task); if (!currentTask) { throw new ZError(11, 'task not found') diff --git a/src/models/ActivityInfo.ts b/src/models/ActivityInfo.ts index 0f68b76..24d3b99 100644 --- a/src/models/ActivityInfo.ts +++ b/src/models/ActivityInfo.ts @@ -36,8 +36,36 @@ export class TaskCfg { show: boolean @prop({ type: mongoose.Schema.Types.Mixed }) cfg: any + @prop() + start?: string + @prop() + end?: string @prop({ type: mongoose.Schema.Types.Mixed }) params: any + + public isStart() { + const now = Date.now() + if (this.start) { + let start = new Date(this.start).getTime() + if (now < start) { + return false + } + } + } + public isEnd() { + const now = Date.now() + if (this.end) { + let end = new Date(this.end).getTime() + if (now > end) { + return false + } + } + return true + } + + public isVaild() { + return this.isStart() && !this.isEnd() + } } interface ActivityInfoClass extends Base, TimeStamps {} @dbconn() @@ -67,11 +95,26 @@ class ActivityInfoClass extends BaseModule { @prop() public comment?: string + public isValie() { + const now = Date.now() + if (this.startTime) { + if (now < this.startTime) { + return false + } + } + if (this.endTime) { + if (now > this.endTime) { + return false + } + } + return true + } public toJson() { let result = super.toJson() let tasks = [] + const now = Date.now() for (let task of this.tasks) { - if (task.show) { + if (task.show && task.isStart()) { tasks.push({ id: task.id, task: task.task, @@ -84,6 +127,7 @@ class ActivityInfoClass extends BaseModule { category: task.category, autoclaim: task.autoclaim, cfg: task.cfg, + end: task.isEnd(), }) } } diff --git a/src/models/ActivityItem.ts b/src/models/ActivityItem.ts new file mode 100644 index 0000000..45c02ec --- /dev/null +++ b/src/models/ActivityItem.ts @@ -0,0 +1,31 @@ +import { dbconn } from 'decorators/dbconn' +import { getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose' +import { Severity } from '@typegoose/typegoose/lib/internal/constants' +import { BaseModule } from './Base' + +/** + */ +@dbconn() +@index({ user: 1, activity: 1}, { unique: false }) +@index({ user: 1, activity: 1, item: 1 }, { unique: true }) +@modelOptions({ schemaOptions: { collection: 'activity_item', timestamps: true }, options: { allowMixed: Severity.ALLOW } }) +class ActivityItemClass extends BaseModule { + @prop() + public user: string + @prop() + public activity: string + @prop() + public item: string + @prop({default: 0}) + public amount: number + @prop() + public last: number + + public toJson() { + return { + id: this.item, + amount: this.amount + } + } +} +export const ActivityItem = getModelForClass(ActivityItemClass, { existingConnection: ActivityItemClass['db'] }) diff --git a/src/models/LotteryRecord.ts b/src/models/LotteryRecord.ts new file mode 100644 index 0000000..7608737 --- /dev/null +++ b/src/models/LotteryRecord.ts @@ -0,0 +1,33 @@ +import { dbconn } from 'decorators/dbconn' +import { getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose' +import { Severity } from '@typegoose/typegoose/lib/internal/constants' +import { BaseModule } from './Base' + +/** + * 用户抽奖记录 + */ +@dbconn() +@index({ user: 1, activity: 1}, { unique: false }) +@index({ user: 1, activity: 1, dateTag: 1}, { unique: false }) +@modelOptions({ schemaOptions: { collection: 'lottery_record', timestamps: true }, options: { allowMixed: Severity.ALLOW } }) +class LotteryRecordClass extends BaseModule { + @prop() + public user: string + @prop() + public activity: string + @prop() + public dateTag: string + @prop() + public reward?: string + @prop({default: 0}) + public amount: number + + public toJson() { + return { + day: this.dateTag, + id: this.reward, + amount: this.amount, + } + } +} +export const LotteryRecord = getModelForClass(LotteryRecordClass, { existingConnection: LotteryRecordClass['db'] }) diff --git a/src/models/LotteryStats.ts b/src/models/LotteryStats.ts new file mode 100644 index 0000000..d5d4f39 --- /dev/null +++ b/src/models/LotteryStats.ts @@ -0,0 +1,41 @@ +import { dbconn } from 'decorators/dbconn' +import { getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose' +import { Severity } from '@typegoose/typegoose/lib/internal/constants' +import { BaseModule } from './Base' + +/** + * 用户抽奖状态 + */ +@dbconn() +@index({ user: 1, activity: 1, dateTag: 1}, { unique: false }) +@modelOptions({ schemaOptions: { collection: 'lottery_stats', timestamps: true }, options: { allowMixed: Severity.ALLOW } }) +class LotteryStatsClass extends BaseModule { + @prop() + public user: string + @prop() + public activity: string + @prop({default: 0}) + public amount: number + @prop({default: 0}) + public daily: number + @prop({default: 0}) + public gacha: number + @prop({default: 0 }) + public share: number + @prop({default: 0}) + public used: number + @prop() + public dateTag: string + + + public toJson() { + return { + amount: this.amount, + daily: this.daily, + gacha: this.gacha, + used: this.used, + day: this.dateTag, + } + } +} +export const LotteryStats = getModelForClass(LotteryStatsClass, { existingConnection: LotteryStatsClass['db'] }) diff --git a/src/models/chain/CheckIn.ts b/src/models/chain/CheckIn.ts new file mode 100644 index 0000000..d8d9dc9 --- /dev/null +++ b/src/models/chain/CheckIn.ts @@ -0,0 +1,57 @@ +import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from '../Base' +import { formatDate, yesterday } from 'utils/date.util' + +@dbconn('chain') +@index({ from: 1 }, { unique: false }) +@index({ from: 1, dateTag: 1}, { unique: true }) +@index({ from: 1, blockTime: 1}, { unique: false }) +@modelOptions({ + schemaOptions: { collection: 'check_in_event', timestamps: true }, +}) +export class CheckInClass extends BaseModule { + @prop({ required: true }) + public from!: string + @prop() + public to: string + @prop({ required: true }) + public hash: string + @prop() + public blockNumber: string + @prop() + public blockHash: string + @prop() + public blockTime: number + @prop() + public dateTag: string + // 连签天数 + @prop({default: 0}) + public count: number + @prop() + public value: string + @prop() + public input: string + + public static async saveEvent(event: any) { + const preDay = formatDate(yesterday()); + const preDayEvent = await CheckIn.findOne({ from: event.from, dateTag: preDay }) + if (preDayEvent) { + event.count = preDayEvent.count + 1 + } + return CheckIn.insertOrUpdate({ hash: event.hash }, event) + } + + public toJson() { + return { + address: this.from, + day: this.dateTag, + time: this.blockTime, + count: this.count + } + } +} + +export const CheckIn = getModelForClass(CheckInClass, { + existingConnection: CheckInClass['db'], +}) diff --git a/src/models/chain/NftHolder.ts b/src/models/chain/NftHolder.ts new file mode 100644 index 0000000..e07274d --- /dev/null +++ b/src/models/chain/NftHolder.ts @@ -0,0 +1,54 @@ +import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from '../Base' +import { ZERO_ADDRESS } from 'common/Constants' + +@dbconn('chain') +@index({ chain: 1, address: 1, tokenId: 1 }, { unique: true }) +@index({ chain: 1, address: 1, user: 1 }, { unique: false }) +@modelOptions({ + schemaOptions: { collection: 'nft_holder', timestamps: true }, +}) +export class NftHolderClass extends BaseModule { + @prop({ required: true }) + public address!: string + @prop({ required: true }) + public chain: string + @prop({ required: true }) + public tokenId: string + @prop() + public blockNumber: number + @prop() + public user: string + @prop({default: false}) + public burn: boolean + + + public static async saveData(event: any) { + const address = event.address; + const chain = event.chain; + const tokenId = event.tokenId; + const blockNumer = event.blockNumber; + const burn = event.to === ZERO_ADDRESS + + let record = await NftHolder.findOne({ address, chain, tokenId }) + if (!record) { + record = new NftHolder({ address, chain, tokenId, blockNumber: blockNumer, user: event.to, burn }) + } else { + if (record.blockNumber < blockNumer) { + if (burn) { + record.burn = true + } else { + record.user = event.to + } + record.blockNumber = blockNumer + } + } + await record.save(); + + } +} + +export const NftHolder = getModelForClass(NftHolderClass, { + existingConnection: NftHolderClass['db'], +}) diff --git a/src/models/chain/NftTransferEvent.ts b/src/models/chain/NftTransferEvent.ts new file mode 100644 index 0000000..4f3fcbb --- /dev/null +++ b/src/models/chain/NftTransferEvent.ts @@ -0,0 +1,66 @@ +import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from '../Base' + +@dbconn('chain') +@index({ chain: 1, address: 1, tokenId: 1 }, { unique: false }) +@index({ chain: 1, address: 1, from: 1, to: 1 }, { unique: false }) +@index({ chain: 1, hash: 1, logIndex: 1}, { unique: true }) +@modelOptions({ + schemaOptions: { collection: 'nft_transfer_event', timestamps: true }, +}) +export class NftTransferEventClass extends BaseModule { + @prop({ required: true }) + public address!: string + @prop({ required: true }) + public chain: string + @prop({ required: true }) + public logIndex: number + @prop() + public event: string + @prop({ required: true }) + public hash: string + @prop() + public blockNumber: number + @prop() + public blockHash: string + @prop() + public removed: boolean + @prop() + public from: string + @prop() + public to: string + @prop() + public tokenId: string + @prop() + public blockTime: number + @prop({ default: 0 }) + public version: number + + public static async saveEvent(event: any) { + const tokenId = event.tokenId || event.value + if (!tokenId) { + return + } + const logIndex = parseInt(event.logIndex || '0') + const from = event.from.toLowerCase() + const to = event.to.toLowerCase() + const hash = event.hash || event.transactionHash + const data = { + address: event.address.toLowerCase(), + blockNumber: parseInt(event.blockNumber), + removed: event.removed, + from, + to, + tokenId, + // blockTime: new Date(event.time).getTime(), + $inc: { version: 1 }, + } + + return NftTransferEvent.insertOrUpdate({ hash, logIndex, chain: event.chain }, data) + } +} + +export const NftTransferEvent = getModelForClass(NftTransferEventClass, { + existingConnection: NftTransferEventClass['db'], +}) diff --git a/src/services/chain.svr.ts b/src/services/chain.svr.ts index fb3f217..2a36103 100644 --- a/src/services/chain.svr.ts +++ b/src/services/chain.svr.ts @@ -1,3 +1,4 @@ +import { NftHolder } from "models/chain/NftHolder" export const queryCheckInList = async (address: string, days: string | number | string[], limit: number = 0) => { const url = process.env.CHAIN_SVR + '/task/check_in' @@ -34,4 +35,11 @@ export const queryBurnNftList = async (address: string, user: string, chain: num chain }) }).then((res) => res.json()) +} + +export const checkHadGacha = async (user: string) => { + const chain = process.env.CHAIN+'' + const address = process.env.GACHA_CONTRACT + const record = await NftHolder.findOne({user, chain, address}) + return !!record } \ No newline at end of file diff --git a/src/tasks/BurnNft.ts b/src/tasks/BurnNft.ts index 3cf10e8..d6388d0 100644 --- a/src/tasks/BurnNft.ts +++ b/src/tasks/BurnNft.ts @@ -10,16 +10,16 @@ export default class BurnNft extends ITask { static show: boolean = true async execute(data: any) { const { task } = data - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) const address = cfg.params.address.toLowerCase() const chain = parseInt(process.env.CHAIN) - const res = await queryBurnNftList(address, this.params.user.address, chain) + const res = await queryBurnNftList(address, this.user.address, chain) if (res.errcode) { throw new ZError(res.errcode, res.errmsg) } const nftList = res.data const localNft = await NftBurnRecord.find({ - user: this.params.user.id, + user: this.user.id, chain, address, }) @@ -28,7 +28,7 @@ export default class BurnNft extends ITask { let tmpNftSet = new Set(); for (let nft of localNft) { localNftSet.add(nft.tokenId) - if (nft.activity === this.params.activity.id && nft.task === task.id) { + if (nft.activity === this.activity.id && nft.task === task.id) { finishAmount += 1 tmpNftSet.add(nft.tokenId) } @@ -45,11 +45,11 @@ export default class BurnNft extends ITask { } for (let finishNft of finishNfts) { const record = new NftBurnRecord({ - user: this.params.user.id, + user: this.user.id, chain, address, tokenId: finishNft, - activity: this.params.activity.id, + activity: this.activity.id, task: task.id }) await record.save() @@ -66,7 +66,7 @@ export default class BurnNft extends ITask { task.status = TaskStatusEnum.PART_SUCCESS } try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'save failed') } diff --git a/src/tasks/DailyCheckIn.ts b/src/tasks/DailyCheckIn.ts index aa580c4..7d9056a 100644 --- a/src/tasks/DailyCheckIn.ts +++ b/src/tasks/DailyCheckIn.ts @@ -4,8 +4,8 @@ import { TaskStatus, TaskStatusEnum } from "models/ActivityUser"; import { TaskCfg, TaskTypeEnum } from "models/ActivityInfo"; import { queryCheckInList, queryCheckInSeq } from "services/chain.svr"; import { updateRankScore } from "services/rank.svr"; +import { LotteryCache } from "common/LotteryCache"; -// TODO:: test /** * 检查每日签到 * days @@ -17,9 +17,9 @@ export default class DailyCheckIn extends ITask { static show: boolean = true async execute(data: any) { - const { address } = this.params.user + const { address } = this.user const { task } = data - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) const days = cfg.params.days || 1 const limit = cfg.params.limit || 0 const res = cfg.type === TaskTypeEnum.DAILY ? @@ -28,8 +28,6 @@ export default class DailyCheckIn extends ITask { if (res.errcode) { throw new ZError(res.errcode, res.errmsg) } - let success = false - if ((cfg.type === TaskTypeEnum.DAILY && task.status === TaskStatusEnum.RUNNING && res.data.length >= days) || (cfg.type === TaskTypeEnum.ONCE && task.status === TaskStatusEnum.RUNNING && res.data.count >= days)) { @@ -37,7 +35,7 @@ export default class DailyCheckIn extends ITask { task.timeFinish = Date.now() task.data = res.data try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'save daily checkin failed') } @@ -55,9 +53,9 @@ export default class DailyCheckIn extends ITask { public async claimReward(task: TaskStatus) { // 增加连续签到奖励分 // 请求前7天的签到记录, 往前查找连续签到的记录, - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) if (cfg.type === TaskTypeEnum.DAILY) { - const res = await queryCheckInList(this.params.user.address, 1, 0) + const res = await queryCheckInList(this.user.address, 1, 0) const [taskId, dateTag] = task.id.split(':'); let list: { day: string, time: number, count: number }[] = res.data; @@ -65,14 +63,14 @@ export default class DailyCheckIn extends ITask { let count = list.length > 0 ? list[0].count : 0; let seq = count % countCfg; let score = cfg.params.score[seq] || 0 + cfg.score; - const user = this.params.user + const user = this.user if (user.boost > 1 && Date.now() < user.boostExpire) { score = Math.floor(score * user.boost) } await updateRankScore({ - user: this.params.user.id, + user: this.user.id, score: score, - activity: this.params.user.activity, + activity: this.user.activity, scoreType: cfg.task, scoreParams: { taskId: task.id, @@ -81,8 +79,15 @@ export default class DailyCheckIn extends ITask { boost: user.boost, } }) + await this.claimItem(cfg) } else { - super.claimReward(task); + await super.claimReward(task); + } + // 更新gacha拥有者抽奖次数, 这里写死, 等想到好的方式再处理 + let record = await new LotteryCache().getData(this.user.id, this.user.activity); + if (record.daily === 0) { + record.daily += 1 + record.amount += 1 } } diff --git a/src/tasks/DiscordConnect.ts b/src/tasks/DiscordConnect.ts index e659de1..cfc5278 100644 --- a/src/tasks/DiscordConnect.ts +++ b/src/tasks/DiscordConnect.ts @@ -9,10 +9,10 @@ export default class DiscordConnect extends ITask { static show: boolean = true async execute(data: any) { - const { address } = this.params.user + const { address } = this.user const { task } = data const res = await checkDiscord(address) - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) if (res.status !== 200) { throw new ZError(11, 'discord check failed') } @@ -24,10 +24,10 @@ export default class DiscordConnect extends ITask { task.status = TaskStatusEnum.SUCCESS task.timeFinish = Date.now() task.data = res.data.data - this.params.user.discordId = res.data.data.userid - this.params.user.discordName = res.data.data.username + this.user.discordId = res.data.data.userid + this.user.discordName = res.data.data.username try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'discord already binded') } diff --git a/src/tasks/DiscordJoin.ts b/src/tasks/DiscordJoin.ts index 141bcb5..b260af1 100644 --- a/src/tasks/DiscordJoin.ts +++ b/src/tasks/DiscordJoin.ts @@ -9,7 +9,7 @@ export default class DiscordJoin extends ITask { async execute(data: any) { const { task } = data - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) let time = cfg.params.time; if (Date.now() - task.timeStart < time * 1000) { throw new ZError(11, 'check discord join failed') @@ -22,7 +22,7 @@ export default class DiscordJoin extends ITask { task.timeFinish = Date.now() task.data = {} try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'already join discord') } diff --git a/src/tasks/DiscordRole.ts b/src/tasks/DiscordRole.ts index bd11cf7..873876a 100644 --- a/src/tasks/DiscordRole.ts +++ b/src/tasks/DiscordRole.ts @@ -8,7 +8,7 @@ export default class DiscordRole extends ITask { static show: boolean = true async execute(data: any) { const { task } = data - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) let time = cfg.params.time; if (Date.now() - task.timeStart < time * 1000) { throw new ZError(11, 'check discord role failed') @@ -21,7 +21,7 @@ export default class DiscordRole extends ITask { task.timeFinish = Date.now() task.data = {} try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'already acquired discord role') } diff --git a/src/tasks/OkxLogin.ts b/src/tasks/OkxLogin.ts index c18050e..9107b56 100644 --- a/src/tasks/OkxLogin.ts +++ b/src/tasks/OkxLogin.ts @@ -11,17 +11,17 @@ export default class OkxLogin extends ITask { async execute(data: any) { const { task } = data - const { activity } = this.params.user - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + const { activity } = this.user + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) let wallet = 'okx'; - let record = LoginRecord.findOne({ user: this.params.user.id, activity, wallet}) + let record = LoginRecord.findOne({ user: this.user.id, activity, wallet}) if (!record ) { throw new ZError(11, 'task not finished') } task.status = TaskStatusEnum.SUCCESS task.timeFinish = Date.now() try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'discord already binded') } diff --git a/src/tasks/ShareCode.ts b/src/tasks/ShareCode.ts index 433effe..a3aa713 100644 --- a/src/tasks/ShareCode.ts +++ b/src/tasks/ShareCode.ts @@ -2,13 +2,16 @@ import { ActivityUser, TaskStatus, TaskStatusEnum } from "models/ActivityUser"; import { ITask } from "./base/ITask"; import { TaskCfg } from "models/ActivityInfo"; import { updateRankScore } from "services/rank.svr"; +import { ActivityItem } from "models/ActivityItem"; +import { LotteryCache } from "common/LotteryCache"; +import { checkHadGacha } from "services/chain.svr"; -const updateInviteScore = async (user: typeof ActivityUser, scores: number[], level: number, reason: string) => { +const updateInviteScore = async (user: typeof ActivityUser, scores: number[], items: any[], level: number, reason: string) => { if (!user.inviteUser || scores.length <= level) { return; } let userSup = await ActivityUser.findById(user.inviteUser) - if (!userSup) { + if (!userSup || !userSup.address) { return; } await updateRankScore({ @@ -21,7 +24,22 @@ const updateInviteScore = async (user: typeof ActivityUser, scores: number[], le level } }) - await updateInviteScore(userSup, scores, level + 1, reason) + // 更新gacha拥有者抽奖次数, 这里写死, 等想到好的方式再处理 + let record = await new LotteryCache().getData(userSup.id, userSup.activity); + record.share += 1 + record.amount += 1 + if (record.gacha ===0 && checkHadGacha(userSup.address)) { + record.gacha += 1 + record.amount += 1 + } + if (items.length > level) { + for (let key in items[level]) { + let amount = items[level][key] + await ActivityItem.insertOrUpdate( + {user: userSup, activity: userSup.activity, item: key}, {$inc: {amount}}) + } + } + await updateInviteScore(userSup, scores, items, level + 1, reason) } export default class ShareCode extends ITask { @@ -30,20 +48,21 @@ export default class ShareCode extends ITask { async execute(data: any) { let { task } = data - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) - if (!this.params.user.inviteUser) { + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) + if (!this.user.inviteUser) { throw new Error('not finished') } let scores = cfg.params.score; + const items = cfg.params.inviteItems || []; task.status = TaskStatusEnum.SUCCESS task.timeFinish = Date.now() task.data = {} - await this.params.user.save(); + await this.user.save(); // According to configuration, add score to user who invite current user if (cfg.autoclaim) { try { await super.claimReward(task); - await updateInviteScore(this.params.user, scores, 0, task.task) + await updateInviteScore(this.user, scores, items, 0, task.task) } catch(err) { console.log(err) } @@ -53,8 +72,9 @@ export default class ShareCode extends ITask { public async claimReward(task: TaskStatus) { await super.claimReward(task); - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) let scores = cfg.params.score; - await updateInviteScore(this.params.user, scores, 0, "invite") + const items = cfg.params.inviteItems || []; + await updateInviteScore(this.user, scores, items, 0, "invite") } } \ No newline at end of file diff --git a/src/tasks/TwitterConnect.ts b/src/tasks/TwitterConnect.ts index 091e0a0..cac695e 100644 --- a/src/tasks/TwitterConnect.ts +++ b/src/tasks/TwitterConnect.ts @@ -9,7 +9,7 @@ export default class TwitterConnect extends ITask { static show: boolean = true async execute(data: any) { - let { address } = this.params.user + let { address } = this.user let res = await checkTwitter(address) if (res.status !== 200) { throw new ZError(11, 'twitter check failed') @@ -22,14 +22,14 @@ export default class TwitterConnect extends ITask { task.status = TaskStatusEnum.SUCCESS task.timeFinish = Date.now() task.data = res.data.data - this.params.user.twitterId = res.data.data.userid - this.params.user.twitterName = res.data.data.username + this.user.twitterId = res.data.data.userid + this.user.twitterName = res.data.data.username try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'twitter already binded') } - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) if (cfg.autoclaim) { try { await this.claimReward(task); diff --git a/src/tasks/TwitterFollow.ts b/src/tasks/TwitterFollow.ts index e7fc7ba..87a28d5 100644 --- a/src/tasks/TwitterFollow.ts +++ b/src/tasks/TwitterFollow.ts @@ -8,7 +8,7 @@ export default class TwitterFollow extends ITask { static show: boolean = true async execute(data: any) { const { task } = data - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) let time = cfg.params.time; if (Date.now() - task.timeStart < time * 1000) { throw new ZError(11, 'follow failed') @@ -21,7 +21,7 @@ export default class TwitterFollow extends ITask { task.timeFinish = Date.now() task.data = {} try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'save failed') } diff --git a/src/tasks/TwitterRetweet.ts b/src/tasks/TwitterRetweet.ts index cff0ffa..ff9d706 100644 --- a/src/tasks/TwitterRetweet.ts +++ b/src/tasks/TwitterRetweet.ts @@ -7,7 +7,7 @@ export default class TwitterRetweet extends ITask { static show: boolean = true async execute(data: any) { const { task } = data - let cfg = this.params.activity.tasks.find(t => t.id === task.id) + let cfg = this.activity.tasks.find(t => t.id === task.id) let time = cfg.params.time; if (Date.now() - task.timeStart < time * 1000) { throw new ZError(11, 'retweet failed') @@ -20,7 +20,7 @@ export default class TwitterRetweet extends ITask { task.timeFinish = Date.now() task.data = {} try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'save failed') } diff --git a/src/tasks/YoutubeFollow.ts b/src/tasks/YoutubeFollow.ts index a7dd3ce..b2770b4 100644 --- a/src/tasks/YoutubeFollow.ts +++ b/src/tasks/YoutubeFollow.ts @@ -8,7 +8,7 @@ export default class YoutubeFollow extends ITask { static show: boolean = true async execute(data: any) { const { task } = data - let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id) let time = cfg.params.time; if (Date.now() - task.timeStart < time * 1000) { throw new ZError(11, 'follow failed') @@ -21,7 +21,7 @@ export default class YoutubeFollow extends ITask { task.timeFinish = Date.now() task.data = {} try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'save failed') } diff --git a/src/tasks/YoutubePost.ts b/src/tasks/YoutubePost.ts index 7250c19..15377dc 100644 --- a/src/tasks/YoutubePost.ts +++ b/src/tasks/YoutubePost.ts @@ -7,7 +7,7 @@ export default class YoutubePost extends ITask { static show: boolean = true async execute(data: any) { const { task } = data - let cfg = this.params.activity.tasks.find(t => t.id === task.id) + let cfg = this.activity.tasks.find(t => t.id === task.id) let time = cfg.params.time; if (Date.now() - task.timeStart < time * 1000) { throw new ZError(11, 'post failed') @@ -20,7 +20,7 @@ export default class YoutubePost extends ITask { task.timeFinish = Date.now() task.data = {} try { - await this.params.user.save() + await this.user.save() } catch(err) { throw new ZError(100, 'save failed') } diff --git a/src/tasks/base/ITask.ts b/src/tasks/base/ITask.ts index 1a9d9f9..3f9c203 100644 --- a/src/tasks/base/ITask.ts +++ b/src/tasks/base/ITask.ts @@ -1,5 +1,6 @@ -import { TaskCfg } from "models/ActivityInfo" -import { TaskStatus, TaskStatusEnum } from "models/ActivityUser" +import { ActivityInfo, TaskCfg } from "models/ActivityInfo" +import { ActivityItem } from "models/ActivityItem" +import { ActivityUser, TaskStatus, TaskStatusEnum } from "models/ActivityUser" import { updateRankScore } from "services/rank.svr" export abstract class ITask { @@ -7,17 +8,21 @@ export abstract class ITask { static show: boolean = true static auto: boolean = false - params: any - constructor(params: any) { + user: typeof ActivityUser + activity: typeof ActivityInfo + + + constructor({user, activity}: {user: typeof ActivityUser, activity: typeof ActivityInfo}) { // do nothing - this.params = params + this.user = user + this.activity = activity } abstract execute(data: any): Promise public async claimReward(task: any) { - const user = this.params.user + const user = this.user const [taskId, dateTag] = task.id.split(':'); - const cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === taskId) + const cfg = this.activity.tasks.find((t: TaskCfg) => t.id === taskId) if (!cfg.score) { return; } @@ -48,6 +53,17 @@ export abstract class ITask { task.status = TaskStatusEnum.CLAIMED task.timeClaim = Date.now() } + await this.claimItem(cfg) await user.save() } + + public async claimItem(cfg: TaskCfg) { + if (cfg.params.items) { + for (let key in cfg.params.items) { + let amount = cfg.params.items[key] + await ActivityItem.insertOrUpdate( + {user: this.user.id, activity: this.user.activity, item: key}, {$inc: {amount}, last: Date.now()}) + } + } + } } \ No newline at end of file