209 lines
6.5 KiB
TypeScript
209 lines
6.5 KiB
TypeScript
import { ZError, SyncLocker, ZRedisClient, BaseController, ROLE_ANON, role, router } from 'zutils'
|
|
import { ActivityChest, ChestStatusEnum } 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'
|
|
import { ChestRecord } from 'models/chain/ChestRecord'
|
|
const chestCfg = require('../../configs/chest.json')
|
|
const chestLevelMap = new Map()
|
|
|
|
for (let cfg of chestCfg.chests) {
|
|
chestLevelMap.set(cfg.level, cfg)
|
|
}
|
|
|
|
const generateNewChest = (uid: string, activity: string, level = 1, status = ChestStatusEnum.LOCKED) => {
|
|
let cfg = chestLevelMap.get(level)
|
|
if (!cfg) {
|
|
throw new ZError(11, 'chest cfg not found')
|
|
}
|
|
let scoreInit = Math.floor(Math.random() * (cfg.scoreMax - cfg.scoreMin + 1) + cfg.scoreMin)
|
|
let chest = new ActivityChest({
|
|
user: uid,
|
|
activity: activity,
|
|
level: level,
|
|
maxBounsCount: cfg.maxBounsCount,
|
|
bounsCfg: cfg.bounsCfg,
|
|
scoreInit,
|
|
status,
|
|
})
|
|
return chest
|
|
}
|
|
|
|
/**
|
|
* 宝箱相关接口
|
|
*/
|
|
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 session = await mongoose.startSession()
|
|
session.startTransaction()
|
|
try {
|
|
// TODO:: 待规则确定后, 检查用户是否符合助力条件
|
|
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.maxBounsCount) {
|
|
throw new ZError(12, 'enhanced times exceed')
|
|
}
|
|
if (chest.status === ChestStatusEnum.OPENED) {
|
|
throw new ZError(13, 'chest already opened')
|
|
}
|
|
if (chest.status === ChestStatusEnum.LOCKED) {
|
|
throw new ZError(14, 'chest is locked')
|
|
}
|
|
if (chest.user === uid) {
|
|
throw new ZError(15, 'can not enhance self')
|
|
}
|
|
const score = Math.floor(Math.random() * (chest.bounsCfg[1] - chest.bounsCfg[0] + 1) + chest.bounsCfg[0])
|
|
chest.bonusUsers.push(uid)
|
|
chest.bonusScores.push(score)
|
|
chest.scoreBonus += score
|
|
await chest.save({ session })
|
|
const chestsForUser = await ActivityChest.find({ user: uid, activity: user.activity }).session(session)
|
|
// 如果用户没有宝箱, 则说明用户是新用户, 生成一个宝箱
|
|
if (chestsForUser.length === 0) {
|
|
const newChest = generateNewChest(uid, user.activity, 1, ChestStatusEnum.LOCKED)
|
|
await newChest.save({ session })
|
|
}
|
|
await session.commitTransaction()
|
|
session.endSession()
|
|
return {
|
|
score: 1,
|
|
}
|
|
} catch (error) {
|
|
session.abortTransaction()
|
|
session.endSession()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 开启宝箱
|
|
* 流程如下:
|
|
* 1. 客户端组装json string: data:,{"p":"cf-20","op":"chest_open","id":"${chestId}"}
|
|
* 2. 将字符串转成hex
|
|
* 3. 将hex字符串作为data, 发起一笔至特定地址的0ETH交易
|
|
* 4. 服务端监听到交易后, 解析input, 获取chestId
|
|
* 5. 服务端调用此接口, 传入chestId, 完成开箱操作
|
|
*/
|
|
@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 openRecord = await ChestRecord.findOne({ from: user.address, chestId: chestid })
|
|
if (!openRecord) {
|
|
throw new ZError(12, 'onchain open record not found')
|
|
}
|
|
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(),
|
|
}
|
|
})
|
|
}
|
|
}
|