diff --git a/configs/uaw_daily_sign.json b/configs/uaw_daily_sign.json new file mode 100644 index 0000000..be2be0e --- /dev/null +++ b/configs/uaw_daily_sign.json @@ -0,0 +1,46 @@ +{ + "total_sign": [ + { + "days": 3, + "reward": 1 + }, + { + "days": 4, + "reward": 4 + }, + { + "days": 5, + "reward": 5 + }, + { + "days": 6, + "reward": 6 + }, + { + "days": 7, + "reward": 7 + } + ], + "sequential_sign": [ + { + "days": 3, + "reward": 1 + }, + { + "days": 4, + "reward": 1 + }, + { + "days": 5, + "reward": 1 + }, + { + "days": 6, + "reward": 1 + }, + { + "days": 7, + "reward": 1 + } + ] +} diff --git a/configs/uaw_rank_level.json b/configs/uaw_rank_level.json new file mode 100644 index 0000000..0c80431 --- /dev/null +++ b/configs/uaw_rank_level.json @@ -0,0 +1,26 @@ +[ + { + "score": 600, + "level": "Challenger" + }, + { + "score": 500, + "level": "Diamond" + }, + { + "score": 400, + "level": "Platnum" + }, + { + "score": 100, + "level": "Bronze" + }, + { + "score": 300, + "level": "Gold" + }, + { + "score": 0, + "level": "Silver" + } +] diff --git a/docs/uaw.md b/docs/uaw.md new file mode 100644 index 0000000..8d4b198 --- /dev/null +++ b/docs/uaw.md @@ -0,0 +1,629 @@ +# UAW相关接口 + +### 说明 + +1. 通用返回格式, errcode=0 表示无错误 + +2. 如无特别说明, 以下接口的 Response 格式指的是 data 字段 +3. 接口名中带\*的表示, 需要验证 token, token 可以设置 header 的 Authorization: Bearer JWT_token, 或 Post body 的 token 字段, 或 Get 的 query token + +```json +{ + "errcode": Number, + "errmsg": String, + "data": {} +} +``` + +### 1. 钱包预登录 + +#### Request + +- URL:`/api/wallet/nonce?address=` +- 方法:`GET` + +query param + +| Name | Type | Desc | +| ------- | ------ | -------- | +| address | string | 钱包地址 | + + + +#### Response + +```json +{ + "nonce": String, + "tips": String +} +``` + +### 2. 钱包登录 + +客户端在获得钱包地址后,须先调用预登录方法获取 nonce 和 tips, 然后调用钱包进行 EIP-721 签名. + +登录成功后返回的 jwt 需要保存至本地存储, 再次载入后, 可解析并获取 exp 字段, 判断当前 token 是否已经过期 + +#### Request + +- URL:`/api/wallet/login` +- 方法:`POST` +- 头部: + - Content-type: application/json +- Body: + +```json +{ + "activity": String, + "signature": String, + "message": SiweMessage +} +``` + +SiweMessage说明: https://docs.login.xyz/sign-in-with-ethereum/quickstart-guide/creating-siwe-messages + +SiweMessage的nonce说明(具体参考例子): + +``` +1. 从钱包预登录接口获取nonce +2. nonce = nonce + '|' + 钱包类型字符串 // 比如okx钱包, nonce|okx +3. 使用 活动id 作为 key, 调用aesEncrypt 加密步骤2的字符串 +4. 将hex string 转换成base58, 传给SiweMessage +``` + + + +#### Response + +```json +{ + "token": String, +} +``` + +### 3. 社交任务活动信息 + +#### Request + +- URL:`/api/activity/:id` +- 方法:`GET` +- 参数: + - `id`(当前活动的ID) + +#### Response + +```js + { + "_id": "TwitterConnect", // 任务id + "name": "活动名称", + "description": "活动描述", + "tasks": [ // 该活动需要完成的任务 + { + "id": "任务id", + "task": "任务类型", + "title": "任务名", + "desc": "任务描述", + "type": 1, //任务类型, 1: 一次性任务, 2: 日常任务 + "repeat": 1, // 任务可重复次数 + "pretasks": ["task id 1"], //前置任务 + "score": 0, // 完成任务可获得的积分 + "category": "", // 任务分类 + "cfg": {}, // 其他一些任务相关配置参数, 比如icon, 或者其他未考虑的参数 + "end": false, // 是否已经结束 + "autoclaim": false // 任务完成后是否自动获取奖励 + } + ], + "startTime": 1702628292366, // 活动开始时间 + "endTime": 1705220292366 // 活动结束时间 + } +``` + + +### 4. *社交任务进度 + +#### Request + +- URL:`/api/tasks/progress` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token +- Body: {} + + +#### Response + +```js +[ + { + "status": 2, // 任务状态, 0: 未开始, 1: 进行中, 2: 成功, 9: 失败 + "id": "TwitterConnect", // 任务id + "timeStart": 1703150269527, // 任务开始时间 + "data": { // 当前任务带的额外信息, 比如twitter的id和昵称等 + "username": "zhl01", + "userid": "564269223" + }, + "timeFinish": 1703150280059 // 任务结束时间 + } + ] +``` + +### 5.\* 开始某个任务 + +#### Request + +- URL:`/api/tasks/begin_task` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token + + +body: + +```js +{ + "task": "TwitterFollow" // 任务id +} + +``` + +#### Response + +```json +{ + "status": 1, // 任务状态, 0: 未开始, 1: 进行中, 2: 成功, 9: 失败 + "id": "TwitterFollow", // 任务id + "timeStart": 1703150294051 // 任务开始时间 + } +``` + +### 6.\* 检查任务状态 + +#### Request + +- URL:`/api/tasks/check_task` +- 方法:`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 + } +``` + +### 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 + +- URL:`/api/activity/upload_invite_code` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + +body: + +```js +{ + "code": "邀请人的邀请码" +} + +``` + +#### Response + +> 只要不返回errcode, 即表示上传成功 +```json +{} +``` + +### 9. 积分排行榜 + +#### Request + +- URL:`/api/activity/leaderboard/:activity/:page` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token +- 参数: + - `activity`(当前活动的ID) + - `page` (返回数据的分页序号, 0 开始) + +> 默认返回100条记录, 如果要返回不同数量, query param传 limit + +#### Response + +```json +[ + { + "rank": 1, // 排名, 从1开始 + "level": 1, // 段位 + "nickname": "昵称", + "score": 1 //获得的积分 + } +] +``` + +### 10.\* 用户状态 + +#### Request + +- URL:`/api/user/state` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + +#### Response + +```json +{ + "address": "钱包地址", + "boost": 1, // 正常值为1, 本次活动不用考虑 + "boostExpire": 0, // 计算得分时, 如果boost过期, 即使boost大于1, 也不计算boost, 本次活动不用考虑 + "twitterId": "", + "twitterName": "", + "discordId": "", + "discordName": "", + "scoreToday": 100, // 今日获得积分 + "scoreTotal": 200, // 总积分 + "rankTotal": "-", + "invite": "邀请人address", + "inviteCount": 0, // 我邀请的用户总数 + "inviteScore": 0, // 我邀请用户总数获得的分数 + "code": "自己的邀请码", + "mapopen": 0, // 地图开启状态, 0: 未开启, 1: 已开启 + } +``` + + +### 11.\* 签到列表 + +#### Request + +- URL:`/api/user/checkin/list/:tag` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + +query param + +| Name | Type | Desc | +| ------- | ------ | -------- | +| tag | string | last: 最新, 前一天+今天, 1month: 当月 | +#### Response + +```json +[ + { + "address": "钱包地址", + "day": "20240105", // 格式化后签到日期, 时区按SG(UTC+8) + "time": 1704436745, // 具体的签到时间 + "count": 0, //连签天数 + "total": 10 //累计签到天数 + }, +] +``` + + + +### 12.\* 探索状态 + +#### Request + +- URL:`/api/game/stat` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + + +#### Response + +```js +{ + ticket: 1, // 可用探索次数 + todayStat: 0, // 当日签到状态, 0: 未签到, 1: 已签到,但未领取 9:已签到, 已领取 + todayTickets: 1, // 当日签到可领取次数 + daysTotal: 1, // 累计签到天数 + daysSeq: 1, // 连续签到天数 + totalStat: [{ // 累计签到状态 + days: 3, // 天数 + tickets: 2, // 满足条件后可领取数量 + state: 0, // 领取状态: 0: 未领取, 1: 可领取, 9: 已领取 + }], + signCfg: [{ // 连续签到配置 + days: 2, //天数 + tickets: 1, //额外增加的次数 + }] +} +``` + +### 13.\* 探索 + +#### Request + +- URL:`/api/game/step` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token + +body: + +```js +{ + "step": 2 // 使用的次数 +} + +``` + +#### Response + +```js +{ + score: 20, //获得积分数量 + chest: [{ + id: '1112323131', // 获得宝箱id + level: 1, //宝箱品级 + }] +} +``` + +### 14.\* 领取累计签到奖励 + +#### Request + +- URL:`/api/user/checkin/claim` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token + +body: + +```js +{ + "days": 3 // 领取的累计签到天数 +} + +``` + +#### Response + +```js +{ + ticket: 2, //获得探索次数 +} +``` + +### 15.\* 已邀请列表 + +#### Request + +- URL:`/api/activity/invite_list` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + + +#### Response + +```js +[{ + level: 1, // 段位 + nickname: '用户昵称', + score: 100, // 获得的积分 +}] +``` + +### 16.\* 宝箱列表 + +#### Request + +- URL:`/api/chest/list` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + + +#### Response + +```js +[{ + id: 1, // 箱子id + stat: 0, // 0: 锁定, 1: 正常 + shareCode: '箱子的分享码', + level: 1, // 箱子品级 + maxBonus: 10, // 最大可助力数量 + scoreInit: 5, // 初始可获得积分 + scoreBonus: 10, // 助力增加的分数 + bonusCount: 2, // 已助力次数 +}] +``` + +### 17.\* 宝箱助力记录 + +#### Request + +- URL:`/api/chest/enhance/list` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token + +body: + +```js +{ + chestid: '12312313' // 宝箱id +} + +``` + +#### Response + +```js +[{ + level: 1, // 段位 + nickname: '用户昵称', + score: 100, // 获得的积分 +}] +``` + +### 18.\* 宝箱助力 + +#### Request + +- URL:`/api/chest/enhance` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token + +body: + +```js +{ + code: '131aasd`1' // 宝箱的分享码 +} + +``` + +#### Response + +```js +{ + score: 100, // 自己获得的积分 +} +``` + +### 19.\* 开启宝箱(这个应该会修改) + +#### Request + +- URL:`/api/chest/open` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token + +body: + +```js +{ + chestid: '131aasd`1' // 宝箱id +} + +``` + +#### Response + +```js +{ + score: 100, // 获得的积分 +} +``` + +### 20.\* 宝箱开启记录 + +#### Request + +- URL:`/api/chest/open/history` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + +#### Response + +```js +[{ + chest: '123123123', + level: 1, // 箱子品级 + score: 100, // 获得的积分 + time: 111 // 开启时间 +}] +``` + +### 21.\* 积分详情列表 + +#### Request + +- URL:`/api/activity/score_list` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + +#### Response + +```js +[{ + score: 100, // 获得的积分 + type: '', // 获取原因 + time: 111 // 开启时间 +}] +``` + +### 22.\* 开启地图 + +#### Request + +- URL:`/api/game/open` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + +> 返回没errcode就表示开启成功 + +### 23.\* 检查签到并领取奖励 + +#### Request + +- URL:`/api/user/checkin` +- 方法:`POST` +- 头部: + - Authorization: Bearer JWT_token + +#### Response + +```js +{ + ticket: 1 // 获得的探索次数 +} +``` \ No newline at end of file diff --git a/initdatas/activity_info.json b/initdatas/activity_info.json index b511266..d4da622 100644 --- a/initdatas/activity_info.json +++ b/initdatas/activity_info.json @@ -281,7 +281,7 @@ "score": 200, "params": {"address": "0xe2E4D5a4045fBFcbCBECAf5b8A94303712d2FA97"} }], - "startTime": 1702628292366, - "endTime": 1705220292366 + "startTime": 1711086450119, + "endTime": 1713678477701 } ] \ No newline at end of file diff --git a/initdatas/uaw_cfg.json b/initdatas/uaw_cfg.json new file mode 100644 index 0000000..3a05d6f --- /dev/null +++ b/initdatas/uaw_cfg.json @@ -0,0 +1,179 @@ +[ + { + "_id": "uaw_activity", + "name": "UAW Activity", + "description": "UAW", + "tasks": [ + { + "id": "e2yhq2lj30vwcpedv7p", + "task": "TwitterConnect", + "title": "", + "type": 1, + "desc": "", + "score": 0, + "category": "", + "autoclaim": false, + "cfg": {"icon": "twitter"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", + "params": {} + }, { + "id": "e2fclylj30vwcpe0szl", + "task": "TwitterFollow", + "title": "", + "type": 1, + "desc": "Follow Counter Fire’s official X account", + "category": "Social Tasks", + "score": 100, + "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", + "task": "TwitterFollow", + "title": "", + "type": 1, + "desc": "", + "category": "Social Tasks", + "score": 100, + "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", + "task": "TwitterRetweet", + "title": "", + "type": 1, + "desc": "Show your friends Counter Fire.", + "category": "Social Tasks", + "score": 150, + "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", + "task": "TwitterRetweet", + "title": "", + "type": 1, + "desc": "Click Verify button and retweet on the tweet", + "category": "Social Tasks", + "score": 200, + "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", + "task": "DiscordConnect", + "title": "", + "type": 1, + "desc": "", + "category": "", + "score": 0, + "autoclaim": false, + "pretasks": [], + "cfg": {"icon": "discord"}, + "start": "2024-01-01 00:00", + "end": "2025-01-01 00:00", + "params": {} + }, { + "id": "e2far3lj30vwcpe0mf8", + "task": "DiscordJoin", + "title": "", + "type": 1, + "desc": "Join Counter Fire’s official Discord server", + "category": "Social Tasks", + "score": 100, + "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", + "task": "DiscordRole", + "title": "Discord Role", + "type": 1, + "desc": "Get a role in Counter Fire’s official Discord ", + "category": "Social Tasks", + "score": 200, + "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", + "task": "YoutubeFollow", + "title": "", + "type": 1, + "desc": "Follow Counter Fire’s official YTB account", + "category": "Social Tasks", + "score": 100, + "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", + "task": "YoutubePost", + "title": "", + "type": 1, + "desc": "Post a video introducing @_CounterFire", + "category": "Social Tasks", + "score": 500, + "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", + "task": "TwitterRetweet", + "title": "", + "type": 2, + "desc": "Showcase your performance in Counter Fire to your friends!", + "category": "Social Tasks", + "score": 300, + "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", + "task": "TwitterRetweet", + "title": "", + "type": 1, + "desc": "Post to confess your 💕 for @_CounterFire", + "category": "Referral to Earn", + "score": 100, + "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} + } + ], + "startTime": 1711086450119, + "endTime": 1713678477701 + } +] \ No newline at end of file diff --git a/packages/zutils b/packages/zutils index e207a95..b97e334 160000 --- a/packages/zutils +++ b/packages/zutils @@ -1 +1 @@ -Subproject commit e207a95fd79c0e926668074e68ca2467952d1ded +Subproject commit b97e33472f46eb8fb47a8cf3c3924c5d26af5eca diff --git a/src/controllers/activity.controller.ts b/src/controllers/activity.controller.ts index a17d957..38ef839 100644 --- a/src/controllers/activity.controller.ts +++ b/src/controllers/activity.controller.ts @@ -1,10 +1,11 @@ import { ActivityInfo } from 'models/ActivityInfo' import { ActivityUser } from 'models/ActivityUser' -import { rankKey } from 'services/rank.svr' -import { yesterday } from 'zutils/utils/date.util' +import { rankKey, rankLevel } from 'services/rank.svr' import { BaseController, ROLE_ANON, SyncLocker, ZError, ZRedisClient, role, router } from 'zutils' +import { ScoreRecord } from 'models/ScoreRecord' +import { formatAddress } from 'zutils/utils/chain.util' -const MAX_LIMIT = 50 +const MAX_LIMIT = 100 export default class ActivityController extends BaseController { @role(ROLE_ANON) @router('get /api/activity/:id') @@ -42,6 +43,45 @@ export default class ActivityController extends BaseController { return {} } + /** + * 邀请列表 + */ + @router('get /api/activity/invite_list') + async inviteUserList(req) { + let user = req.user + const totalKey = rankKey(user.activity) + let users = await ActivityUser.find({ inviteUser: user.id }) + let results = [] + for (let u of users) { + const totalScore = await new ZRedisClient().zscore(totalKey, u.id) + const score = totalScore ? parseInt(totalScore + '') : 0 + results.push({ + // user: u.id, + level: rankLevel(score), + nickname: u.twitterName || u.discordName || formatAddress(u.address), + score, + }) + } + return results + } + + /** + * 积分详情列表 + */ + @router('get /api/activity/score_list') + async scoreList(req) { + let user = req.user + const records = await ScoreRecord.find({ user: user.id, activity: user.activity }) + return records.map(record => { + return { + score: record.score, + type: record.type, + //@ts-ignore + time: record.createdAt.getTime(), + } + }) + } + @role(ROLE_ANON) @router('get /api/activity/leaderboard/:activity/:page') async inviteCode(req) { @@ -53,25 +93,25 @@ export default class ActivityController extends BaseController { const end = start + limit - 1 const records = await new ZRedisClient().zrevrange(`${activity}:score`, start, end) let results: any = [] - const yesterdayKey = rankKey(activity, yesterday()) + // const yesterdayKey = rankKey(activity, yesterday()) for (let i = 0; i < records.length; i += 2) { const id = records[i] let score = parseInt(records[i + 1]) const user = await ActivityUser.findById(id) - let invite = '' - if (user?.inviteUser) { - const inviteUser = await ActivityUser.findById(user.inviteUser) - if (inviteUser) { - invite = inviteUser.address - } - } - const yesterdayScore = await new ZRedisClient().zscore(yesterdayKey, id) + // let invite = '' + // if (user?.inviteUser) { + // const inviteUser = await ActivityUser.findById(user.inviteUser) + // if (inviteUser) { + // invite = inviteUser.address + // } + // } + // const yesterdayScore = await new ZRedisClient().zscore(yesterdayKey, id) results.push({ rank: start + i / 2 + 1, - address: user?.address || 'unknow', - invite, + level: rankLevel(score), + nickname: user.twitterName || user.discordName || formatAddress(user.address), score, - yesterday: yesterdayScore ? parseInt(yesterdayScore + '') : 0, + // yesterday: yesterdayScore ? parseInt(yesterdayScore + '') : 0, }) } return results diff --git a/src/controllers/chain.controller.ts b/src/controllers/chain.controller.ts index d1ce347..ef8d3bd 100644 --- a/src/controllers/chain.controller.ts +++ b/src/controllers/chain.controller.ts @@ -6,7 +6,7 @@ import { queryStakeList } from 'services/chain.svr' import { sign } from 'zutils/utils/chain.util' export default class ChainController extends BaseController { - @router('get /api/stake/list') + // @router('get /api/stake/list') async stakeList(req) { const user = req.user const records = await queryStakeList(user.address) @@ -14,7 +14,7 @@ export default class ChainController extends BaseController { return result } - @router('post /api/lottery/claim_usdt') + // @router('post /api/lottery/claim_usdt') async preClaimUsdt(req: FastifyRequest) { new SyncLocker().checkLock(req) let user = req.user diff --git a/src/controllers/chest.controller.ts b/src/controllers/chest.controller.ts new file mode 100644 index 0000000..acc6979 --- /dev/null +++ b/src/controllers/chest.controller.ts @@ -0,0 +1,155 @@ +import { ZError, SyncLocker, ZRedisClient, BaseController, ROLE_ANON, role, router } from 'zutils' +import { ActivityChest } from 'models/ActivityChest' +import { ActivityUser } from 'models/ActivityUser' +import { mongoose } from '@typegoose/typegoose' +import { rankKey, rankLevel, updateRankScore } from 'services/rank.svr' +import { formatDate } from 'zutils/utils/date.util' +import { ScoreRecord } from 'models/ScoreRecord' +/** + * 宝箱相关接口 + */ +class BoxController extends BaseController { + /** + * 宝箱列表 + */ + @router('get /api/chest/list') + async chestList(req) { + const user = req.user + const chests = await ActivityChest.find({ activity: user.activity, user, status: 0 }) + return chests.map(chest => chest.toJson()) + } + /** + * 宝箱助力列表 + */ + @router('post /api/chest/enhance/list') + async enhanceList(req) { + const user = req.user + const { chestid } = req.query + if (!chestid) { + throw new ZError(11, 'chestid is required') + } + const chest = await ActivityChest.findById(chestid) + if (!chest) { + throw new ZError(12, 'chest not found') + } + if (chest.status === 0) { + throw new ZError(13, 'chest is locked') + } + if (chest.status === 9) { + throw new ZError(14, 'chest had been opened') + } + const result = [] + const users = await ActivityUser.find({ _id: { $in: chest.bonusUsers } }) + const userMap = new Map() + const totalKey = rankKey(user.activity) + for (let user of users) { + // get score from redis + userMap.set(user.id, user) + } + for (let i = 0; i < chest.bonusUsers.length; i++) { + const user = userMap.get(chest.bonusUsers[i]) + const totalScore = await new ZRedisClient().zscore(totalKey, user.id) + const score = totalScore ? parseInt(totalScore + '') : 0 + result.push({ + nickname: user.twitterName || user.discordName, + level: rankLevel(score), + score: score, + }) + } + return result + } + /** + * 宝箱助力 + */ + @router('post /api/chest/enhance') + async enhance(req) { + new SyncLocker().checkLock(req) + const { code } = req.params + const user = req.user + const uid = user.uid + const score = 10 //TODO:: 根据规则生成助力积分 + const session = await mongoose.startSession() + session.startTransaction() + try { + const chest = await ActivityChest.findOne({ shareCode: code, activity: user.activity }).session(session) + if (chest.bonusUsers.includes(uid)) { + throw new ZError(10, 'user already enhanced') + } + if (chest.bonusUsers.length >= chest.maxBonus) { + throw new ZError(12, 'enhanced times exceed') + } + chest.bonusUsers.push(uid) + chest.bonusScores.push(score) + chest.scoreBonus += score + await chest.save({ session }) + await session.commitTransaction() + session.endSession() + return { + score: 1, // TODO:: 根据规则生成自己获得助力积分 + } + } catch (error) { + session.abortTransaction() + session.endSession() + throw error + } + } + + /** + * 开启宝箱 + */ + @router('post /api/chest/open') + async openChest(req) { + new SyncLocker().checkLock(req) + const user = req.user + const { chestid } = req.query + if (!chestid) { + throw new ZError(11, 'chestid is required') + } + const chest = await ActivityChest.findById(chestid) + if (!chest) { + throw new ZError(12, 'chest not found') + } + if (chest.user !== user.id) { + throw new ZError(13, 'chest not belong to user') + } + if (chest.status === 1) { + throw new ZError(14, 'chest already opened') + } + chest.status = 1 + const score = chest.scoreInit + chest.scoreBonus + const dateTag = formatDate(new Date()) + await updateRankScore({ + user: user.id, + score: score, + activity: user.activity, + scoreType: 'open_chest', + scoreParams: { + date: dateTag, + chestId: chest.id, + level: chest.level, + }, + }) + await chest.save() + return { score } + } + + /** + * 宝箱开启记录 + */ + @router('get /api/chest/open/history') + async openChestHistory(req) { + const user = req.user + const records = await ScoreRecord.find({ user: user.id, activity: user.activity, scoreType: 'open_chest' }).sort({ + createdAt: -1, + }) + return records.map(record => { + return { + chest: record.data.chestId, + score: record.score, + level: record.data.level, + // @ts-ignore + time: record.createdAt.getTime(), + } + }) + } +} diff --git a/src/controllers/game.controller.ts b/src/controllers/game.controller.ts new file mode 100644 index 0000000..62db543 --- /dev/null +++ b/src/controllers/game.controller.ts @@ -0,0 +1,204 @@ +import { ActivityGame } from 'models/ActivityGame' +import { DAILY_SIGN, SIGN_TOTAL, TicketRecord, USE_TICKET } from 'models/TicketRecord' +import { queryCheckInList } from 'services/chain.svr' +import { checkInToday, seqSignCfg, seqSignScore, totalSignCfg, totalSignScore } from 'services/sign.svr' +import { ZError, SyncLocker, ZRedisClient, BaseController, ROLE_ANON, role, router } from 'zutils' +import { formatDate } from 'zutils/utils/date.util' +/** + * 探索游戏相关接口 + */ +class GameController extends BaseController { + /** + * 签到 + */ + @router('post /api/user/checkin') + async checkIn(req) { + new SyncLocker().checkLock(req) + const user = req.user + const { address } = user + const dateTag = formatDate(new Date()) + const gameRecord = await ActivityGame.insertOrUpdate({ user: user.id, activity: user.activity }, {}) + if (gameRecord.status === 0) { + throw new ZError(11, 'map not open') + } + if (dateTag === gameRecord.lastSignDay) { + throw new ZError(12, 'already claimed') + } + const record = await checkInToday(address, dateTag) + if (!record) { + throw new ZError(13, 'had not signed in') + } + const reward = seqSignScore(record.count) + gameRecord.lastSignDay = dateTag + gameRecord.tickets += 1 + reward + const ticketRecord = new TicketRecord({ + user: user.id, + activity: user.activity, + type: DAILY_SIGN, + data: {}, + score: 1 + reward, + }) + await ticketRecord.save() + await gameRecord.save() + return { ticket: reward + 1 } + } + + /** + * 签到列表 + */ + @router('get /api/user/checkin/list/:tag') + async checkInList(req) { + const user = req.user + const { tag } = req.params + let days: any = 1 + if (tag === '1month') { + days = '1month' + } else if (tag === 'last') { + days = '1' + } + const res = await queryCheckInList(user.address, days, 0) + return res + } + /** + * 领取累计签到奖励 + * //TODO:: test + */ + @router('post /api/user/checkin/claim') + async claimCheckResult(req) { + new SyncLocker().checkLock(req) + const user = req.user + let { days } = req.params + if (!days) { + throw new ZError(11, 'invalid days') + } + days = parseInt(days) + const dateTag = formatDate(new Date()) + const checkRecord = await checkInToday(user.address, dateTag) + if (!checkRecord) { + throw new ZError(12, 'not signed in') + } + if (days > checkRecord.total) { + throw new ZError(13, 'invalid days') + } + const ticketRecords = await TicketRecord.find({ user: user.id, activity: user.activity, type: SIGN_TOTAL }) + const claimedSet = new Set() + ticketRecords.forEach(record => { + claimedSet.add(record.data.day) + }) + if (claimedSet.has(days)) { + throw new ZError(14, 'already claimed') + } + + const score = totalSignScore(days) + if (score === 0) { + throw new ZError(15, 'invalid days') + } + const record = new TicketRecord({ + user: user.id, + activity: user.activity, + type: SIGN_TOTAL, + data: { day: days }, + score, + }) + const gameRecord = await ActivityGame.insertOrUpdate({ user: user.id, activity: user.activity }, {}) + gameRecord.tickets += score + await gameRecord.save() + await record.save() + return { ticket: score } + } + /** + * //TODO::开启地图 + */ + @router('get /api/game/open') + async openMap(req) { + const user = req.user + return {} + } + /** + * 探索状态 + */ + @router('get /api/game/stat') + async gameStat(req, res) { + const user = req.user + const record = await ActivityGame.insertOrUpdate({ user: user.id, activity: user.activity }, {}) + const signCfg = seqSignCfg + const dateTag = formatDate(new Date()) + const checkRecord = await checkInToday(user.address, dateTag) + let todayStat = 0 + if (checkRecord) { + // 有签到的链上记录, 说明已签到 + todayStat = 1 + } + if (record.lastSignDay === dateTag) { + // 检查是否已领取 + todayStat = 2 + } + const scoreBonus = seqSignScore(checkRecord.count) + const ticketRecords = await TicketRecord.find({ user: user.id, activity: user.activity, type: SIGN_TOTAL }) + const claimedSet = new Set() + ticketRecords.forEach(record => { + claimedSet.add(record.data.day) + }) + + const totalStat = [] + for (let cfg of totalSignCfg) { + let state = 0 + if (cfg.days <= checkRecord.total) { + state = 1 + } + if (claimedSet.has(cfg.days)) { + state = 9 + } + totalStat.push({ + days: cfg.days, + tickets: cfg.reward, + state, + }) + } + return { + ticket: record.tickets, + signCfg, + todayStat, + todayTickets: 1 + scoreBonus, + daysTotal: checkRecord.total, + daysSeq: checkRecord.count, + totalStat, + } + } + /** + * 探索 + */ + @router('post /api/game/step') + async gameStep(req, res) { + new SyncLocker().checkLock(req) + const user = req.user + let { step } = req.params + step = step || '1' + if (isNaN(step)) { + throw new ZError(11, 'invalid step') + } + step = parseInt(step) + + const record = await ActivityGame.insertOrUpdate({ user: user.id, activity: user.activity }, {}) + if (record.status === 0) { + throw new ZError(12, 'map not open') + } + if (record.tickets < step) { + throw new ZError(13, 'insufficient tickets') + } + record.tickets -= step + // TODO:: 生成奖励, 存入data, 并返回 + const ticketRecord = new TicketRecord({ + user: user.id, + activity: user.activity, + type: USE_TICKET, + data: {}, + score: -step, + }) + await ticketRecord.save() + await record.save() + let result = [] + + return result + } +} diff --git a/src/controllers/lottery.controller.ts b/src/controllers/lottery.controller.ts index 77700d4..0eb4ed2 100644 --- a/src/controllers/lottery.controller.ts +++ b/src/controllers/lottery.controller.ts @@ -27,7 +27,7 @@ const draw = (rewards: { probability: number }[]) => { } export default class LotteryController extends BaseController { - @router('get /api/lottery/stats') + // @router('get /api/lottery/stats') async userStats(req) { let user = req.user let record = await new LotteryCache().getData(user.id, user.activity) @@ -37,7 +37,7 @@ export default class LotteryController extends BaseController { return result } - @router('post /api/lottery/history') + // @router('post /api/lottery/history') async userLotteryHistory(req) { let user = req.user let { day, page, limit } = req.params @@ -59,7 +59,7 @@ export default class LotteryController extends BaseController { } } - @router('get /api/lottery/items') + // @router('get /api/lottery/items') async items(req) { const items = ALL_ITEMS const cfgs = LOTTERY_CFG @@ -78,7 +78,7 @@ export default class LotteryController extends BaseController { return { items: result, start: cfgs.start, end: cfgs.end } } - @router('get /api/lottery/draw') + // @router('get /api/lottery/draw') async draw(req) { new SyncLocker().checkLock(req) let user = req.user @@ -142,7 +142,7 @@ export default class LotteryController extends BaseController { return reward } - @router('get /api/lottery/fusion') + // @router('get /api/lottery/fusion') async fusion(req) { new SyncLocker().checkLock(req) let user = req.user diff --git a/src/controllers/sign.controller.ts b/src/controllers/sign.controller.ts index 06b8e21..ef1542c 100644 --- a/src/controllers/sign.controller.ts +++ b/src/controllers/sign.controller.ts @@ -5,11 +5,12 @@ import { DEFAULT_EXPIRED, NonceRecord } from 'models/NonceRecord' import { ScoreRecord } from 'models/ScoreRecord' import { LoginRecordQueue } from 'queue/loginrecord.queue' import { queryCheckInList } from 'services/chain.svr' -import { rankKey } from 'services/rank.svr' +import { rankKey, updateRankScore } from 'services/rank.svr' import { SiweMessage } from 'siwe' import { nextday } from 'zutils/utils/date.util' import { checkParamsNeeded } from 'zutils/utils/net.util' -import { aesDecrypt, base58ToHex } from 'zutils/utils/security.util' +import { aesDecrypt } from 'zutils/utils/security.util' +import { base58ToHex } from 'zutils/utils/string.util' const LOGIN_TIP = 'This signature is just to verify your identity' @@ -133,7 +134,8 @@ class SignController extends BaseController { } return result } - @router('get /api/user/state/boost') + + // @router('get /api/user/state/boost') async boost(req) { new SyncLocker().checkLock(req) const user = req.user @@ -145,32 +147,4 @@ class SignController extends BaseController { await user.save() return { boost: user.boost, boostExpire: user.boostExpire } } - - @router('get /api/user/checkin/list/:tag') - async checkInList(req) { - const user = req.user - const { tag } = req.params - let days: any = 1 - if (tag === '1month') { - days = '1month' - } else if (tag === 'last') { - days = '1' - } - const res = await queryCheckInList(user.address, days, 0) - return res.data - } - - /** - * regist user by token from wallet-svr - * TODO:: - * @param req - * @param res - * @returns - */ - @role(ROLE_ANON) - @router('post /api/wallet/verify_token') - async verifyToken(req, res) { - const { wallet_token } = req.params - return {} - } } diff --git a/src/models/ActivityChest.ts b/src/models/ActivityChest.ts new file mode 100644 index 0000000..408e8cf --- /dev/null +++ b/src/models/ActivityChest.ts @@ -0,0 +1,74 @@ +import { dbconn } from 'decorators/dbconn' +import { getModelForClass, index, modelOptions, mongoose, pre, prop } from '@typegoose/typegoose' +import { Severity } from '@typegoose/typegoose/lib/internal/constants' +import { BaseModule } from './Base' +import { convert } from 'zutils/utils/number.util' +const alphabet = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI' + +/** + * 活动宝箱 + */ +@dbconn() +@index({ user: 1, activity: 1 }, { unique: false }) +@index({ shareCode: 1, activity: 1 }, { unique: true }) +@modelOptions({ + schemaOptions: { collection: 'activity_box', timestamps: true }, + options: { allowMixed: Severity.ALLOW }, +}) +@pre('save', async function () { + if (!this.shareCode) { + // 取ObjectId的time和inc段, + // 将time段倒序(倒序后, 如果以0开始, 则移除0, 随机拼接一个hex字符), 然后拼接inc段, 再转换成52进制 + let timeStr = this.id.slice(0, 8).split('').reverse().join('') + if (timeStr.indexOf('0') === 0) { + let randomStr = convert({ numStr: ((Math.random() * 51) | (0 + 1)) + '', base: 10, to: 52 }) + timeStr = randomStr + timeStr.slice(1) + } + let shortId = timeStr + this.id.slice(-6) + this.shareCode = convert({ numStr: shortId, base: 16, to: 52, alphabet }) + } +}) +class ActivityChestClass extends BaseModule { + // 盒子的分享码 + @prop() + public shareCode: string + // 0 锁定, 1 未开启 9 已开启 + @prop({ default: 0 }) + public status: number + @prop() + public user: string + @prop() + public activity: string + // 品级 + @prop({ default: 0 }) + public level: number + // 最大助力次数 + @prop() + public maxBonus: number + // 基础积分 + @prop({ default: 0 }) + public scoreInit: number + // 助力积分 + @prop({ default: 0 }) + public scoreBonus: number + + @prop({ type: () => [String], default: [] }) + public bonusUsers: string[] + @prop({ type: () => [Number], default: [] }) + public bonusScores: number[] + + public toJson() { + return { + // @ts-ignore + id: this._id, + stat: this.status, + shareCode: this.shareCode, + level: this.level, + maxBonus: this.maxBonus, + scoreInit: this.scoreInit, + scoreBonus: this.scoreBonus, + bounsCount: this.bonusUsers.length, + } + } +} +export const ActivityChest = getModelForClass(ActivityChestClass, { existingConnection: ActivityChestClass['db'] }) diff --git a/src/models/ActivityGame.ts b/src/models/ActivityGame.ts new file mode 100644 index 0000000..433a9c0 --- /dev/null +++ b/src/models/ActivityGame.ts @@ -0,0 +1,38 @@ +import { dbconn } from 'decorators/dbconn' +import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' +import { Severity } from '@typegoose/typegoose/lib/internal/constants' +import { BaseModule } from './Base' + +/** + * 活动游戏数据 + */ +@dbconn() +@index({ user: 1, activity: 1 }, { unique: false }) +@modelOptions({ + schemaOptions: { collection: 'activity_game', timestamps: true }, + options: { allowMixed: Severity.ALLOW }, +}) +class ActivityGameClass extends BaseModule { + @prop() + public user: string + @prop() + public activity: string + // 0 未开启 + @prop({ default: 0 }) + public status: number + // 拥有的ticket数量 + @prop() + public tickets: number + // 最后一次签到日期 + @prop() + public lastSignDay: string + + public toJson() { + return { + // @ts-ignore + id: this._id, + stat: this.status, + } + } +} +export const ActivityGame = getModelForClass(ActivityGameClass, { existingConnection: ActivityGameClass['db'] }) diff --git a/src/models/ScoreRecord.ts b/src/models/ScoreRecord.ts index 11aa67d..1ce6029 100644 --- a/src/models/ScoreRecord.ts +++ b/src/models/ScoreRecord.ts @@ -25,6 +25,17 @@ class ScoreRecordClass extends BaseModule { @prop({ type: mongoose.Schema.Types.Mixed }) public data: any + + public toJson() { + return { + user: this.user, + activity: this.activity, + score: this.score, + type: this.type, + //@ts-ignore + time: this.createdAt.getTime(), + } + } } export const ScoreRecord = getModelForClass(ScoreRecordClass, { existingConnection: ScoreRecordClass['db'] }) diff --git a/src/models/TicketRecord.ts b/src/models/TicketRecord.ts new file mode 100644 index 0000000..bac0552 --- /dev/null +++ b/src/models/TicketRecord.ts @@ -0,0 +1,47 @@ +import { Severity, getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from './Base' + +export const DAILY_SIGN = 'daily_sign' +// 累计签到奖励 +export const SIGN_TOTAL = 'sign_total' +// 使用门票 +export const USE_TICKET = 'use_ticket' + +@dbconn() +@index({ user: 1 }, { unique: false }) +@index({ activity: 1 }, { unique: false }) +@index({ user: 1, activity: 1, type: 1 }, { unique: false }) +@modelOptions({ + schemaOptions: { collection: 'ticket_record', timestamps: true }, + options: { allowMixed: Severity.ALLOW }, +}) +class TicketRecordClass extends BaseModule { + @prop({ required: true }) + public user: string + + @prop({ required: true }) + public activity: string + + @prop() + public score: number + + @prop() + public type: string + + @prop({ type: mongoose.Schema.Types.Mixed }) + public data: any + + public toJson() { + return { + user: this.user, + activity: this.activity, + score: this.score, + type: this.type, + //@ts-ignore + time: this.createdAt.getTime(), + } + } +} + +export const TicketRecord = getModelForClass(TicketRecordClass, { existingConnection: TicketRecordClass['db'] }) diff --git a/src/models/chain/CheckIn.ts b/src/models/chain/CheckIn.ts index 55bed47..3d67a7e 100644 --- a/src/models/chain/CheckIn.ts +++ b/src/models/chain/CheckIn.ts @@ -26,8 +26,11 @@ export class CheckInClass extends BaseModule { @prop() public dateTag: string // 连签天数 - @prop({ default: 0 }) + @prop({ default: 1 }) public count: number + // 累计签到天数 + @prop({ default: 1 }) + public total: number @prop() public value: string @prop() @@ -39,6 +42,8 @@ export class CheckInClass extends BaseModule { if (preDayEvent) { event.count = preDayEvent.count + 1 } + const total = await CheckIn.countDocuments({ from: event.from }) + event.total = total + 1 return CheckIn.insertOrUpdate({ hash: event.hash }, event) } diff --git a/src/services/chain.svr.ts b/src/services/chain.svr.ts index e146656..596422f 100644 --- a/src/services/chain.svr.ts +++ b/src/services/chain.svr.ts @@ -1,41 +1,58 @@ +import { CheckIn } from 'models/chain/CheckIn' import { NftHolder } from 'models/chain/NftHolder' import { NftStake } from 'models/chain/NftStake' +import { getMonthBegin, getNDayAgo } from 'zutils/utils/date.util' export const queryCheckInList = async (address: string, days: string | number | string[], limit: number = 0) => { - const url = process.env.CHAIN_SVR + '/task/check_in' - return fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - address, - days, - limit, - }), - }).then(res => res.json()) + let query: any = { from: address.toLowerCase() } + if (!limit) { + if (typeof days === 'number') { + let begin = getNDayAgo(days, true) + query.blockTime = { $gt: (begin.getTime() / 1000) | 0 } + } else if (typeof days === 'string') { + if (days === '1month') { + let date = getMonthBegin(new Date()) + query.blockTime = { $gt: (date.getTime() / 1000) | 0 } + } else { + query.dateTag = days + } + } else if (Array.isArray(days)) { + query.dateTag = { $in: days } + } + } + let records + if (limit) { + records = await CheckIn.find(query).sort({ _id: -1 }).limit(limit) + } else { + records = await CheckIn.find(query).sort({ _id: -1 }) + } + let result = [] + for (let record of records) { + result.push(record.toJson()) + } + return result } export const queryCheckInSeq = async (address: string) => { - const url = process.env.CHAIN_SVR + '/task/check_in/max_seq' - return fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - address, - }), - }).then(res => res.json()) + const record = await CheckIn.findOne({ from: address.toLowerCase() }).sort({ count: -1 }) + return record.toJson() } export const queryBurnNftList = async (address: string, user: string, chain: number) => { - const url = process.env.CHAIN_SVR + '/task/nft/checkburn' - return fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - address, - user, - chain, - }), - }).then(res => res.json()) + address = address.toLowerCase() + user = user.toLowerCase() + + let records = await NftHolder.find({ address, chain, user, burn: true }).sort({ blockNumber: -1 }) + let result = [] + for (let record of records) { + result.push({ + address: record.address, + chain: record.chain, + user: record.user, + tokenId: record.tokenId, + }) + } + return result } export const checkHadGacha = async (user: string) => { diff --git a/src/services/rank.svr.ts b/src/services/rank.svr.ts index 4340c0f..dc57862 100644 --- a/src/services/rank.svr.ts +++ b/src/services/rank.svr.ts @@ -1,7 +1,7 @@ import { ScoreRecord } from 'models/ScoreRecord' import { ZRedisClient } from 'zutils' import { formatDate } from 'zutils/utils/date.util' - +const rankLevels = require('../../configs/uaw_rank_level.json') /** * 更新排行榜 * @param param0 @@ -63,3 +63,8 @@ export const rankKey = (activity: string, date?: Date) => { const dateTag = formatDate(date) return `${activity}:score:${dateTag}` } + +export const rankLevel = (score: number) => { + const data = rankLevels.find(o => score >= o.score) + return data.level +} diff --git a/src/services/sign.svr.ts b/src/services/sign.svr.ts new file mode 100644 index 0000000..4593b3b --- /dev/null +++ b/src/services/sign.svr.ts @@ -0,0 +1,28 @@ +import { CheckIn } from 'models/chain/CheckIn' + +const signCfg = require('../../configs/uaw_daily_sign.json') +export const totalSignCfg = signCfg.total_sign.sort((a, b) => a.days - b.days) + +// 按days倒序排列 +const totalSignCfg2 = signCfg.total_sign.sort((a, b) => b.days - a.days) +export const seqSignCfg = signCfg.sequential_sign.sort((a, b) => a.seq - b.seq) +let total = 0 +let seqSignCfg2 = [] +for (let i = 0, l = seqSignCfg.length; i < l; i++) { + total += seqSignCfg[i].reward + seqSignCfg2[l - i - 1] = { days: seqSignCfg[i].days, reward: total } +} + +export const totalSignScore = (days: number) => { + const data = totalSignCfg2.find(o => days >= o.days) + return data?.reward || 0 +} + +export const seqSignScore = (days: number) => { + const data = seqSignCfg2.find(o => days >= o.days) + return data?.reward || 0 +} + +export const checkInToday = async (address: string, dateTag: string) => { + return CheckIn.findOne({ from: address.toLowerCase(), dateTag }) +} diff --git a/src/tasks/BurnNft.ts b/src/tasks/BurnNft.ts index 520e14b..c6d2931 100644 --- a/src/tasks/BurnNft.ts +++ b/src/tasks/BurnNft.ts @@ -13,11 +13,7 @@ export default class BurnNft extends ITask { 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.user.address, chain) - if (res.errcode) { - throw new ZError(res.errcode, res.errmsg) - } - const nftList = res.data + const nftList = await queryBurnNftList(address, this.user.address, chain) const localNft = await NftBurnRecord.find({ user: this.user.id, chain, diff --git a/src/tasks/DailyCheckIn.ts b/src/tasks/DailyCheckIn.ts index e3bab49..0c93380 100644 --- a/src/tasks/DailyCheckIn.ts +++ b/src/tasks/DailyCheckIn.ts @@ -26,17 +26,16 @@ export default class DailyCheckIn extends ITask { cfg.type === TaskTypeEnum.DAILY ? await queryCheckInList(address, days - 1, limit) : await queryCheckInSeq(address) - if (res.errcode) { - throw new ZError(res.errcode, res.errmsg) - } 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) + // @ts-ignore + (cfg.type === TaskTypeEnum.DAILY && task.status === TaskStatusEnum.RUNNING && res.length >= days) || + // @ts-ignore + (cfg.type === TaskTypeEnum.ONCE && task.status === TaskStatusEnum.RUNNING && res.count >= days) ) { task.status = TaskStatusEnum.SUCCESS task.timeFinish = Date.now() - task.data = res.data + task.data = res try { await this.user.save() } catch (err) { @@ -60,7 +59,7 @@ export default class DailyCheckIn extends ITask { if (cfg.type === TaskTypeEnum.DAILY) { 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 + let list: { day: string; time: number; count: number }[] = res const countCfg = cfg.params.score.length let count = list.length > 0 ? list[0].count : 0