342 lines
11 KiB
TypeScript
342 lines
11 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 { 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,
|
|
}
|
|
})
|
|
}
|
|
}
|