增加测验的相关接口
This commit is contained in:
parent
8cf133b200
commit
e9a4a4d552
85
doc/api.md
85
doc/api.md
@ -198,4 +198,89 @@
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 获取测验题目列表
|
||||||
|
|
||||||
|
1. Method: POST
|
||||||
|
2. URI: /api/:accountid/exam/list
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| -------- | -------------------------------------- |
|
||||||
|
| accountid | 帐号id |
|
||||||
|
|
||||||
|
> POST参数
|
||||||
|
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| -------- | -------------------------------------- |
|
||||||
|
| shop | 店铺id, 比如 607ff59d4a4e16687a3b7079 |
|
||||||
|
|
||||||
|
3. Response: JSON
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
session: "6080f330b9655b5c0467ee5a", // 当前局的id,提交答案时必须上报该字段
|
||||||
|
timeone: 10, // 每题最多回答时间
|
||||||
|
records: [{
|
||||||
|
"id": "6080f330b9655b5c0467ee5e", // 题目id
|
||||||
|
"title": "“大丈夫为国捐躯,死而无憾!”这话是谁说的?", // 问题
|
||||||
|
"answers": [ // 可选答案
|
||||||
|
"刘铭传",
|
||||||
|
"徐骧",
|
||||||
|
"刘步蟾",
|
||||||
|
"刘永福"
|
||||||
|
],
|
||||||
|
"category": "体育-体育", // 类型
|
||||||
|
"type": 1, // 题目类型 1: 普通的文字选择题, 2: 图形
|
||||||
|
"quality": 1 // 题目难度
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 上报测验题目答案
|
||||||
|
|
||||||
|
1. Method: POST
|
||||||
|
2. URI: /api/:accountid/exam/answer
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| -------- | -------------------------------------- |
|
||||||
|
| accountid | 帐号id |
|
||||||
|
|
||||||
|
> POST参数
|
||||||
|
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| -------- | -------------------------------------- |
|
||||||
|
| id | 题目id |
|
||||||
|
| session | 当局的id, 从关卡题目列表中获取 |
|
||||||
|
| answer | 回答的选项 |
|
||||||
|
| type | 回答类型, 0: 正常, 1: 超时 |
|
||||||
|
|
||||||
|
3. Response: JSON
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
result: 1, //答题结果 1: 正确, 0 : 错误
|
||||||
|
gameResult: 0, // 当前局游戏结果 0: 未完成, -1: 失败, 1: 胜利
|
||||||
|
overtime: 0, // 当前回答是否超时 0: 未超时, 1: 超时
|
||||||
|
"stats": { // 当局的状态
|
||||||
|
"1111": {
|
||||||
|
"answer": [ // 每一题的结果
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"rightCount": 1, // 答对的数量
|
||||||
|
"errorCount": 1, // 答错的数量
|
||||||
|
"comboCount": 0, // 当前连续答对的数量
|
||||||
|
"maxCombo": 1, // 当局连续答对的最大数量
|
||||||
|
"score": 10, // 当前得分
|
||||||
|
"star": 1, // 当局胜利后获得的星星
|
||||||
|
"timeLeft": 1, // 当局剩余时间
|
||||||
|
"gameResult": 0, // 当局的游戏结果, 单人的话和上一层gameResult相同
|
||||||
|
"timeLast": 1620973155307 //上次回答时间
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
132
src/api/controllers/exam.controller.ts
Normal file
132
src/api/controllers/exam.controller.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import BaseController from '../../common/base.controller'
|
||||||
|
import { role, router } from '../../decorators/router'
|
||||||
|
import { isObjectId } from '../../utils/string.util'
|
||||||
|
import { ZError } from '../../common/ZError'
|
||||||
|
import { ShopExam } from '../../models/shop/ShopExam'
|
||||||
|
import { Puzzle } from '../../models/content/Puzzle'
|
||||||
|
import {
|
||||||
|
PuzzleSession,
|
||||||
|
PuzzleStatusClass
|
||||||
|
} from '../../models/match/PuzzleSession'
|
||||||
|
import {
|
||||||
|
calcExamScore,
|
||||||
|
getRank,
|
||||||
|
transformRecord, updateExamRank, updateSingleRank
|
||||||
|
} from '../../services/GameLogic'
|
||||||
|
|
||||||
|
class ExamController extends BaseController {
|
||||||
|
@role('anon')
|
||||||
|
@router('post /api/:accountid/exam/list')
|
||||||
|
async list(req, res) {
|
||||||
|
let { shop, accountid } = req.params
|
||||||
|
if (!shop || !isObjectId(shop)) {
|
||||||
|
throw new ZError(10, '没有店铺id或者店铺id格式不正确, 测试使用: 607ff59d4a4e16687a3b7079')
|
||||||
|
}
|
||||||
|
const now = Date.now()
|
||||||
|
let record = await ShopExam.findOne({shop, beginTime: {$lte: now}, endTime: {$gte: now}, active: true, deleted: false})
|
||||||
|
if (!record) {
|
||||||
|
throw new ZError(11, '暂时没有可参加的测试')
|
||||||
|
}
|
||||||
|
let params = {}
|
||||||
|
if (record.qtypes && record.qtypes.length > 0) {
|
||||||
|
params = {$or: [{tag: {$in: record.qtypes}}, {sub_tag: {$in: record.qtypes}}]}
|
||||||
|
}
|
||||||
|
let questions = await Puzzle.randomQuestions(params, record.qcount)
|
||||||
|
let history = new PuzzleSession({ shop, level: 0})
|
||||||
|
let stat = new PuzzleStatusClass()
|
||||||
|
stat.timeLast = now
|
||||||
|
history.members.set(accountid, stat)
|
||||||
|
for (let record of questions) {
|
||||||
|
history.questions.set(record.id, record.a1)
|
||||||
|
}
|
||||||
|
history.expire = Date.now() + (record.qcount * record.timeone || 90) * 1000
|
||||||
|
history.type = 2
|
||||||
|
history.total = record.qcount
|
||||||
|
history.timeone = record.timeone
|
||||||
|
history.difficultyMode = 0
|
||||||
|
history.qtypes = record.qtypes
|
||||||
|
await history.save()
|
||||||
|
const results = transformRecord(questions)
|
||||||
|
return {
|
||||||
|
session: history.id,
|
||||||
|
records: results,
|
||||||
|
timeone: record.timeone
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@role('anon')
|
||||||
|
@router('post /api/:accountid/exam/answer')
|
||||||
|
async report(req, res) {
|
||||||
|
/**
|
||||||
|
* type 0:正常 1: 超时
|
||||||
|
*/
|
||||||
|
let { id, answer, type, session, accountid } = req.params
|
||||||
|
if (!id || !session) {
|
||||||
|
throw new ZError(11, 'param mismatch')
|
||||||
|
}
|
||||||
|
let history = await PuzzleSession.findById(session)
|
||||||
|
if (!history) {
|
||||||
|
throw new ZError(13, 'not found match info')
|
||||||
|
}
|
||||||
|
if (!history.members.has(accountid)) {
|
||||||
|
throw new ZError(14, 'not in current match')
|
||||||
|
}
|
||||||
|
if (!history.questions.has(id)) {
|
||||||
|
throw new ZError(16, 'current question not in current match')
|
||||||
|
}
|
||||||
|
let statMap = history.members.get(accountid)
|
||||||
|
if (statMap.answer.has(id)) {
|
||||||
|
throw new ZError(15, 'current question already answered')
|
||||||
|
}
|
||||||
|
let time = (history.timeone* 1000 - (Date.now() - statMap.timeLast)) / 1000
|
||||||
|
let record = history.questions.get(id)
|
||||||
|
let result = record === answer ? 1 : 0
|
||||||
|
let overtime = 0
|
||||||
|
if (type == 1) { // type = 1 为客户端上报的超时消息, 直接判负
|
||||||
|
result = 0
|
||||||
|
overtime = 1
|
||||||
|
} else if (time < 0){
|
||||||
|
result = 0
|
||||||
|
overtime = 1
|
||||||
|
}
|
||||||
|
statMap.timeLast = Date.now()
|
||||||
|
statMap.answer.set(id, result)
|
||||||
|
if (result == 1) {
|
||||||
|
statMap.rightCount++
|
||||||
|
statMap.comboCount++
|
||||||
|
statMap.maxCombo = Math.max(statMap.maxCombo, statMap.comboCount)
|
||||||
|
} else {
|
||||||
|
statMap.errorCount++
|
||||||
|
statMap.comboCount = 0
|
||||||
|
}
|
||||||
|
history.status = 1
|
||||||
|
|
||||||
|
let score = result ? calcExamScore(time, statMap.comboCount) : 0
|
||||||
|
statMap.score += score
|
||||||
|
history.markModified('members')
|
||||||
|
await history.save()
|
||||||
|
let gameResult = 0
|
||||||
|
let rspData: any = { result, answer: record, stats: history.members, overtime }
|
||||||
|
if (statMap.answer.size >= history.questions.size) {
|
||||||
|
gameResult = 1
|
||||||
|
}
|
||||||
|
if (gameResult === 1) {
|
||||||
|
history.status = 9
|
||||||
|
let rankObj = {
|
||||||
|
shop: history.shop,
|
||||||
|
level: 'exam',
|
||||||
|
accountId: accountid,
|
||||||
|
score: statMap.score,
|
||||||
|
mode: 0,
|
||||||
|
session: history.id
|
||||||
|
}
|
||||||
|
await history.save()
|
||||||
|
await updateExamRank(rankObj)
|
||||||
|
let {rankList, userRank } = await getRank({shop: history.shop, level: 'exam', accountId: accountid, mode: 0, skip: 0, limit: 20})
|
||||||
|
rspData.rankList = rankList
|
||||||
|
rspData.userRank = userRank
|
||||||
|
}
|
||||||
|
rspData.gameResult = gameResult
|
||||||
|
return rspData
|
||||||
|
}
|
||||||
|
}
|
@ -27,9 +27,7 @@ import {
|
|||||||
transformRecord,
|
transformRecord,
|
||||||
updateSingleRank
|
updateSingleRank
|
||||||
} from '../../services/GameLogic'
|
} from '../../services/GameLogic'
|
||||||
import { ObjectId } from 'bson'
|
|
||||||
import { isObjectId } from '../../utils/string.util'
|
import { isObjectId } from '../../utils/string.util'
|
||||||
import { getAccountScore } from '../../services/Rank'
|
|
||||||
|
|
||||||
|
|
||||||
class PuzzleController extends BaseController {
|
class PuzzleController extends BaseController {
|
||||||
@ -103,7 +101,7 @@ class PuzzleController extends BaseController {
|
|||||||
}
|
}
|
||||||
let record = history.questions.get(id)
|
let record = history.questions.get(id)
|
||||||
let result = record === answer ? 1 : 0
|
let result = record === answer ? 1 : 0
|
||||||
if (type == 1) {
|
if (type == 1) { // type = 1 为客户端上报的超时消息, 直接判负
|
||||||
result = 0
|
result = 0
|
||||||
}
|
}
|
||||||
if (history.status == 9 || history.hasExpired()) {
|
if (history.status == 9 || history.hasExpired()) {
|
||||||
@ -113,7 +111,7 @@ class PuzzleController extends BaseController {
|
|||||||
if (statMap.answer.has(id)) {
|
if (statMap.answer.has(id)) {
|
||||||
throw new ZError(15, 'current question already answered')
|
throw new ZError(15, 'current question already answered')
|
||||||
}
|
}
|
||||||
|
statMap.timeLast = Date.now()
|
||||||
statMap.answer.set(id, result)
|
statMap.answer.set(id, result)
|
||||||
if (result == 1) {
|
if (result == 1) {
|
||||||
statMap.rightCount++
|
statMap.rightCount++
|
||||||
|
@ -55,6 +55,12 @@ export class PuzzleStatusClass {
|
|||||||
|
|
||||||
@prop()
|
@prop()
|
||||||
timeLeft: number
|
timeLeft: number
|
||||||
|
/**
|
||||||
|
* 最后一次答题时间
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
@prop()
|
||||||
|
timeLast: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏结果
|
* 游戏结果
|
||||||
@ -90,6 +96,7 @@ export class PuzzleSessionClass extends BaseModule {
|
|||||||
* 类型
|
* 类型
|
||||||
* 0: 单人
|
* 0: 单人
|
||||||
* 1: 多人
|
* 1: 多人
|
||||||
|
* 2: 测验
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
@prop({default: 0})
|
@prop({default: 0})
|
||||||
@ -117,6 +124,13 @@ export class PuzzleSessionClass extends BaseModule {
|
|||||||
@prop()
|
@prop()
|
||||||
public total: number
|
public total: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每一题回答时间
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
@prop()
|
||||||
|
public timeone: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 比赛状态
|
* 比赛状态
|
||||||
* 0: 未开始答题
|
* 0: 未开始答题
|
||||||
@ -148,6 +162,7 @@ export class PuzzleSessionClass extends BaseModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get scheduleKey() {
|
public get scheduleKey() {
|
||||||
|
// @ts-ignore
|
||||||
return this.room + this._id
|
return this.room + this._id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ export class ShopExamClass extends BaseModule {
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
@prop({default: 5})
|
@prop({default: 5})
|
||||||
public timePer: number
|
public timeone: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 活动正式开始时间, 从0点开始
|
* 活动正式开始时间, 从0点开始
|
||||||
|
@ -114,6 +114,10 @@ export async function sendOneQuestion(history: any) {
|
|||||||
}, history.scheduleKey)
|
}, history.scheduleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calcExamScore(time: number, combo: number) {
|
||||||
|
const cfg = new GameEnv()
|
||||||
|
return cfg.pvpBaseScore + time / 100 * cfg.pvpTimeRate + combo * cfg.pvpComboRate
|
||||||
|
}
|
||||||
|
|
||||||
export function calcPvpScore(timeKey: string, combo: number) {
|
export function calcPvpScore(timeKey: string, combo: number) {
|
||||||
const time = new Schedule().getLeftTime(timeKey)
|
const time = new Schedule().getLeftTime(timeKey)
|
||||||
@ -135,7 +139,7 @@ export function rankKey(shop: string, level: number|string, mode: number) {
|
|||||||
*/
|
*/
|
||||||
export async function updateSingleRank({shop, level, accountId, score, session, mode }
|
export async function updateSingleRank({shop, level, accountId, score, session, mode }
|
||||||
: {shop: string,
|
: {shop: string,
|
||||||
level: number,
|
level: number | string,
|
||||||
accountId: string,
|
accountId: string,
|
||||||
score: number,
|
score: number,
|
||||||
mode: number,
|
mode: number,
|
||||||
@ -155,6 +159,17 @@ export async function updateSingleRank({shop, level, accountId, score, session,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateExamRank({shop, level, accountId, score, session, mode }
|
||||||
|
: {shop: string,
|
||||||
|
level: number | string,
|
||||||
|
accountId: string,
|
||||||
|
score: number,
|
||||||
|
mode: number,
|
||||||
|
session: string}) {
|
||||||
|
const totalKey = rankKey(shop, 'exam', mode)
|
||||||
|
await updateRank(accountId, score, totalKey)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRank({shop, level, accountId, mode, skip, limit }
|
export async function getRank({shop, level, accountId, mode, skip, limit }
|
||||||
: {shop: string,
|
: {shop: string,
|
||||||
level: number | string,
|
level: number | string,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user