const mongoose = require('mongoose'); const { Schema } = mongoose; const gameSchema = new Schema({ leagueId: { type: Number, required: true }, eventId: { type: Number, required: true }, leagueName: { type: String, required: true }, teamHomeName: { type: String, required: true }, teamAwayName: { type: String, required: true }, timestamp: { type: Number, required: true }, matchNumStr: { type: String, required: false } }, { _id: false }); const relSchema = new Schema({ jc: { type: gameSchema }, ps: { type: gameSchema }, ob: { type: gameSchema }, }, { _id: false }); const relationSchema = new Schema({ id: { type: Number, required: true }, rel: { type: relSchema, required: true }, }, { toJSON: { transform(doc, ret) { delete ret._id; delete ret.__v; } }, toObject: { transform(doc, ret) { delete ret._id; delete ret.__v; } } }); const Relation = mongoose.model('Relation', relationSchema); const childOptions = process.env.NODE_ENV == 'development' ? { execArgv: ['--inspect=9228'] } : {}; // const { fork } = require('child_process'); // const events_child = fork('./triangle/eventsMatch.js', [], childOptions); const axios = require('axios'); const calcTotalProfit = require('../triangle/totalProfitCalc'); const Logs = require('../libs/logs'); const Setting = require('./Setting'); const Request = { callbacks: {}, count: 0, } const GAMES = { Leagues: {}, List: {}, Relations: {}, Solutions: {}, }; /** * 精确浮点数字 * @param {number} number * @param {number} x * @returns {number} */ const fixFloat = (number, x=2) => { return parseFloat(number.toFixed(x)); } /** * 更新联赛列表 */ const syncLeaguesList = ({ mk, leagues }) => { axios.post('https://api.isthe.me/api/p/syncLeague', { mk, leagues }) .then(res => { Logs.out('syncLeaguesList', res.data); }) .catch(err => { Logs.out('syncLeaguesList', err.message); }); } const updateLeaguesList = ({ mk, leagues }) => { const leaguesList = GAMES.Leagues; if (JSON.stringify(leaguesList[mk]) != JSON.stringify(leagues)) { leaguesList[mk] = leagues; syncLeaguesList({ mk, leagues }); return leagues.length; } return 0; } /** * 获取筛选过的联赛 */ const getFilteredLeagues = async (mk) => { return axios.get(`https://api.isthe.me/api/p/getLeagueTast?mk=${mk}`) .then(res => { if (res.data.code == 0) { return res.data.data; } return Promise.reject(new Error(res.data.message)); }); } /** * 更新比赛列表 */ const syncGamesList = ({ platform, mk, games }) => { axios.post('https://api.isthe.me/api/p/syncGames', { platform, mk, games }) .then(res => { Logs.out('syncGamesList', res.data); }) .catch(err => { Logs.out('syncGamesList', err.message); }); } const updateGamesList = (({ platform, mk, games } = {}) => { syncGamesList({ platform, mk, games }); return new Promise((resolve, reject) => { if (!platform || !games) { return reject(new Error('PLATFORM_GAMES_INVALID')); } const marketType = mk == 0 ? 'early' : 'today'; let gamesList = games; if (platform == 'jc') { gamesList = []; const gamesEvents = []; const gamesOutrights = []; games.forEach(game => { const { eventId, events, evtime, special, sptime, ...gameInfo } = game; gamesList.push({ eventId, ...gameInfo }); gamesEvents.push({ eventId, events, evtime }); gamesOutrights.push({ parentId: eventId, special, sptime }); }); updateGamesEvents({ platform, games: gamesEvents, outrights: gamesOutrights }); } const timestamp = Date.now(); const GAMES_LIST = GAMES.List; if (!GAMES_LIST[platform]) { GAMES_LIST[platform] = {}; } if (!GAMES_LIST[platform][marketType]) { GAMES_LIST[platform][marketType] = { games: gamesList, timestamp }; return resolve({ add: gamesList.length, del: 0 }); } const oldGames = GAMES_LIST[platform][marketType].games; const newGames = gamesList; const updateCount = { add: 0, del: 0, }; const newMap = new Map(newGames.map(item => [item.eventId, item])); for (let i = oldGames.length - 1; i >= 0; i--) { if (!newMap.has(oldGames[i].eventId)) { oldGames.splice(i, 1); updateCount.del += 1; } } const oldIds = new Set(oldGames.map(item => item.eventId)); const relatedGames = Object.values(GAMES.Relations).map(rel => rel[platform] ?? {}); const relatedMap = new Map(relatedGames.map(item => [item.eventId, item])); newGames.forEach(item => { if (!oldIds.has(item.eventId)) { oldGames.push(item); updateCount.add += 1; } if (relatedMap.has(item.eventId)) { const relatedGame = relatedMap.get(item.eventId); relatedGame.mk = mk; } }); GAMES_LIST[platform][marketType].timestamp = timestamp; resolve(updateCount); }); }); /** * 更新比赛盘口 */ const updateGamesEvents = ({ platform, mk, games, outrights }) => { return new Promise((resolve, reject) => { if (!platform || (!games && !outrights)) { return reject(new Error('PLATFORM_GAMES_INVALID')); } const relatedGames = Object.values(GAMES.Relations).map(rel => rel[platform] ?? {}); if (!relatedGames.length) { return resolve({ update: 0 }); } const updateCount = { update: 0 }; const relatedMap = new Map(relatedGames.map(item => [item.eventId, item])); games?.forEach(game => { const { eventId, evtime, events } = game; const relatedGame = relatedMap.get(eventId); if (relatedGame) { relatedGame.evtime = evtime; relatedGame.events = events; updateCount.update ++; } }); outrights?.forEach(outright => { const { parentId, sptime, special } = outright; const relatedGame = relatedMap.get(parentId); if (relatedGame) { relatedGame.sptime = sptime; relatedGame.special = special; updateCount.update ++; } }); resolve(updateCount); }); } /** * 获取比赛列表 */ const getGamesList = () => { const gamesListMap = {}; Object.keys(GAMES.List).forEach(platform => { const { today, early } = GAMES.List[platform]; const todayList = today?.games ?? []; const earlyList = early?.games ?? []; const timestamp_today = today?.timestamp ?? 0; const timestamp_early = early?.timestamp ?? 0; const timestamp = Math.max(timestamp_today, timestamp_early); gamesListMap[platform] = { games: [...todayList, ...earlyList], timestamp, timestamp_today, timestamp_early, } }); return gamesListMap; } /** * 获取比赛盘口 */ const getGamesEvents = ({ platform, relIds = [] } = {}) => { const idSet = new Set(relIds); const relations = { ...GAMES.Relations }; Object.keys(relations).forEach(id => { if (idSet.size && !idSet.has(id)) { delete relations[id]; } }); if (platform) { return Object.values(relations).map(rel => rel[platform] ?? {}); } const gamesEvents = {}; Object.values(relations).forEach(rel => { Object.keys(rel).forEach(platform => { const game = rel[platform] ?? {}; const { eventId, events, special } = game; if (!gamesEvents[platform]) { gamesEvents[platform] = {}; } gamesEvents[platform][eventId] = { ...events, ...special }; }); }); return gamesEvents; } /** * 更新关联比赛 */ const updateGamesRelation = async (relation) => { const { id, rel } = relation; return Relation.findOne({ id }) .then(result => { if (!result) { const gameRelation = new Relation(relation); return gameRelation.save(); } return Relation.updateOne({ id }, { $set: { rel } }); }) .then(result => { GAMES.Relations[id] = rel; return result; }); } /** * 删除关联比赛 */ const removeGamesRelation = async (id) => { if (!id) { return Promise.reject(new Error('ID_INVALID')); } return Relation.deleteOne({ id }) .then(result => { delete GAMES.Relations[id]; return result; }); } /** * 获取关联比赛 */ const getGamesRelation = (listEvents) => { const relationIds = Object.keys(GAMES.Relations); if (listEvents) { return relationIds.map(id => { const rel = GAMES.Relations[id]; return { id, rel }; }); } return relationIds.map(id => { const rel = { ...GAMES.Relations[id] }; Object.keys(rel).forEach(platform => { const game = { ...rel[platform] }; delete game.events; delete game.evtime; delete game.special; delete game.sptime; rel[platform] = game; }); return { id, rel }; }); } /** * 清理关联比赛 */ const relationsCleanup = () => { const expireTime = Date.now() - 1000*60*5; const gamesRelation = getGamesRelation(); gamesRelation.forEach(item => { const { id, rel } = item; const expire = Object.values(rel).find(event => { return event.timestamp <= expireTime; }); if (expire) { Logs.out('relation cleanup', id); removeGamesRelation(id); } return true; }); } /** * 从数据库中同步关联比赛 */ const relationSync = () => { Relation.find().then(relation => relation.forEach(item => { const { id, rel } = item.toObject(); GAMES.Relations[id] = rel; })); } /** * 更新中单方案 */ const updateSolutions = (solutions) => { if (solutions?.length) { const solutionsHistory = GAMES.Solutions; const updateIds = { add: [], update: [] } solutions.forEach(item => { const { sid, sol: { win_average } } = item; if (!solutionsHistory[sid]) { solutionsHistory[sid] = item; updateIds.add.push({ sid, win_average }); return; } const historyWinAverage = solutionsHistory[sid].sol.win_average; if (win_average != historyWinAverage) { solutionsHistory[sid] = item; updateIds.update.push({ sid, win_average, his_average: historyWinAverage, diff: fixFloat(win_average - historyWinAverage) }); return; } const { timestamp } = item; solutionsHistory[sid].timestamp = timestamp; }); // if (updateIds.add.length || updateIds.update.length) { // const solutionsList = Object.values(solutionsHistory).sort((a, b) => b.sol.win_average - a.sol.win_average); // Logs.outDev('solutions history update', JSON.stringify(solutionsList, null, 2), JSON.stringify(updateIds, null, 2)); // } } } /** * 获取中单方案 */ const getSolutions = async () => { const solutionsList = Object.values(GAMES.Solutions); const relIds = solutionsList.map(item => item.info.id); const gamesEvents = getGamesEvents({ relIds }); const gamesRelations = getGamesRelation(); const relationsMap = new Map(gamesRelations.map(item => [item.id, item.rel])); const solutions = solutionsList.sort((a, b) => b.sol.win_average - a.sol.win_average).map(item => { const { info: { id } } = item; const relation = relationsMap.get(id); return { ...item, info: { id, ...relation } } }); return { solutions, gamesEvents }; } /** * 清理中单方案 */ const solutionsCleanup = () => { const solutionsHistory = GAMES.Solutions; Object.keys(solutionsHistory).forEach(sid => { const { timestamp } = solutionsHistory[sid]; const nowTime = Date.now(); if (nowTime - timestamp > 1000*60) { delete solutionsHistory[sid]; Logs.out('solution history timeout', sid); return; } const solution = solutionsHistory[sid]; const eventTime = solution.info.timestamp; if (nowTime > eventTime) { delete solutionsHistory[sid]; Logs.out('solution history expired', sid); } }); } const getTotalProfit = (sid1, sid2, gold_side_inner) => { const preSolution = GAMES.Solutions[sid1]; const subSolution = GAMES.Solutions[sid2]; const relId1 = preSolution?.info?.id; const relId2 = subSolution?.info?.id; const relIds = [relId1, relId2]; const gamesEvents = getGamesEvents({ relIds }); const gamesRelations = getGamesRelation(); const relationsMap = new Map(gamesRelations.map(item => [item.id, item.rel])); const preRelation = relationsMap.get(relId1); const subRelation = relationsMap.get(relId2); preSolution.info = { id: relId1, ...preRelation }; subSolution.info = { id: relId2, ...subRelation }; const sol1 = preSolution?.sol; const sol2 = subSolution?.sol; if (!sol1 || !sol2 || !gold_side_inner) { return {}; } const profit = calcTotalProfit(sol1, sol2, gold_side_inner); return { profit, preSolution, subSolution, gamesEvents }; } // const getSetting = async () => { // return Setting.get(); // } // const getDataFromChild = (type, callback) => { // const id = ++Request.count; // Request.callbacks[id] = callback; // events_child.send({ method: 'get', id, type }); // } // events_child.on('message', async (message) => { // const { callbacks } = Request; // const { method, id, type, data } = message; // if (method == 'get' && id) { // let responseData = null; // if (type == 'getGamesRelation') { // responseData = getGamesRelation(true); // } // else if (type == 'getSetting') { // responseData = await getSetting(); // } // // else if (type == 'getSolutionHistory') { // // responseData = getSolutionHistory(); // // } // events_child.send({ type: 'response', id, data: responseData }); // } // else if (method == 'post') { // if (type == 'updateSolutions') { // updateSolutions(data); // } // } // else if (method == 'response' && id && callbacks[id]) { // callbacks[id](data); // delete callbacks[id]; // } // }); relationSync(); setInterval(() => { relationsCleanup(); }, 5000); setInterval(() => { solutionsCleanup(); }, 1000*30); module.exports = { updateLeaguesList, getFilteredLeagues, updateGamesList, updateGamesEvents, getGamesList, updateGamesRelation, getGamesRelation, removeGamesRelation, getGamesEvents, getSolutions, getTotalProfit, }