From 4f5d9b77edbd45ce8f41eefd1045436b0d91409d Mon Sep 17 00:00:00 2001 From: liangtongchuan Date: Mon, 2 Jan 2023 21:45:10 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(battle):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BD=95=E5=83=8F=E4=B8=8A=E4=BC=A0=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../servers/battle/handler/ladderHandler.ts | 12 ++- .../app/servers/battle/handler/pvpHandler.ts | 13 ++- shared/consts/statusCode.ts | 3 + shared/db/LadderMatchRec.ts | 10 ++- shared/db/PvpRecord.ts | 13 ++- shared/pubUtils/battleUtils.ts | 49 +++++++++++ web-server/app/controller/game.ts | 81 ++++++++++++++++++- web-server/app/router.ts | 1 + web-server/config/config.default.ts | 5 ++ web-server/package.json | 5 +- 10 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 shared/pubUtils/battleUtils.ts diff --git a/game-server/app/servers/battle/handler/ladderHandler.ts b/game-server/app/servers/battle/handler/ladderHandler.ts index ca57e3257..98bada3cd 100644 --- a/game-server/app/servers/battle/handler/ladderHandler.ts +++ b/game-server/app/servers/battle/handler/ladderHandler.ts @@ -11,7 +11,7 @@ import { LadderMatchRecModel } from '../../../db/LadderMatchRec'; import { HeroModel } from '../../../db/Hero'; import { LADDER } from '../../../pubUtils/dicParam'; import { handleCost } from '../../../services/role/rewardService'; -import { DEBUG_MAGIC_WORD, ITEM_CHANGE_REASON, LADDER_OPP_STATUS, LADDER_STATUS, REDIS_KEY } from '../../../consts'; +import { DEBUG_MAGIC_WORD, ITEM_CHANGE_REASON, LADDER_OPP_STATUS, LADDER_STATUS, REDIS_KEY, WAR_TYPE } from '../../../consts'; import { checkBattleHeroesByHid } from '../../../services/normalBattleService'; import { ServerlistModel } from '../../../db/Serverlist'; import { saveLadderDefCeByData } from '../../../services/redisService'; @@ -19,6 +19,7 @@ import { pushLadderTopChangeMsg } from '../../../services/sysChatService'; import { checkTaskInLadderEnd, checkTaskInLadderStart, checkTaskInLadderSweep } from '../../../services/task/taskService'; import { BattleRecordModel } from '../../../db/BattleRecord'; import { isHeroHidden } from '../../../services/dataService'; +import { getRemoteRplFilePath, getRemoteRplPrefix } from '../../../pubUtils/battleUtils'; export default function (app: Application) { new HandlerService(app, {}); @@ -356,7 +357,14 @@ export class LadderHandler { async getRec(msg: {}, session: BackendSession) { let roleId = session.get('roleId'); let list = await LadderMatchRecModel.findRec(roleId); - return resResult(STATUS.SUCCESS, { list }); + return resResult(STATUS.SUCCESS, { + list: list.map(rec => { + const { roleId1, battleCode, hasRpl } = rec; + const rplFileUrl = battleCode && hasRpl ? getRemoteRplFilePath(roleId1, WAR_TYPE.LADDER, battleCode) : ''; + return { ...rec, rplFileUrl }; + }), + rplPrefixUrl: getRemoteRplPrefix(pinus.app.get('env')) + }); } // debug接口 diff --git a/game-server/app/servers/battle/handler/pvpHandler.ts b/game-server/app/servers/battle/handler/pvpHandler.ts index 5875b132e..2d0da0086 100644 --- a/game-server/app/servers/battle/handler/pvpHandler.ts +++ b/game-server/app/servers/battle/handler/pvpHandler.ts @@ -1,3 +1,4 @@ +import { WAR_TYPE } from './../../../consts/constModules/battleConst'; import { Application, BackendSession, pinus, HandlerService, } from 'pinus'; import { findIndex } from 'underscore'; @@ -21,6 +22,7 @@ import { BattleRecordModel } from '../../../db/BattleRecord'; import { PvpRecordModel, PvpRecordParam, PvpRecordType } from '../../../db/PvpRecord'; import { pvpEndParamInter } from '../../../pubUtils/interface'; import { getSeconds, nowSeconds } from '../../../pubUtils/timeUtil'; +import { getRemoteRplPrefix, getRemoteRplFilePath } from '../../../pubUtils/battleUtils'; import { PlayerDetail, PlayerDetailHero } from '../../../domain/battleField/guild'; import { PvpSaveDataModel } from '../../../db/PvpSaveData'; import { PVPConfigModel } from '../../../db/PvpConfig'; @@ -262,7 +264,7 @@ export class PvpHandler { let seasonNum: number = this.app.get('pvpSeasonNum'); // 战报记录 - await PvpRecordModel.createRec({ roleId1: roleId, roleId2: robotIdComBack(oppRoleId), warId: BattleRecord.battleId, attackInfo, defenseInfo, createTime: nowSeconds(), timeout: seasonNum != pvpSeasonNum }); + await PvpRecordModel.createRec({ roleId1: roleId, roleId2: robotIdComBack(oppRoleId), battleCode, warId: BattleRecord.battleId, attackInfo, defenseInfo, createTime: nowSeconds(), timeout: seasonNum != pvpSeasonNum }); // 更新battleRecord await BattleRecordModel.updateBattleRecordByCode(battleCode, { @@ -600,7 +602,14 @@ export class PvpHandler { if(record.attackInfo) record.attackInfo['serverName'] = serverNames[record.attackInfo.serverId]; } - return resResult(STATUS.SUCCESS, { list: pvpRecords }); + return resResult(STATUS.SUCCESS, { + list: pvpRecords.map(rec => { + const { roleId1, battleCode, hasRpl } = rec; + const rplFileUrl = battleCode && hasRpl ? getRemoteRplFilePath(roleId1, WAR_TYPE.PVP, battleCode) : ''; + return { ...rec, rplFileUrl }; + }), + rplPrefixUrl: getRemoteRplPrefix(pinus.app.get('env')) + }); } // debug接口 diff --git a/shared/consts/statusCode.ts b/shared/consts/statusCode.ts index d6b756c61..fe5555b2a 100644 --- a/shared/consts/statusCode.ts +++ b/shared/consts/statusCode.ts @@ -62,6 +62,9 @@ export const STATUS = { BATTLE_END_WRONG_TYPE: { code: 20008, simStr: '此类型无法使用通用结算' }, BATTLE_GOLD_NOT_ENOUGH: { code: 20009, simStr: '元宝不足' }, BATTLE_REGRET_MAX: { code: 20010, simStr: '悔棋步数达上限' }, + BATTLE_NOT_FOUND: { code: 20011, simStr: '未找到对应关卡'}, + BATTLE_RPL_UPDATE_ERR: { code: 20012, simStr: '录像状态更新失败'}, + BATTLE_RPL_NOT_SUPPORT: { code: 20013, simStr: '暂不支持保存此类战斗的录像'}, // 主线 20100 - 20199 BATTLE_INFO_VALIDATE_ERR: { code: 20101, simStr: '关卡信息不同' }, diff --git a/shared/db/LadderMatchRec.ts b/shared/db/LadderMatchRec.ts index 6f01d4f1f..e41a291f9 100644 --- a/shared/db/LadderMatchRec.ts +++ b/shared/db/LadderMatchRec.ts @@ -33,6 +33,9 @@ export default class LadderMatchRec extends BaseModel { @prop({ required: true, type: () => LadderDefense, default: {}, _id: false }) defense: LadderDefense; // 守方信息 + @prop({ required: true, default: false }) + hasRpl: boolean; // 是否存在对应录像 + public static async findByRoleId(roleId: string, getters = false) { const result: LadderMatchRecType = await LadderMatchRecModel.findOne({ roleId1: roleId }).lean({ getters}); return result; @@ -94,7 +97,7 @@ export default class LadderMatchRec extends BaseModel { let recs: LadderMatchRecType[] = await LadderMatchRecModel.find({ $or: [{roleId1: roleId}, { roleId2: roleId }], status: LADDER_STATUS.COMPLETE - }).select({ battleCode: 1, roleId1: 1, roleId2: 1, _id: -1, endTime: 1, attackInfo: 1, defenseInfo: 1 }).limit(1000).lean(); + }).select({ battleCode: 1, roleId1: 1, roleId2: 1, _id: -1, endTime: 1, attackInfo: 1, defenseInfo: 1, hasRpl: 1 }).limit(1000).lean(); return recs; } @@ -120,6 +123,11 @@ export default class LadderMatchRec extends BaseModel { }, { $set: { status: LADDER_STATUS.COMPLETE, timeout: true, endTime: Date.now() } }); } + + public static async updateRplStatus(battleCode: string, hasRpl: boolean) { + let result = await LadderMatchRecModel.findOneAndUpdate({ battleCode }, { hasRpl }, { new: true }).lean(); + return result; + } } export const LadderMatchRecModel = getModelForClass(LadderMatchRec); diff --git a/shared/db/PvpRecord.ts b/shared/db/PvpRecord.ts index 2809c97c8..f2f4cf040 100644 --- a/shared/db/PvpRecord.ts +++ b/shared/db/PvpRecord.ts @@ -114,6 +114,7 @@ export class PvpRecordPlayerInfo { } @index({ code: 1 }) +@index({ battleCode: 1 }) @index({ roleId1: 1, updatedAt: -1 }) @index({ roleId2: 1, updatedAt: -1 }) @@ -123,6 +124,8 @@ export default class PvpRecord extends BaseModel { @prop({ required: true }) roleId2: string; // 角色 id + @prop({ required: false, default: '' }) + battleCode: string; @prop({ required: true, default: 0 }) code: string; // code @prop({ required: true, default: 0 }) @@ -132,12 +135,15 @@ export default class PvpRecord extends BaseModel { @prop({ required: true, type: PvpRecordPlayerInfo, default: {}, _id: false }) defenseInfo: PvpRecordPlayerInfo; // 守方信息 + @prop({ required: true, default: false }) + hasRpl: boolean; // 是否存在对应录像 + @prop({ required: true, default: 0 }) createTime: number; @prop({ required: true, default: 0 }) timeout: boolean; - public static async createRec(param: { roleId1: string, roleId2: string, warId: number, attackInfo: PvpRecordPlayerInfo, defenseInfo: PvpRecordPlayerInfo, createTime: number, timeout?: boolean }) { + public static async createRec(param: { roleId1: string, roleId2: string, battleCode: string, warId: number, attackInfo: PvpRecordPlayerInfo, defenseInfo: PvpRecordPlayerInfo, createTime: number, timeout?: boolean }) { await this.delPvpRecords(); let code = genCode(6); const result = await PvpRecordModel.findOneAndUpdate({ code }, param, { new: true, upsert: true }).lean(); @@ -155,6 +161,11 @@ export default class PvpRecord extends BaseModel { let result = await PvpRecordModel.deleteMany({ createTime: {$lt: t}});//删除小于三天的战报 return result; } + + public static async updateRplStatus(battleCode: string, hasRpl: boolean) { + let result = await PvpRecordModel.findOneAndUpdate({ battleCode }, { hasRpl }, { new: true }).lean(); + return result; + } } export const PvpRecordModel = getModelForClass(PvpRecord); diff --git a/shared/pubUtils/battleUtils.ts b/shared/pubUtils/battleUtils.ts new file mode 100644 index 000000000..c01034948 --- /dev/null +++ b/shared/pubUtils/battleUtils.ts @@ -0,0 +1,49 @@ +import * as crc from 'crc'; +import { md5 } from './sdkUtil'; + +const BATTLE_CLASS_MOD = 100; // 存档分类模数 +const CDN_URL_PREFIX_SQ = 'https://download-sgzzyz.yev242.com'; // sq cdn 服务器地址前缀 +const CDN_URL_PREFIX_ZYZ = 'http://zyz-download.trgame.cn'; // 公司下载服务器地址前缀 + +// 将字符串 crc32 处理后取模,以将随机字符串分组 +function modStr(str: string, mod: number) { + if (typeof(str) !== 'string' || typeof(mod) !== 'number') return undefined; + return crc.crc32(str) % mod; +} + +function getPrefixByEnv(env: string) { + switch (env) { + case 'dev': + case 'monitor': + case 'alpha': + case 'stable': + return CDN_URL_PREFIX_ZYZ; + case 'sq1': + case 'sq4': + default: + return CDN_URL_PREFIX_SQ; + } +} + +export function getLocalRplUrl(roleId: string, warType: number, battleCode: string) { + const battleClass = modStr(battleCode, BATTLE_CLASS_MOD); // 将存档文件按一定规则分批保存 + const writePath = `/zyz_logs/rpls/${roleId}/${warType}/${battleClass}`; + return writePath; +} + +export function getRemoteRplUrl(env: string, roleId: string, warType: number, battleCode: string) { + const battleClass = modStr(battleCode, BATTLE_CLASS_MOD); // 将存档文件按一定规则分批保存 + const rplUrl = `${getPrefixByEnv(env)}/rpls/${md5(env).substring(0, 4)}/${roleId}/${warType}/${battleClass}`; + return rplUrl; +} + +export function getRemoteRplPrefix(env: string) { + const rplUrl = `${getPrefixByEnv(env)}/rpls/${md5(env).substring(0, 4)}`; + return rplUrl; +} + +export function getRemoteRplFilePath(roleId: string, warType: number, battleCode: string) { + const battleClass = modStr(battleCode, BATTLE_CLASS_MOD); // 将存档文件按一定规则分批保存 + const rplUrl = `/${roleId}/${warType}/${battleClass}/${battleCode}.bin`; + return rplUrl; +} diff --git a/web-server/app/controller/game.ts b/web-server/app/controller/game.ts index 07af2e100..ef94f31ef 100644 --- a/web-server/app/controller/game.ts +++ b/web-server/app/controller/game.ts @@ -1,5 +1,10 @@ -import { STATUS } from '@consts'; +import { UserModel } from '@db/User'; +import { LadderMatchRecModel } from '@db/LadderMatchRec'; +import { PvpRecordModel } from '@db/PvpRecord'; +import { BattleRecordModel } from '@db/BattleRecord'; +import { STATUS, WAR_TYPE } from '@consts'; import { Controller } from 'egg'; +import * as fs from 'fs'; import { RoleModel } from '@db/Role'; import { NoticeModel } from '@db/Notice'; import { ServerParamWithRole, GroupParam } from '../domain/gameField/serverlist'; @@ -10,7 +15,10 @@ import { RedisClient } from 'redis'; import { REDIS_KEY } from '@consts'; import { RegionModel } from '@db/Region'; import { getRandEelmWithWeight } from 'app/pubUtils/util'; +import { getLocalRplUrl, getRemoteRplUrl } from 'app/pubUtils/battleUtils' import { ChannelInfoModel } from '@db/ChannelInfo'; +const sendToWormhole = require('stream-wormhole'); +const pump = require('mz-modules/pump'); export default class GameController extends Controller { @@ -178,4 +186,75 @@ export default class GameController extends Controller { ctx.body = ctx.service.utils.resResult(STATUS.SUCCESS, { host: res.clientHost, port: res.clientPort }); return } + + public async upload() { + const { ctx } = this; + const parts = ctx.multipart(); + let part; + let [writePath, token, battleCode, fullPath, remoteUrl] = ['', '', '', '', '']; + while ((part = await parts()) != null) { + if (part.length) { + console.log('kv: ', `${part[0]}: ${part[1]}`); + if (part[0] === 'token') { + token = part[1]; + } else if (part[0] === 'battleCode') { + battleCode = part[1]; + } + } else { + if (!part.filename) { + continue; + } + console.log(part); + if (part.fieldname === 'rpl' && battleCode !== '') { + console.log('field: ', part.fieldname); + console.log('filename: ', part.filename); + if (token === '') { + ctx.body = ctx.service.utils.resResult(STATUS.WRONG_PARMS); + return; + } + const user = await UserModel.findUserByToken(token); + if (!user) { + console.error('token invalid'); + ctx.body = ctx.service.utils.resResult(STATUS.TOKEN_ERR); + return; + } + + let battleRec = await BattleRecordModel.getBattleRecordByCode(battleCode, true); + if (!battleRec) return ctx.body = ctx.service.utils.resResult(STATUS.BATTLE_NOT_FOUND); + + let { warType, roleId } = battleRec; + if (warType !== WAR_TYPE.PVP && warType !== WAR_TYPE.LADDER) return ctx.body = ctx.service.utils.resResult(STATUS.BATTLE_RPL_NOT_SUPPORT); + + writePath = getLocalRplUrl(roleId, warType, battleCode); + try { + fs.accessSync(writePath); + } catch (err) { + if (err) { + fs.mkdirSync(writePath, { recursive: true }); + } + } + fullPath = `${writePath}/${battleCode}.bin` + console.log(fullPath); + if (!fs.existsSync(fullPath)) { + fs.writeFileSync(fullPath, ''); + } + const writeStream = fs.createWriteStream(fullPath); + await pump(part, writeStream); + + let updateDBRes; + if (warType === WAR_TYPE.PVP) { + updateDBRes = await PvpRecordModel.updateRplStatus(battleCode, true); + } else if (warType === WAR_TYPE.LADDER) { + updateDBRes = await LadderMatchRecModel.updateRplStatus(battleCode, true); + } + if (!updateDBRes) return ctx.body = ctx.service.utils.resResult(STATUS.BATTLE_RPL_UPDATE_ERR); + remoteUrl = `${getRemoteRplUrl(ctx.app.config.realEnv, roleId, warType, battleCode)}/${battleCode}.bin`; + } else { + await sendToWormhole(part); + } + } + } + ctx.body = ctx.service.utils.resResult(STATUS.SUCCESS, { rplUrl: remoteUrl }); + return; + } } diff --git a/web-server/app/router.ts b/web-server/app/router.ts index 3fa3d76aa..725f9fda6 100644 --- a/web-server/app/router.ts +++ b/web-server/app/router.ts @@ -29,6 +29,7 @@ export default (app: Application) => { router.post('/update/getversion', controller.update.getversion); router.post('/update/getupdateurl', controller.update.getUpdateUrl); router.post('/web/reloadresource', app.middleware.gmTokenParser(), controller.game.reloadResource); + router.post('/web/upload', controller.game.upload); // sdk 回调 diff --git a/web-server/config/config.default.ts b/web-server/config/config.default.ts index f8964e79f..f68c8977a 100644 --- a/web-server/config/config.default.ts +++ b/web-server/config/config.default.ts @@ -55,6 +55,11 @@ export default (appInfo: EggAppInfo) => { dir: path.join(appInfo.baseDir, '/app/public'), }; + config.multipart = { + fileSize: '10mb', + fileExtensions: ['.bin'], // 支持上传 bin 类型文件 + }; + config.customLogger = { linkLogger: { file: path.join(appInfo.root, 'logs/web-server/link-log.log'), diff --git a/web-server/package.json b/web-server/package.json index 01eb66fe6..fa5c2ac96 100644 --- a/web-server/package.json +++ b/web-server/package.json @@ -49,12 +49,13 @@ "reflect-metadata": "^0.1.13", "request": "^2.88.2", "request-promise": "^4.2.6", + "stream-to-array": "^2.3.0", "thinkingdata-node": "^1.2.2", "underscore": "^1.13.1" }, "devDependencies": { "@types/mocha": "^2.2.40", - "@types/node": "^7.0.12", + "@types/node": "^10.0.0", "@types/redis": "^2.8.31", "@types/request-promise": "^4.1.47", "@types/supertest": "^2.0.0", @@ -69,7 +70,7 @@ "typescript": "^3.0.0" }, "engines": { - "node": ">=8.9.0" + "node": ">=10.20.1" }, "ci": { "version": "8"