增加uaw相关接口

This commit is contained in:
CounterFire2023 2024-03-22 19:51:51 +08:00
parent 010ada3bd4
commit 6ff402e670
22 changed files with 1571 additions and 98 deletions

View File

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

View File

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

629
docs/uaw.md Normal file
View File

@ -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 // 获得的探索次数
}
```

View File

@ -281,7 +281,7 @@
"score": 200,
"params": {"address": "0xe2E4D5a4045fBFcbCBECAf5b8A94303712d2FA97"}
}],
"startTime": 1702628292366,
"endTime": 1705220292366
"startTime": 1711086450119,
"endTime": 1713678477701
}
]

179
initdatas/uaw_cfg.json Normal file
View File

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

@ -1 +1 @@
Subproject commit e207a95fd79c0e926668074e68ca2467952d1ded
Subproject commit b97e33472f46eb8fb47a8cf3c3924c5d26af5eca

View File

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

View File

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

View File

@ -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(),
}
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

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

28
src/services/sign.svr.ts Normal file
View File

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

View File

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

View File

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