diff --git a/docs/uaw.md b/docs/uaw.md index d55dc3d..b1f7293 100644 --- a/docs/uaw.md +++ b/docs/uaw.md @@ -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'] // 激活码列表 +``` \ No newline at end of file diff --git a/initdatas/activity_info.json b/initdatas/activity_info.json index a7f3a10..f5d2336 100644 --- a/initdatas/activity_info.json +++ b/initdatas/activity_info.json @@ -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, diff --git a/package.json b/package.json index 3eac1e9..19902a5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 090d1d5..8d27177 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -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' diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 5c69ca1..907f137 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -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) + '' diff --git a/src/controllers/tasks.controller.ts b/src/controllers/tasks.controller.ts index f132dd5..0c38180 100644 --- a/src/controllers/tasks.controller.ts +++ b/src/controllers/tasks.controller.ts @@ -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(':') diff --git a/src/controllers/voucher.controller.ts b/src/controllers/voucher.controller.ts index d94a81c..4cf12c4 100644 --- a/src/controllers/voucher.controller.ts +++ b/src/controllers/voucher.controller.ts @@ -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, + } } } diff --git a/src/models/ActivityChest.ts b/src/models/ActivityChest.ts index 4608b3b..f51d1ae 100644 --- a/src/models/ActivityChest.ts +++ b/src/models/ActivityChest.ts @@ -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) } }) diff --git a/src/models/ActivityUser.ts b/src/models/ActivityUser.ts index 9c17f0d..eddbe5b 100644 --- a/src/models/ActivityUser.ts +++ b/src/models/ActivityUser.ts @@ -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 { diff --git a/src/models/VoucherRecord.ts b/src/models/VoucherRecord.ts new file mode 100644 index 0000000..5d93d3b --- /dev/null +++ b/src/models/VoucherRecord.ts @@ -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'] }) diff --git a/yarn.lock b/yarn.lock index 0de7e7c..b4f3f51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"