✨ feat(battle): 添加录像上传接口
This commit is contained in:
@@ -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接口
|
||||
|
||||
@@ -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接口
|
||||
|
||||
@@ -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: '关卡信息不同' },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
49
shared/pubUtils/battleUtils.ts
Normal file
49
shared/pubUtils/battleUtils.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 回调
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user