修改探索地图为上链, 修改连续签到奖励为手动领取

This commit is contained in:
CounterFire2023 2024-04-07 19:09:49 +08:00
parent 50011f1eaf
commit 66f2d39124
8 changed files with 339 additions and 30 deletions

15
.vscode/launch.json vendored
View File

@ -15,7 +15,20 @@
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node"
"type": "node"
},
{
"name": "Debug Admin",
"request": "launch",
"runtimeArgs": [
"run-script",
"dev:admin"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"type": "node"
},
]
}

View File

@ -15,6 +15,15 @@
}
```
### 0. 更新记录
#### 20240407
1. 增加探索预处理接口(24), 用于探索信息上链前创建一条探索记录
1. 修改 探索(13) request和response结构
1. 探索状态(12) 增加返回已探索总次数totalUsed
1. 探索状态(12), 移除signCfg, 增加seqStat, 用于标识连续签到奖励领取状态
1. 增加领取连续签到奖励(25)的接口
### 1. 钱包预登录
#### Request
@ -367,6 +376,7 @@ query param
```js
{
ticket: 1, // 可用探索次数
totalUsed: 1, // 已探索总次数
todayStat: 0, // 当日签到状态, 0: 未签到, 1: 已签到,但未领取 9:已签到, 已领取
todayTickets: 1, // 当日签到可领取次数
daysTotal: 1, // 累计签到天数
@ -376,9 +386,10 @@ query param
tickets: 2, // 满足条件后可领取数量
state: 0, // 领取状态: 0: 未领取, 1: 可领取, 9: 已领取
}],
signCfg: [{ // 连续签到配置
days: 2, //天数
tickets: 1, //额外增加的次数
seqStat: [{ // 连续签到状态
days: 3, // 天数
tickets: 2, // 满足条件后可领取数量
state: 0, // 领取状态: 0: 未领取, 1: 可领取, 9: 已领取
}]
}
```
@ -396,7 +407,7 @@ body:
```js
{
"step": 2 // 使用的次数
"id": "" // 探索预处理接口返回的id
}
```
@ -635,4 +646,61 @@ body:
{
ticket: 1 // 获得的探索次数
}
```
```
### 24.\* 探索预处理
#### Request
- URL`/api/game/pre_step`
- 方法:`POST`
- 头部:
- Authorization: Bearer JWT_token
body:
```js
{
"step": 2 // 使用的次数
}
```
#### Response
```js
{
id: '' // 本次探索上链需要的数据
}
```
### 25.\* 领取连续签到奖励
#### Request
- URL`/api/user/checkin/claim_seq`
- 方法:`POST`
- 头部:
- Authorization: Bearer JWT_token
body:
```js
{
"days": 3 // 领取的累计签到天数
}
```
#### Response
```js
{
ticket: 2, //获得探索次数
}
```
###

View File

@ -7,6 +7,8 @@
"build": "tsc -p tsconfig.json",
"dev:api": "ts-node -r tsconfig-paths/register src/api.ts",
"prod:api": "node dist/api.js",
"dev:admin": "ts-node -r tsconfig-paths/register src/admin.ts",
"prod:admin": "node dist/admin.js",
"lint": "eslint --ext .ts src/**",
"format": "eslint --ext .ts src/** --fix",
"initdata": "ts-node src/initdata.ts"

View File

@ -1,6 +1,6 @@
import { MANUAL_OPEN_GAME, RESET_STEP, SCORE_GAME_STEP } from 'common/Constants'
import { ActivityGame } from 'models/ActivityGame'
import { DAILY_SIGN, SIGN_TOTAL, TicketRecord, USE_TICKET } from 'models/TicketRecord'
import { DAILY_SIGN, SIGN_SEQ, SIGN_TOTAL, TicketRecord, USE_TICKET } from 'models/TicketRecord'
import { queryCheckInList } from 'services/chain.svr'
import { checkInToday, seqSignCfg, seqSignScore, totalSignCfg, totalSignScore } from 'services/sign.svr'
import { ZError, SyncLocker, BaseController, router } from 'zutils'
@ -8,6 +8,9 @@ import { formatDate } from 'zutils/utils/date.util'
import { generateChestLevel, generateNewChest, generateStepReward } from 'services/game.svr'
import { ChestStatusEnum } from 'models/ActivityChest'
import { updateRankScore } from 'services/rank.svr'
import { ExploreRecord } from 'models/ExploreRecord'
import { isObjectId } from 'zutils/utils/string.util'
import { GeneralScription } from 'models/chain/GeneralScription'
/**
*
@ -116,6 +119,57 @@ class GameController extends BaseController {
await ticketRecord.save()
return { ticket: score }
}
/**
*
*/
@router('post /api/user/checkin/claim_seq')
async claimCheckSeqResult(req) {
new SyncLocker().checkLock(req)
const user = req.user
let { days } = req.params
if (!days || isNaN(days)) {
throw new ZError(11, 'invalid days')
}
days = parseInt(days)
if (days < 1) {
throw new ZError(12, 'invalid days')
}
const dateTag = formatDate(new Date())
const checkRecord = await checkInToday(user.address.toLowerCase(), dateTag)
if (!checkRecord) {
throw new ZError(12, 'not signed in')
}
if (days > checkRecord.total) {
throw new ZError(13, 'invalid days')
}
const ticketRecords = await TicketRecord.find({ user: user.id, activity: user.activity, type: SIGN_TOTAL })
const claimedSet = new Set()
ticketRecords.forEach(record => {
claimedSet.add(record.data.day)
})
if (claimedSet.has(days)) {
throw new ZError(14, 'already claimed')
}
const score = totalSignScore(days)
if (score === 0) {
throw new ZError(15, 'invalid days')
}
const ticketRecord = new TicketRecord({
user: user.id,
activity: user.activity,
type: SIGN_SEQ,
data: { day: days },
score,
})
const gameRecord = await ActivityGame.insertOrUpdate({ user: user.id, activity: user.activity }, {})
if (MANUAL_OPEN_GAME && gameRecord.status === 0) {
throw new ZError(12, 'map not open')
}
await ActivityGame.updateOne({ user: user.id, activity: user.activity }, { $inc: { tickets: score } })
await ticketRecord.save()
return { ticket: score }
}
/**
*
*/
@ -157,10 +211,22 @@ class GameController extends BaseController {
todayStat = 2
}
const scoreBonus = seqSignScore(checkRecord?.count || 0)
const ticketRecords = await TicketRecord.find({ user: user.id, activity: user.activity, type: SIGN_TOTAL })
const ticketRecords = await TicketRecord.find({
user: user.id,
activity: user.activity,
type: { $in: [SIGN_TOTAL, USE_TICKET, SIGN_SEQ] },
})
const claimedSet = new Set()
const seqSet = new Set()
let totalUsed = 0
ticketRecords.forEach(record => {
claimedSet.add(record.data.day)
if (record.type === USE_TICKET) {
totalUsed += record.score
} else if (record.type === SIGN_TOTAL) {
claimedSet.add(record.data.day)
} else if (record.type === SIGN_SEQ) {
seqSet.add(record.data.day)
}
})
const totalStat = []
@ -178,17 +244,66 @@ class GameController extends BaseController {
state,
})
}
// 连续签到, 根据最大连续签到天数来判断是否拥有领取的资格
const seqStat = []
for (let cfg of seqSignCfg) {
let state = 0
if (cfg.days <= checkRecord?.maxSeq || 0) {
state = 1
}
if (seqSet.has(cfg.days)) {
state = 9
}
seqStat.push({
days: cfg.days,
tickets: cfg.reward,
state,
})
}
totalStat.sort((a, b) => a.days - b.days)
return {
ticket: record.tickets,
totalUsed: Math.abs(totalUsed),
signCfg,
todayStat,
todayTickets: 1 + scoreBonus,
daysTotal: checkRecord?.total || 0,
daysSeq: checkRecord?.count || 0,
seqStat,
totalStat,
}
}
@router('get /api/game/pre_step')
async gameChest(req) {
const user = req.user
new SyncLocker().checkLock(req)
let { step } = req.params
step = step || '1'
if (isNaN(step)) {
throw new ZError(11, 'invalid step')
}
// check if step is safe int
step = parseInt(step)
if (step < 1) {
step = 1
}
const record = await ActivityGame.insertOrUpdate({ user: user.id, activity: user.activity }, {})
if (MANUAL_OPEN_GAME && record.status === 0) {
throw new ZError(12, 'map not open')
}
if (record.tickets < step) {
throw new ZError(13, 'insufficient tickets')
}
const exploreRecord = new ExploreRecord({
user: user.id,
activity: user.activity,
step,
})
await exploreRecord.save()
return { id: exploreRecord.id }
}
/**
*
*/
@ -196,26 +311,36 @@ class GameController extends BaseController {
async gameStep(req, res) {
new SyncLocker().checkLock(req)
const user = req.user
let { step } = req.params
step = step || '1'
if (isNaN(step)) {
throw new ZError(11, 'invalid step')
const { id } = req.params
if (!id) {
throw new ZError(11, 'invalid id')
}
// check if step is safe int
step = parseInt(step)
if (step < 1) {
step = 1
if (!isObjectId(id)) {
throw new ZError(12, 'invalid id')
}
// const session = await mongoose.startSession()
// session.startTransaction()
// try {
const chainRecord = await GeneralScription.findOne({ from: user.address.toLowerCase(), op: 'explore', data: id })
if (!chainRecord) {
throw new ZError(13, 'waiting for chain confirm')
}
const exploreRecord = await ExploreRecord.findById(id)
if (!exploreRecord) {
throw new ZError(14, 'invalid id')
}
if (exploreRecord.status !== 0) {
throw new ZError(15, 'invalid status')
}
const step = exploreRecord.step
const record = await ActivityGame.insertOrUpdate({ user: user.id, activity: user.activity }, {})
if (MANUAL_OPEN_GAME && record.status === 0) {
throw new ZError(12, 'map not open')
throw new ZError(16, 'map not open')
}
if (record.tickets < step) {
throw new ZError(13, 'insufficient tickets')
exploreRecord.status = -1
await exploreRecord.save()
throw new ZError(17, 'insufficient tickets')
}
exploreRecord.status = 1
await exploreRecord.save()
const ticketRecord = new TicketRecord({
user: user.id,
activity: user.activity,
@ -253,14 +378,7 @@ class GameController extends BaseController {
await chest.save()
}
await ticketRecord.save()
// await ticketRecord.save({ session })
// await session.commitTransaction()
// session.endSession()
return { score, chests: chests.map(chest => chest.toJson()) }
// } catch (e) {
// session.abortTransaction()
// session.endSession()
// throw e
// }
}
}

View File

@ -0,0 +1,37 @@
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
@dbconn()
@index({ user: 1, activity: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'explore_record', timestamps: true },
})
class ExploreRecordClass extends BaseModule {
@prop({ required: true })
public user: string
@prop({ required: true })
public activity: string
@prop()
public step: number
/**
* 0: 未完成
* 1: 已完成
* -1: 无效
*/
@prop({ default: 0 })
public status: number
public toJson() {
return {
user: this.user,
activity: this.activity,
step: this.step,
status: this.status,
}
}
}
export const ExploreRecord = getModelForClass(ExploreRecordClass, { existingConnection: ExploreRecordClass['db'] })

View File

@ -5,6 +5,8 @@ import { BaseModule } from './Base'
export const DAILY_SIGN = 'daily_sign'
// 累计签到奖励
export const SIGN_TOTAL = 'sign_total'
// 连续签到奖励
export const SIGN_SEQ = 'sign_seq'
// 使用门票
export const USE_TICKET = 'use_ticket'

View File

@ -28,6 +28,9 @@ export class CheckInClass extends BaseModule {
// 连签天数
@prop({ default: 1 })
public count: number
// 最大连签天数
@prop({ default: 1 })
public maxSeq: number
// 累计签到天数
@prop({ default: 1 })
public total: number

View File

@ -0,0 +1,66 @@
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from '../Base'
import { hexToUtf8 } from 'zutils/utils/string.util'
import logger from 'logger/logger'
@dbconn('chain')
@index({ from: 1, op: 1 }, { unique: false })
@index({ from: 1, op: 1, data: 1 }, { unique: true })
@index({ hash: 1 }, { unique: true })
@index({ from: 1, blockTime: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'general_scription_record', timestamps: true },
})
export class GeneralScriptionClass extends BaseModule {
@prop({ required: true })
public from!: string
@prop()
public to: string
@prop({ required: true })
public hash: string
@prop()
public blockNumber: string
@prop()
public blockHash: string
@prop()
public blockTime: number
@prop()
public dateTag: string
@prop()
public data: string
@prop()
public op: string
@prop()
public value: string
@prop()
public input: string
@prop({ default: 0 })
public stat: number
public static async saveEvent(event: any) {
const dataStr = hexToUtf8(event.input)
const regexp = /data:,{\"p\":\"cf-20\",\"op\":\"(.+?)\",\"val\":\"(.+?)\"}/
const match = dataStr.match(regexp)
if (!match) {
logger.log('not a general scription:', event.hash)
return
}
event.op = match[1]
event.data = match[2]
logger.log('general scription with op:', event.op, ' data:', event.data)
return GeneralScription.insertOrUpdate({ hash: event.hash }, event)
}
public toJson() {
return {
address: this.from,
day: this.dateTag,
time: this.blockTime,
}
}
}
export const GeneralScription = getModelForClass(GeneralScriptionClass, {
existingConnection: GeneralScriptionClass['db'],
})