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