增加宝箱激活码相关接口
This commit is contained in:
parent
db8a16b954
commit
17961ce320
71
docs/uaw.md
71
docs/uaw.md
@ -52,6 +52,9 @@
|
|||||||
2. 增加接口: 合作伙伴NFT列表
|
2. 增加接口: 合作伙伴NFT列表
|
||||||
3. 增加接口: 领取NFT holder奖励
|
3. 增加接口: 领取NFT holder奖励
|
||||||
|
|
||||||
|
#### 20240416
|
||||||
|
1. 增加接口: 兑换宝箱激活码(30)
|
||||||
|
|
||||||
### 1. 钱包预登录
|
### 1. 钱包预登录
|
||||||
|
|
||||||
#### Request
|
#### 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'] // 激活码列表
|
||||||
|
```
|
@ -66,9 +66,9 @@
|
|||||||
}, {
|
}, {
|
||||||
"id": "e2fuah0j30vwcpe0my7",
|
"id": "e2fuah0j30vwcpe0my7",
|
||||||
"task": "TwitterRetweet",
|
"task": "TwitterRetweet",
|
||||||
"title": "Repost on X",
|
"title": "Retweet on X",
|
||||||
"type": 1,
|
"type": 1,
|
||||||
"desc": "Show your friends Counter Fire.",
|
"desc": "Retweet specific tweets",
|
||||||
"category": "Social Tasks",
|
"category": "Social Tasks",
|
||||||
"score": 50,
|
"score": 50,
|
||||||
"autoclaim": false,
|
"autoclaim": false,
|
||||||
@ -81,9 +81,9 @@
|
|||||||
}, {
|
}, {
|
||||||
"id": "e2fuah0j30vwcpe0my9",
|
"id": "e2fuah0j30vwcpe0my9",
|
||||||
"task": "TwitterLike",
|
"task": "TwitterLike",
|
||||||
"title": "Like on X",
|
"title": "Like the tweets on X",
|
||||||
"type": 1,
|
"type": 1,
|
||||||
"desc": "Show your friends Counter Fire.",
|
"desc": "Like specific tweets",
|
||||||
"category": "Social Tasks",
|
"category": "Social Tasks",
|
||||||
"score": 50,
|
"score": 50,
|
||||||
"autoclaim": false,
|
"autoclaim": false,
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"fastify-xml-body-parser": "^2.2.0",
|
"fastify-xml-body-parser": "^2.2.0",
|
||||||
"mongodb-extended-json": "^1.11.1",
|
"mongodb-extended-json": "^1.11.1",
|
||||||
"mongoose": "8.2.3",
|
"mongoose": "8.2.3",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
"node-schedule": "^2.0.0",
|
"node-schedule": "^2.0.0",
|
||||||
"siwe": "^2.1.4",
|
"siwe": "^2.1.4",
|
||||||
"tracer": "^1.1.6",
|
"tracer": "^1.1.6",
|
||||||
|
@ -63,3 +63,5 @@ export const SCORE_ENHANCE_CHEST_GIFT = 'enhance_chest_gift'
|
|||||||
export const RECAPTCHA_MIN_SCORE = 0.5
|
export const RECAPTCHA_MIN_SCORE = 0.5
|
||||||
// 排行榜分数缩放系数
|
// 排行榜分数缩放系数
|
||||||
export const RANK_SCORE_SCALE = 100
|
export const RANK_SCORE_SCALE = 100
|
||||||
|
|
||||||
|
export const BASE52_ALPHABET = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI'
|
||||||
|
@ -8,6 +8,10 @@ export const isValidShareCode = (str: string) => {
|
|||||||
return /^[3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI]{10}$/.test(str)
|
return /^[3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI]{10}$/.test(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isValidVoucherCode = (str: string) => {
|
||||||
|
return /^[3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI]{12}$/.test(str)
|
||||||
|
}
|
||||||
|
|
||||||
export const formatNumShow = (num: number) => {
|
export const formatNumShow = (num: number) => {
|
||||||
if (num >= 10) {
|
if (num >= 10) {
|
||||||
return Math.round(num) + ''
|
return Math.round(num) + ''
|
||||||
|
@ -78,6 +78,7 @@ export default class TasksController extends BaseController {
|
|||||||
@router('post /api/tasks/begin_task')
|
@router('post /api/tasks/begin_task')
|
||||||
async beginTask(req) {
|
async beginTask(req) {
|
||||||
new SyncLocker().checkLock(req)
|
new SyncLocker().checkLock(req)
|
||||||
|
logger.db('begin_task', req)
|
||||||
let user = req.user
|
let user = req.user
|
||||||
let activity = req.activity
|
let activity = req.activity
|
||||||
let { task } = req.params
|
let { task } = req.params
|
||||||
@ -136,6 +137,7 @@ export default class TasksController extends BaseController {
|
|||||||
@router('post /api/tasks/check_task')
|
@router('post /api/tasks/check_task')
|
||||||
async checkTask(req) {
|
async checkTask(req) {
|
||||||
const user = req.user
|
const user = req.user
|
||||||
|
logger.db('chect_task', req)
|
||||||
const activity = req.activity
|
const activity = req.activity
|
||||||
const { task } = req.params
|
const { task } = req.params
|
||||||
const [taskId, dateTag] = task.split(':')
|
const [taskId, dateTag] = task.split(':')
|
||||||
|
@ -1,26 +1,83 @@
|
|||||||
import { isValidShareCode } from 'common/Utils'
|
import { isValidShareCode, isValidVoucherCode } from 'common/Utils'
|
||||||
import logger from 'logger/logger'
|
import logger from 'logger/logger'
|
||||||
import { ChestStatusEnum } from 'models/ActivityChest'
|
import { ChestStatusEnum } from 'models/ActivityChest'
|
||||||
import { NFTHolderRecord } from 'models/NFTHodlerRecord'
|
import { SourceEnum, VoucherRecord, VoucherStatusEnum } from 'models/VoucherRecord'
|
||||||
import { queryNftBalance } from 'services/chain.svr'
|
import { generateNewChest } from 'services/game.svr'
|
||||||
import { generateChestLevel, generateNewChest } from 'services/game.svr'
|
|
||||||
import { checkDiscordRole } from 'services/oauth.svr'
|
|
||||||
import { SyncLocker, BaseController, router, role, ROLE_ANON, ZError } from 'zutils'
|
import { SyncLocker, BaseController, router, role, ROLE_ANON, ZError } from 'zutils'
|
||||||
|
import { customAlphabet } from 'nanoid'
|
||||||
|
import { BASE52_ALPHABET } from 'common/Constants'
|
||||||
/**
|
/**
|
||||||
* 礼品券相关接口
|
* 礼品券相关接口
|
||||||
*/
|
*/
|
||||||
class VoucherController extends BaseController {
|
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')
|
@router('post /api/voucher/claim')
|
||||||
async claimVoucherReward(req) {
|
async claimVoucherReward(req) {
|
||||||
new SyncLocker().checkLock(req)
|
new SyncLocker().checkLock(req)
|
||||||
logger.db('claim_voucher', req)
|
logger.db('claim_voucher', req)
|
||||||
const user = req.user
|
const user = req.user
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
if (!id || !isValidShareCode(id)) {
|
if (!id || !isValidVoucherCode(id)) {
|
||||||
throw new ZError(10, 'invild voucher 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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { getModelForClass, index, modelOptions, mongoose, pre, prop } from '@typ
|
|||||||
import { Severity } from '@typegoose/typegoose/lib/internal/constants'
|
import { Severity } from '@typegoose/typegoose/lib/internal/constants'
|
||||||
import { BaseModule } from './Base'
|
import { BaseModule } from './Base'
|
||||||
import { convert } from 'zutils/utils/number.util'
|
import { convert } from 'zutils/utils/number.util'
|
||||||
const alphabet = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI'
|
import { BASE52_ALPHABET } from 'common/Constants'
|
||||||
|
|
||||||
export enum ChestStatusEnum {
|
export enum ChestStatusEnum {
|
||||||
LOCKED = 0,
|
LOCKED = 0,
|
||||||
@ -32,7 +32,7 @@ export enum ChestStatusEnum {
|
|||||||
}
|
}
|
||||||
let shortId = timeStr + this.id.slice(-6)
|
let shortId = timeStr + this.id.slice(-6)
|
||||||
console.log(shortId)
|
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)
|
console.log(this.id, this.shareCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -12,8 +12,7 @@ import { dbconn } from 'decorators/dbconn'
|
|||||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
|
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
|
||||||
import { BaseModule } from './Base'
|
import { BaseModule } from './Base'
|
||||||
import { convert } from 'zutils/utils/number.util'
|
import { convert } from 'zutils/utils/number.util'
|
||||||
|
import { BASE52_ALPHABET } from 'common/Constants'
|
||||||
const alphabet = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI'
|
|
||||||
|
|
||||||
export enum TaskStatusEnum {
|
export enum TaskStatusEnum {
|
||||||
NOT_START = 0,
|
NOT_START = 0,
|
||||||
@ -65,7 +64,7 @@ export interface ActivityUserClass extends Base, TimeStamps {}
|
|||||||
timeStr = randomStr + timeStr.slice(1)
|
timeStr = randomStr + timeStr.slice(1)
|
||||||
}
|
}
|
||||||
let shortId = timeStr + this.id.slice(-6)
|
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 {
|
export class ActivityUserClass extends BaseModule {
|
||||||
|
83
src/models/VoucherRecord.ts
Normal file
83
src/models/VoucherRecord.ts
Normal 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'] })
|
@ -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"
|
resolved "https://registry.npmjs.org/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz"
|
||||||
integrity sha512-9MqxMH/BSJC7dnLsEMPyfN5Dvoo49IsPFYMcHw3Bcfc2kN0lpHRBSzlMSVx4HGyJ7s9B31CyBTVehWJoQ8Ctew==
|
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:
|
natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user