增加宝箱激活码相关接口

This commit is contained in:
CounterFire2023 2024-04-17 11:15:59 +08:00
parent db8a16b954
commit 17961ce320
11 changed files with 242 additions and 18 deletions

View File

@ -52,6 +52,9 @@
2. 增加接口: 合作伙伴NFT列表
3. 增加接口: 领取NFT holder奖励
#### 20240416
1. 增加接口: 兑换宝箱激活码(30)
### 1. 钱包预登录
#### Request
@ -873,3 +876,71 @@ body:
]
}
```
### 30.\* 兑换宝箱激活码
#### Request
- URL`/api/voucher/claim`
- 方法POST
- 头部:
- Authorization: Bearer JWT_token
body:
```js
{
"id": "兑换码"
}
```
#### Response
```js
{
chests: [
{ // 结构同 18.宝箱列表
id: 1, // 箱子id
stat: 0, // 0: 锁定, 1: 正常
shareCode: '箱子的分享码',
level: 1, // 箱子品级
maxBonus: 10, // 最大可助力数量
scoreInit: 5, // 初始可获得积分
scoreBonus: 10, // 助力增加的分数
bonusCount: 2, // 已助力次数
}
]
}
```
### 31.\* 生成测试用宝箱激活码
> 只在测试环境有效
>
#### Request
- URL`/api/voucher/generate`
- 方法POST
- 头部:
- Authorization: Bearer JWT_token
body:
```js
{
"num": 10, //要生成的激活码数量
}
```
#### Response
```js
['1234567890ab'] // 激活码列表
```

View File

@ -66,9 +66,9 @@
}, {
"id": "e2fuah0j30vwcpe0my7",
"task": "TwitterRetweet",
"title": "Repost on X",
"title": "Retweet on X",
"type": 1,
"desc": "Show your friends Counter Fire.",
"desc": "Retweet specific tweets",
"category": "Social Tasks",
"score": 50,
"autoclaim": false,
@ -81,9 +81,9 @@
}, {
"id": "e2fuah0j30vwcpe0my9",
"task": "TwitterLike",
"title": "Like on X",
"title": "Like the tweets on X",
"type": 1,
"desc": "Show your friends Counter Fire.",
"desc": "Like specific tweets",
"category": "Social Tasks",
"score": 50,
"autoclaim": false,

View File

@ -30,6 +30,7 @@
"fastify-xml-body-parser": "^2.2.0",
"mongodb-extended-json": "^1.11.1",
"mongoose": "8.2.3",
"nanoid": "^5.0.7",
"node-schedule": "^2.0.0",
"siwe": "^2.1.4",
"tracer": "^1.1.6",

View File

@ -63,3 +63,5 @@ export const SCORE_ENHANCE_CHEST_GIFT = 'enhance_chest_gift'
export const RECAPTCHA_MIN_SCORE = 0.5
// 排行榜分数缩放系数
export const RANK_SCORE_SCALE = 100
export const BASE52_ALPHABET = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI'

View File

@ -8,6 +8,10 @@ export const isValidShareCode = (str: string) => {
return /^[3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI]{10}$/.test(str)
}
export const isValidVoucherCode = (str: string) => {
return /^[3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI]{12}$/.test(str)
}
export const formatNumShow = (num: number) => {
if (num >= 10) {
return Math.round(num) + ''

View File

@ -78,6 +78,7 @@ export default class TasksController extends BaseController {
@router('post /api/tasks/begin_task')
async beginTask(req) {
new SyncLocker().checkLock(req)
logger.db('begin_task', req)
let user = req.user
let activity = req.activity
let { task } = req.params
@ -136,6 +137,7 @@ export default class TasksController extends BaseController {
@router('post /api/tasks/check_task')
async checkTask(req) {
const user = req.user
logger.db('chect_task', req)
const activity = req.activity
const { task } = req.params
const [taskId, dateTag] = task.split(':')

View File

@ -1,26 +1,83 @@
import { isValidShareCode } from 'common/Utils'
import { isValidShareCode, isValidVoucherCode } from 'common/Utils'
import logger from 'logger/logger'
import { ChestStatusEnum } from 'models/ActivityChest'
import { NFTHolderRecord } from 'models/NFTHodlerRecord'
import { queryNftBalance } from 'services/chain.svr'
import { generateChestLevel, generateNewChest } from 'services/game.svr'
import { checkDiscordRole } from 'services/oauth.svr'
import { SourceEnum, VoucherRecord, VoucherStatusEnum } from 'models/VoucherRecord'
import { generateNewChest } from 'services/game.svr'
import { SyncLocker, BaseController, router, role, ROLE_ANON, ZError } from 'zutils'
import { customAlphabet } from 'nanoid'
import { BASE52_ALPHABET } from 'common/Constants'
/**
*
*/
class VoucherController extends BaseController {
@router('post /api/voucher/generate')
async generateTestVoucher(req) {
logger.db('generate_test_voucher', req)
if (process.env.NODE_ENV !== 'development') {
throw new ZError(10, 'only support in development')
}
const user = req.user
let { num } = req.params
if (!num || num <= 1) {
num = 1
}
let results = []
const nanoid = customAlphabet(BASE52_ALPHABET, 8)
for (let i = 0; i < num; i++) {
let code = 'test' + nanoid()
let voucher = new VoucherRecord({
batch: 'test',
code,
scoreInit: 0,
chestLevel: 1,
chestNum: 1,
deliverTime: Date.now(),
activity: user.activity,
status: VoucherStatusEnum.NORMAL,
source: SourceEnum.TEST,
creator: user.id,
})
await voucher.save()
results.push(voucher.id)
}
return results
}
@router('post /api/voucher/claim')
async claimVoucherReward(req) {
new SyncLocker().checkLock(req)
logger.db('claim_voucher', req)
const user = req.user
const { id } = req.params
if (!id || !isValidShareCode(id)) {
throw new ZError(10, 'invild voucher id')
if (!id || !isValidVoucherCode(id)) {
throw new ZError(10, 'invild voucher code')
}
const record = await VoucherRecord.findOne({ code: id, status: VoucherStatusEnum.NORMAL })
if (!record) {
throw new ZError(11, 'voucher not found')
}
record.status = VoucherStatusEnum.USED
record.user = user.id
if (record.bonusScores.length > 0) {
record.score = record.scoreInit + record.bonusScores[Math.floor(Math.random() * record.bonusScores.length)]
} else {
record.score = record.scoreInit
}
let chestList: any = []
if (record.chestNum > 0) {
for (let i = 0; i < record.chestNum; i++) {
const chest = generateNewChest(user.id, user.activity, record.chestLevel, ChestStatusEnum.NORMAL)
await chest.save()
record.chests.push(chest.id)
chestList.push(chest.toJson())
}
}
record.userTime = Date.now()
await record.save()
return {}
return {
score: record.score,
chests: chestList,
}
}
}

View File

@ -3,7 +3,7 @@ import { getModelForClass, index, modelOptions, mongoose, pre, prop } from '@typ
import { Severity } from '@typegoose/typegoose/lib/internal/constants'
import { BaseModule } from './Base'
import { convert } from 'zutils/utils/number.util'
const alphabet = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI'
import { BASE52_ALPHABET } from 'common/Constants'
export enum ChestStatusEnum {
LOCKED = 0,
@ -32,7 +32,7 @@ export enum ChestStatusEnum {
}
let shortId = timeStr + this.id.slice(-6)
console.log(shortId)
this.shareCode = convert({ numStr: shortId, base: 16, to: 52, alphabet })
this.shareCode = convert({ numStr: shortId, base: 16, to: 52, alphabet: BASE52_ALPHABET })
console.log(this.id, this.shareCode)
}
})

View File

@ -12,8 +12,7 @@ import { dbconn } from 'decorators/dbconn'
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
import { BaseModule } from './Base'
import { convert } from 'zutils/utils/number.util'
const alphabet = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI'
import { BASE52_ALPHABET } from 'common/Constants'
export enum TaskStatusEnum {
NOT_START = 0,
@ -65,7 +64,7 @@ export interface ActivityUserClass extends Base, TimeStamps {}
timeStr = randomStr + timeStr.slice(1)
}
let shortId = timeStr + this.id.slice(-6)
this.inviteCode = convert({ numStr: shortId, base: 16, to: 52, alphabet })
this.inviteCode = convert({ numStr: shortId, base: 16, to: 52, alphabet: BASE52_ALPHABET })
}
})
export class ActivityUserClass extends BaseModule {

View File

@ -0,0 +1,83 @@
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'
export enum VoucherStatusEnum {
LOCKED = 0,
INITED = 1,
NORMAL = 2,
USED = 9,
}
export enum SourceEnum {
TEST = 'test',
ADMIN = 'admin',
}
/**
*
*/
@dbconn()
@index({ user: 1, activity: 1 }, { unique: false })
@index({ code: 1, activity: 1 }, { unique: true })
@modelOptions({
schemaOptions: { collection: 'activity_box', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
export class VoucherRecordClass extends BaseModule {
@prop()
public batch: number
@prop()
public code: string
// 0 锁定, 1 未发放 2 已发放,待使用 9 已兑换
@prop({ enum: VoucherStatusEnum, default: VoucherStatusEnum.INITED })
public status: VoucherStatusEnum
@prop()
public user: string
@prop()
public activity: string
// 基础积分
@prop({ default: 0 })
public scoreInit: number
// 浮动积分
@prop({ type: () => [Number], default: [] })
public bonusScores: number[]
// 可获得的宝箱等级
@prop({ default: 1 })
public chestLevel: number
// 可获得的宝箱数量
@prop({ default: 1 })
public chestNum: number
// 最终得到的积分
@prop({ default: 0 })
public score: number
// 最终得到的宝箱列表
@prop({ type: () => [String], default: [] })
public chests: string[]
@prop()
public comment: string
@prop()
public creator: string
@prop({ enum: SourceEnum, default: SourceEnum.ADMIN })
public source: SourceEnum
// 发放时间
@prop()
public deliverTime: number
// 使用时间
@prop()
public userTime: number
public toJson() {
return {
// @ts-ignore
id: this.id,
stat: this.status,
code: this.code,
score: this.score,
chests: this.chests,
}
}
}
export const VoucherRecord = getModelForClass(VoucherRecordClass, { existingConnection: VoucherRecordClass['db'] })

View File

@ -3317,6 +3317,11 @@ nano-json-stream-parser@^0.1.2:
resolved "https://registry.npmjs.org/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz"
integrity sha512-9MqxMH/BSJC7dnLsEMPyfN5Dvoo49IsPFYMcHw3Bcfc2kN0lpHRBSzlMSVx4HGyJ7s9B31CyBTVehWJoQ8Ctew==
nanoid@^5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.7.tgz#6452e8c5a816861fd9d2b898399f7e5fd6944cc6"
integrity sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"