diff --git a/package.json b/package.json index ad74b01..1a1b13a 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "mongoose-findorcreate": "^3.0.0", "nanoid": "^3.1.23", "node-html-to-image": "^3.1.0", + "node-schedule": "^2.0.0", "qrcode": "^1.4.4", "querystring": "^0.2.1", "queue": "^6.0.2", @@ -62,6 +63,7 @@ "@types/dotenv": "^8.2.0", "@types/mongoose": "5.10.3", "@types/node": "^14.14.20", + "@types/node-schedule": "^1.3.2", "@types/redis": "^2.8.28", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^3.4.0", diff --git a/src/admin.server.ts b/src/admin.server.ts index 4d3c7c4..9811d0f 100644 --- a/src/admin.server.ts +++ b/src/admin.server.ts @@ -9,6 +9,7 @@ import { AdminRole } from 'models/admin/AdminRole' import { Shop } from './models/shop/Shop' import { MongoTool } from './services/MongoTool' import { GameItem } from './models/content/GameItem' +import ExamSchedule from './schedule/exam.schedule' const rbacPlugin = require('plugins/zrbac') const zAuthPlugin = require('plugins/zauth') @@ -148,6 +149,11 @@ export class AdminServer { new MongoTool().init('gameitem', id) } + private initSchedules() { + new ExamSchedule().scheduleAll() + // new ExamSchedule().parseAllRecord() + } + public async start() { let self = this return new Promise(async (resolve, reject) => { @@ -158,6 +164,7 @@ export class AdminServer { self.registerRouter() self.setErrHandler() self.setFormatSend() + self.initSchedules() this.server.listen({ port: config.admin.port }, (err: any, address: any) => { if (err) { logger.log(err) diff --git a/src/models/shop/ShopExam.ts b/src/models/shop/ShopExam.ts index 8415621..efd41c0 100644 --- a/src/models/shop/ShopExam.ts +++ b/src/models/shop/ShopExam.ts @@ -1,10 +1,11 @@ import { dbconn } from '../../decorators/dbconn' -import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' +import { getModelForClass, index, modelOptions, prop, ReturnModelType } from '@typegoose/typegoose' import { BaseModule } from '../Base' import { noJson } from '../../decorators/nojson' import { Severity } from '@typegoose/typegoose/lib/internal/constants' import { Base } from '@typegoose/typegoose/lib/defaultClasses' import { CompactPuzzleClass } from '../match/PuzzleSession' +import { AdminClass } from '../admin/Admin' export class ShopPuzzleClass extends Base { @prop() @@ -70,7 +71,7 @@ export class ExamRewardClass extends Base { /** * 奖励类型 - * @type {number} 0: 单局, 1: 累积 + * @type {number} 0: 单局, 1: 累积 2: 累计排名 */ @prop({ default: 0 }) public type: number @@ -192,6 +193,19 @@ export class ShopExamClass extends BaseModule { @prop({ type: [ShopPuzzleClass] }) public questions: ShopPuzzleClass[] + /** + * 挑战状态 + * @type {number} 0: 默认状态, 9: 奖励已发放 + */ + @prop({ default: 0 }) + public status: number + + /** + * 返回所有已完结但未结算的挑战记录 + */ + public static allUnSettleRecords(this: ReturnModelType) { + return this.find({ endTime: { $gte: Date.now() }, status: { $ne: 9 } }) + } public static parseQueryParam(params) { let { key, timeBegin, timeEnd, active, shop, source } = params let opt: any = { deleted: false } @@ -239,6 +253,25 @@ export class ShopExamClass extends BaseModule { return results } + /** + * 根据排名获取赛季奖励 + * @param {number} rank + */ + public getRewardByRank(rank: number) { + if (!this.rewardInfo || this.rewardInfo.length === 0) { + return [] + } + + let results = [] + for (let reward of this.rewardInfo) { + if (rank <= reward.rank && reward.type === 2) { + results.push({ id: reward._id + '', coupon: reward.coupon, count: reward.count }) + break + } + } + return results + } + public toPartnerJson() { const exportKeys = [ '_id', diff --git a/src/models/user/UserMail.ts b/src/models/user/UserMail.ts index 2e28f50..7edf2c0 100644 --- a/src/models/user/UserMail.ts +++ b/src/models/user/UserMail.ts @@ -73,6 +73,18 @@ export class UserMailClass extends BaseModule { return record } + public static async addOneMail({ accountId, shop, items, title, content, senderShop }) { + let record = new UserMail({}) + record.title = title + record.content = content + record.sender = shop + record.accountId = accountId + record.items = items + record.senderShop = senderShop + record.expire = Date.now() + 3600 * 24 * 15 * 1000 + await record.save() + } + public static async updateExpire(accountId: string) { await UserMail.updateMany( { accountId, deleted: false, expire: { $lt: Date.now() } }, diff --git a/src/schedule/exam.schedule.ts b/src/schedule/exam.schedule.ts new file mode 100644 index 0000000..b85b2bc --- /dev/null +++ b/src/schedule/exam.schedule.ts @@ -0,0 +1,70 @@ +import * as schedule from 'node-schedule' +import { singleton } from '../decorators/singleton' +import { ShopExam } from '../models/shop/ShopExam' +import { getRankCount, getRankList } from '../services/Rank' +import { rankKey } from '../services/GameLogic' +import { UserMail } from '../models/user/UserMail' +import { Coupon } from '../models/shop/Coupon' +import { Shop } from '../models/shop/Shop' + +@singleton +export default class ExamSchedule { + async parseOneExam(data: any) { + const shop = data.shop + let shopName = '' + const shopData = await Shop.fetchByID(shop) + if (shopData) { + shopName = shopData.showName + } + const level = data.id + '_total' + const mode = 2 + const skip = 0 + const key = rankKey(shop, level, mode) + const limit = (await getRankCount(key)) as number + let datas: any = await getRankList(skip, limit, key) + const accountIDS = [] + for (let i = 0, l = datas.length; i < l; i += 2) { + accountIDS.push(datas[i]) + } + data.rewardInfo.sort((a, b) => a.rank - b.rank) + for (let i = 0, l = accountIDS.length; i < l; i++) { + let rewards = data.getRewardByRank(i + 1) + let items = [] + for (let r of rewards) { + let name = '抽奖券' + if (r.type === 0) { + let cdata = await Coupon.findById(r.coupon) + if (cdata) { + name = cdata.name + } + } + items.push({ + itemId: r.coupon, + name, + count: r.count, + }) + } + const title = `${data.name} 第${i + 1}名奖励` + const content = `您在 ${shopName} 的 ${data.name}活动中获得第${i + 1}名, 获得如下奖励` + await UserMail.addOneMail({ + accountId: accountIDS[i], + shop, + items, + title, + content, + senderShop: shopName, + }) + } + } + async parseAllRecord() { + let records = await ShopExam.allUnSettleRecords() + for (let record of records) { + await this.parseOneExam(record) + } + } + scheduleAll() { + const job = schedule.scheduleJob('1 0 0 * * *', async () => { + await this.parseAllRecord() + }) + } +} diff --git a/yarn.lock b/yarn.lock index f46b8d1..6f07be3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -194,6 +194,13 @@ "@types/mongodb" "*" "@types/node" "*" +"@types/node-schedule@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/node-schedule/-/node-schedule-1.3.2.tgz#cc7e32c6795cbadc8de03d0e1f86311727375423" + integrity sha512-Y0CqdAr+lCpArT8CJJjJq4U2v8Bb5e7ru2nV/NhDdaptCMCRdOL3Y7tAhen39HluQMaIKWvPbDuiFBUQpg7Srw== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@^14.14.20": version "14.14.41" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615" @@ -773,6 +780,14 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cron-parser@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-3.5.0.tgz#b1a9da9514c0310aa7ef99c2f3f1d0f8c235257c" + integrity sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ== + dependencies: + is-nan "^1.3.2" + luxon "^1.26.0" + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -870,6 +885,13 @@ default-user-agent@^1.0.0: dependencies: os-name "~1.0.3" +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + degenerator@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-2.2.0.tgz#49e98c11fa0293c5b26edfbb52f15729afcdb254" @@ -1845,6 +1867,14 @@ is-json@^2.0.1: resolved "https://registry.yarnpkg.com/is-json/-/is-json-2.0.1.tgz#6be166d144828a131d686891b983df62c39491ff" integrity sha1-a+Fm0USCihMdaGiRuYPfYsOUkf8= +is-nan@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -2092,6 +2122,11 @@ loglevel@^1.6.7, loglevel@^1.7.0: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== +long-timeout@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" + integrity sha1-lyHXiLR+C8taJMLivuGg2lXatRQ= + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -2106,6 +2141,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +luxon@^1.26.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.27.0.tgz#ae10c69113d85dab8f15f5e8390d0cbeddf4f00f" + integrity sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA== + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" @@ -2308,6 +2348,15 @@ node-html-to-image@^3.1.0: puppeteer "3.0.0" puppeteer-cluster "^0.21.0" +node-schedule@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-2.0.0.tgz#73ab4957d056c63708409cc1fab676e0e149c191" + integrity sha512-cHc9KEcfiuXxYDU+HjsBVo2FkWL1jRAUoczFoMIzRBpOA4p/NRHuuLs85AWOLgKsHtSPjN8csvwIxc2SqMv+CQ== + dependencies: + cron-parser "^3.1.0" + long-timeout "0.1.1" + sorted-array-functions "^1.3.0" + nth-check@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" @@ -2330,6 +2379,11 @@ object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.2.tgz#b6385a3e2b7cae0b5eafcf90cddf85d128767f30" integrity sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA== +object-keys@^1.0.12: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -3011,6 +3065,11 @@ sonic-boom@^1.0.2: atomic-sleep "^1.0.0" flatstr "^1.0.12" +sorted-array-functions@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5" + integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA== + source-map-support@^0.5.17: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"