import { ZError, SyncLocker, ZRedisClient, BaseController, ROLE_ANON, role, router } from 'zutils' import { ActivityChest, ChestStatusEnum } from 'models/ActivityChest' import { ActivityUser } from 'models/ActivityUser' import { rankKey, rankLevel, updateRankScore } from 'services/rank.svr' import { formatDate } from 'utils/utcdate.util' import { ScoreRecord } from 'models/ScoreRecord' import { ChestRecord } from 'models/chain/ChestRecord' import { generateChestBonus, generateNewChest } from 'services/game.svr' import { ENHANCE_CHEST_GIFT, MAX_ENHANCE_COUNT_ADV, MAX_ENHANCE_COUNT_BASE, SCORE_ENHANCE_CHEST_GIFT, SCORE_OPEN_CHEST, } from 'common/Constants' import { formatAddress } from 'zutils/utils/chain.util' import { isObjectIdString, isValidShareCode } from 'common/Utils' import { checkReCaptcha } from 'services/google.svr' import { GeneralScription } from 'models/chain/GeneralScription' import { ChestEnhanceRecord } from 'models/ChestEnhanceRecord' /** * 宝箱相关接口 */ class BoxController extends BaseController { /** * 宝箱列表 */ @router('get /api/chest/list') async chestList(req) { const user = req.user const openRecords = await ChestRecord.find({ from: user.address.toLowerCase() }) const chestSet = new Set() openRecords.forEach(record => { chestSet.add(record.chestId) }) const chests = await ActivityChest.find({ activity: user.activity, user: user.id, status: { $in: [0, 1, 2] }, }).sort({ level: -1, }) // 20240328: 不再有上锁的箱子了 // for (let chest of chests) { // // TODO:: 待规则确定后, 将符合条件的锁定的箱子解锁 // if (chest.status === 0) { // if (user.allTaskFinished()) { // chest.status = 1 // await chest.save() // } // } // } const results = chests.map(chest => chest.toJson()) for (let result of results) { if (result.stat > 0 && chestSet.has(result.id)) { result.stat = 2 } } return results } /** * 宝箱助力列表 */ @router('post /api/chest/enhance/list') async enhanceList(req) { const user = req.user let { chestId, chestid } = req.params chestid = chestId || chestid if (!isObjectIdString(chestid)) { throw new ZError(11, 'must provide valid chestid') } 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') } const result = [] const records = await ChestEnhanceRecord.find({ chest: chest.id, activity: user.activity }).sort({ score: -1 }) const uids = records.map(record => record.user) const users = await ActivityUser.find({ _id: { $in: uids } }) const userMap = new Map() users.forEach(u => userMap.set(u.id, u)) for (let i = 0; i < records.length; i++) { const u = userMap.get(records[i].user) const score = records[i].score result.push({ nickname: u?.twitterName || u?.discordName || u?.address ? formatAddress(u.address) : 'unknown', avatar: u.twitterAvatar || '', score: score, // @ts-ignore time: records[i].createdAt.getTime(), }) } return result } /** * 我的助力列表 */ @router('post /api/user/enhance/list') async myEnhanceList(req) { const user = req.user const result = [] const records = await ChestEnhanceRecord.find({ user: user.id, activity: user.activity }).sort({ _id: -1 }) const uids = records.map(record => record.chestOwner) const users = await ActivityUser.find({ _id: { $in: uids } }) const userMap = new Map() users.forEach(u => userMap.set(u.id, u)) for (let i = 0; i < records.length; i++) { const u = userMap.get(records[i].user) const score = records[i].myScore || 0 result.push({ nickname: u?.twitterName || u?.discordName || u?.address ? formatAddress(u.address) : 'unknown', avatar: u.twitterAvatar || '', score: score, // @ts-ignore time: records[i].createdAt.getTime(), }) } return result } /** * 宝箱助力状态查询 */ @router('post /api/chest/enhance/state') async enhanceState(req) { const { code, chestId } = req.params const user = req.user if (code && !isValidShareCode(code)) { throw new ZError(11, 'invalid share code') } if (chestId && !isObjectIdString(chestId)) { throw new ZError(11, 'invalid chest id') } if (!code && !chestId) { throw new ZError(11, 'must provide share code or chest id') } let chest: any if (chestId) { chest = await ActivityChest.findById(chestId) } else { chest = await ActivityChest.findOne({ shareCode: code, activity: user.activity }) } if (!chest) { throw new ZError(12, 'chest not found') } if (chest.status === ChestStatusEnum.OPENED) { throw new ZError(14, 'chest already opened') } if (chest.status === ChestStatusEnum.LOCKED) { throw new ZError(15, 'chest is locked') } const enhanced = chest.bonusUsers.includes(user.id) ? 1 : 0 const userMax = user.twitterId && user.discordId ? MAX_ENHANCE_COUNT_ADV : MAX_ENHANCE_COUNT_BASE const dateTag = formatDate(new Date()) const userCurrent = await ChestEnhanceRecord.countDocuments({ user: user.id, activity: user.activity, dateTag }) return { userCurrent, userMax, enhanced, chestCurrent: chest.bonusUsers.length, chestMax: chest.maxBounsCount, } } /** * 宝箱助力 */ @router('post /api/chest/enhance') async enhance(req) { new SyncLocker().checkLock(req) await checkReCaptcha(req, 'chest_share') const { code } = req.params const user = req.user const uid = user.id if (!isValidShareCode(code)) { throw new ZError(11, 'invalid share code') } const chainRecord = await GeneralScription.findOne({ from: user.address.toLowerCase(), op: 'chest_enhance', data: code, }) if (!chainRecord) { throw new ZError(13, 'waiting for chain confirm') } const chest = await ActivityChest.findOne({ shareCode: code, activity: user.activity }) if (!chest) { throw new ZError(12, 'chest not found') } if (chest.bonusUsers.includes(uid)) { throw new ZError(10, 'user already enhanced') } if (chest.bonusUsers.length >= chest.maxBounsCount) { throw new ZError(13, 'enhanced times exceed') } if (chest.status === ChestStatusEnum.OPENED) { throw new ZError(14, 'chest already opened') } if (chest.status === ChestStatusEnum.LOCKED) { throw new ZError(15, 'chest is locked') } // 生产环境不能助力自己 if (process.env.NODE_ENV === 'production' && chest.user === uid) { throw new ZError(15, 'can not enhance self') } const userMax = user.twitterId && user.discordId ? MAX_ENHANCE_COUNT_ADV : MAX_ENHANCE_COUNT_BASE const dateTag = formatDate(new Date()) const userCurrent = await ChestEnhanceRecord.countDocuments({ user: user.id, activity: user.activity, dateTag }) if (userCurrent >= userMax) { throw new ZError(16, 'user enhance times exceed') } const score = chest.bounsCfg[chest.bonusUsers.length] || chest.bounsCfg[chest.bounsCfg.length - 1] await ActivityChest.updateOne( { _id: chest.id }, { $inc: { scoreBonus: score }, $push: { bonusUsers: uid }, }, ) const enhanceRecord = new ChestEnhanceRecord({ user: uid, activity: user.activity, dateTag, chest: chest.id, score, chestOwner: chest.user, myScore: ENHANCE_CHEST_GIFT, }) await enhanceRecord.save() await updateRankScore({ user: user.id, score: ENHANCE_CHEST_GIFT, activity: user.activity, scoreType: SCORE_ENHANCE_CHEST_GIFT, scoreParams: { date: dateTag, chestId: chest.id, }, }) return { score: ENHANCE_CHEST_GIFT, } // const chestsForUser = await ActivityChest.find({ user: uid, activity: user.activity }) // 如果用户没有宝箱, 则说明用户是新用户, 生成一个宝箱 // if (chestsForUser.length === 0) { // const newChest = generateNewChest(uid, user.activity, 1, ChestStatusEnum.NORMAL) // await newChest.save() // return { // score: 0, // chests: [newChest.toJson()], // } // } else { // return { // score: 0, // } // } } /** * 开启宝箱 * 流程如下: * 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.params if (!isObjectIdString(chestId)) { throw new ZError(11, 'must provide valid chestId') } const openRecord = await ChestRecord.findOne({ from: user.address.toLowerCase(), 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 === ChestStatusEnum.OPENED) { throw new ZError(14, 'chest already opened') } chest.status = ChestStatusEnum.OPENED const score = chest.scoreInit + chest.scoreBonus const dateTag = formatDate(new Date()) let items = await generateChestBonus(chest) if (!chest.items) { chest.items = [] } items.forEach(item => { chest.items.push(item.id) }) await updateRankScore({ user: user.id, score: score, activity: user.activity, scoreType: SCORE_OPEN_CHEST, scoreParams: { date: dateTag, chestId: chest.id, level: chest.level, items: chest.items, }, }) await chest.save() return { score, items } } /** * 宝箱开启记录 */ @router('get /api/chest/open/history') async openChestHistory(req) { const user = req.user const records = await ScoreRecord.find({ user: user.id, activity: user.activity, type: SCORE_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(), items: record.data.items, } }) } }