const axios = require('axios'); const Logs = require('../libs/logs'); const Cache = require('../libs/cache'); const Setting = require('./Setting'); const { eventSolutions } = require('../triangle/eventSolutions'); const { calcTotalProfit, calcTotalProfitWithFixedFirst } = require('../triangle/totalProfitCalc'); const fs = require('fs'); const path = require('path'); const GamesCacheFile = path.join(__dirname, '../data/games.cache'); const childOptions = process.env.NODE_ENV == 'development' ? { execArgv: ['--inspect=9228'], stdio: ['pipe', 'pipe', 'pipe', 'ipc'] } : {}; const { fork } = require('child_process'); const events_child = fork('./triangle/eventsMatch.js', [], childOptions); const PS_IOR_KEYS = [ ['0', 'ior_mh', 'ior_mn', 'ior_mc'], // ['0', 'ior_rh_05', 'ior_mn', 'ior_rc_05'], ['-1', 'ior_rh_15', 'ior_wmh_1', 'ior_rac_05'], ['-2', 'ior_rh_25', 'ior_wmh_2', 'ior_rac_15'], ['+1', 'ior_rah_05', 'ior_wmc_1', 'ior_rc_15'], ['+2', 'ior_rah_15', 'ior_wmc_2', 'ior_rc_25'], // ['0-1', 'ior_ot_0', 'ior_os_0-1', 'ior_ot_1'], ['2-3', 'ior_ot_2', 'ior_os_2-3', 'ior_ot_3'], ]; const BASE_URL = 'https://api.czxd8.com/api/p'; const IS_DEV = process.env.NODE_ENV == 'development'; const GAMES = { Leagues: {}, Baselist: {}, Relations: {}, Solutions: {}, }; const Request = { callbacks: {}, count: 0, } /** * 精确浮点数字 * @param {number} number * @param {number} x * @returns {number} */ const fixFloat = (number, x=2) => { return parseFloat(number.toFixed(x)); } /** * 获取市场类型 */ const getMarketType = (mk) => { return mk == 0 ? 'early' : 'today'; } /** * 同步联赛列表 */ const syncLeaguesList = ({ mk, leagues }) => { if (IS_DEV) { return Logs.out('syncLeaguesList', { mk, leagues }); } axios.post(`${BASE_URL}/syncLeague`, { mk, leagues }) .then(res => { // Logs.out('syncLeaguesList', res.data); }) .catch(err => { Logs.out('syncLeaguesList', err.message); }); } /** * 更新联赛列表 */ const updateLeaguesList = ({ mk, leagues }) => { const leaguesMap = GAMES.Leagues; const nowTime = Date.now(); const expireTime = nowTime - 1000 * 60 * 5; if (!leaguesMap[mk]) { leaguesMap[mk] = { timestamp: 0, leagues: [], }; } if (leaguesMap[mk].timestamp < expireTime || JSON.stringify(leaguesMap[mk].leagues) != JSON.stringify(leagues)) { leaguesMap[mk].leagues = leagues; leaguesMap[mk].timestamp = nowTime; syncLeaguesList({ mk, leagues }); return leagues.length; } return 0; } /** * 获取筛选过的联赛 */ const getFilteredLeagues = async (mk) => { return axios.get(`${BASE_URL}/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 }) => { if (IS_DEV) { return Logs.out('syncGamesList', { platform, mk, games }); } axios.post(`${BASE_URL}/syncGames`, { platform, mk, games }) .then(res => { // Logs.out('syncGamesList', { platform, mk, count: games.length }, res.data); }) .catch(err => { Logs.out('syncGamesList', { platform, mk }, err.message); }); } /** * 同步基准比赛列表 */ const syncBaseList = ({ marketType, games }) => { const baseList = GAMES.Baselist; if (!baseList[marketType]) { baseList[marketType] = games; } const newMap = new Map(games.map(item => [item.eventId, item])); // 删除不存在的项 for (let i = baseList[marketType].length - 1; i >= 0; i--) { if (!newMap.has(baseList[marketType][i].eventId)) { baseList[marketType].splice(i, 1); } } // 添加或更新 const oldIds = new Set(baseList[marketType].map(item => item.eventId)); games.forEach(game => { if (!oldIds.has(game.eventId)) { // 添加新项 baseList[marketType].push(game); } }); } /** * 更新比赛列表 */ const updateGamesList = (({ platform, mk, games } = {}) => { return new Promise((resolve, reject) => { if (!platform || !games) { return reject(new Error('PLATFORM_GAMES_INVALID')); } const marketType = getMarketType(mk); syncGamesList({ platform, mk, games }); if (platform == 'ps') { syncBaseList({ marketType, games }); } resolve(); }); }); /** * 提交盘口数据 */ const submitOdds = ({ platform, mk, games }) => { if (IS_DEV) { return Logs.out('syncOdds', { platform, mk, games }); } axios.post(`${BASE_URL}/syncOdds`, { platform, mk, games}) .then(res => { // Logs.out('syncOdds', { platform, mk, count: games.length }, res.data); }) .catch(err => { Logs.out('syncOdds', { platform, mk }, err.message); }); } /** * 同步基准盘口 */ const syncBaseEvents = ({ mk, games, outrights }) => { const marketType = getMarketType(mk); const baseList = GAMES.Baselist; if (!baseList[marketType]) { return; } const baseMap = new Map(baseList[marketType].map(item => [item.eventId, item])); games?.forEach(game => { const { eventId, evtime, events } = game; const baseGame = baseMap.get(eventId); if (baseGame) { baseGame.evtime = evtime; baseGame.events = events; } }); outrights?.forEach(outright => { const { parentId, sptime, special } = outright; const baseGame = baseMap.get(parentId); if (baseGame) { baseGame.sptime = sptime; baseGame.special = special; } }); if (games?.length) { const gamesList = baseList[marketType]?.map(game => { const { evtime, events, sptime, special, ...gameInfo } = game; const expireTimeEv = Date.now() - 15000; const expireTimeSP = Date.now() - 30000; let odds = {}; if (evtime > expireTimeEv) { odds = { ...odds, ...events }; } if (sptime > expireTimeSP) { odds = { ...odds, ...special }; } const matches = PS_IOR_KEYS.map(([label, ...keys]) => { const match = keys.map(key => { if (key.includes('os') && !odds[key]) { return { key, value: 1 }; } else { return { key, value: odds[key] ?? 0 } } }); return { label, match }; }).filter(item => item.match.every(entry => entry.value !== 0)); return { ...gameInfo, matches, uptime: Math.min(evtime ?? 0, sptime ?? 0) }; }); if (gamesList.filter(item => item.uptime > 0).length) { submitOdds({ platform: 'ps', mk, games: gamesList }); } const relatedGames = Object.values(GAMES.Relations).map(item => item.rel?.['ps'] ?? {}); if (!relatedGames.length) { return 0; } let update = 0; const relatedMap = new Map(relatedGames.map(item => [item.eventId, item])); gamesList?.forEach(game => { const { eventId, matches, uptime } = game; const relatedGame = relatedMap.get(eventId); if (relatedGame) { const events = {}; matches.forEach(({ label, match }) => { match.forEach(({ key, value }) => { events[key] = value; }); }); relatedGame.evtime = uptime; relatedGame.events = events; update ++; } }); return update; } } const updateGamesEvents = ({ platform, mk, games, outrights }) => { return new Promise((resolve, reject) => { if (!platform || (!games && !outrights)) { return reject(new Error('PLATFORM_GAMES_INVALID')); } if (platform == 'ps') { const update = syncBaseEvents({ mk, games, outrights }); return resolve({ update }); } const relatedGames = Object.values(GAMES.Relations).map(item => item.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 getGamesEvents = ({ platform, relIds = [] } = {}) => { if (!relIds.length) { return null; } 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 fetchGamesRelation = async (mk='') => { return axios.get(`${BASE_URL}/getGameTast?mk=${mk}`) .then(res => { if (res.data.code == 0) { const now = Date.now(); const gamesRelation = res.data.data?.filter(item => { const timestamp = new Date(item.timestamp).getTime(); item.timestamp = timestamp; return timestamp > now; }).map(item => { const { id, mk, league_name, event_id: ps_event_id, league_id: ps_league_id, team_home_name: ps_team_home_name, team_away_name: ps_team_away_name, ob_event_id, ob_league_id, ob_team_home_name, ob_team_away_name, hg_event_id, hg_league_id, hg_team_home_name, hg_team_away_name, timestamp, } = item; const rel = { ps: { eventId: +ps_event_id, leagueId: +ps_league_id, leagueName: league_name, teamHomeName: ps_team_home_name, teamAwayName: ps_team_away_name, timestamp }, ob: ob_event_id ? { eventId: +ob_event_id, leagueId: +ob_league_id, leagueName: league_name, teamHomeName: ob_team_home_name, teamAwayName: ob_team_away_name, timestamp } : null, hg: hg_event_id ? { eventId: +hg_event_id, leagueId: +hg_league_id, leagueName: league_name, teamHomeName: hg_team_home_name, teamAwayName: hg_team_away_name, timestamp } : null }; return { id: ps_event_id, mk, rel }; }) ?? []; return gamesRelation; } return Promise.reject(new Error(res.data.message)); }); } const getGamesRelation = ({ mk, listEvents } = {}) => { const relations = Object.values(GAMES.Relations).filter(item => { if (typeof(mk) === 'undefined' || mk === '') { return true; } return item.mk == mk; }); if (listEvents) { return relations; } const gamesRelation = relations.map(item => { const { rel, ...relationInfo } = item; const tempRel = { ...rel }; Object.keys(tempRel).forEach(platform => { const { events, evtime, sptime, special, ...gameInfo } = tempRel[platform]; tempRel[platform] = gameInfo; }); return { ...relationInfo, rel: tempRel }; }); return gamesRelation; } /** * 定时更新关联比赛列表 */ const updateGamesRelation = () => { fetchGamesRelation() .then(res => { const gamesRelation = res.flat(); const updateCount = { add: 0, update: 0, delete: 0 }; gamesRelation.forEach(item => { const { id, mk } = item; const oldItem = GAMES.Relations[id]; if (!oldItem) { GAMES.Relations[id] = item; updateCount.add ++; } else if (oldItem.mk != mk) { GAMES.Relations[id] = item; updateCount.update ++; } }); const relations = new Set(gamesRelation.map(item => +item.id)); Object.keys(GAMES.Relations).forEach(id => { if (!relations.has(+id)) { delete GAMES.Relations[id]; updateCount.delete ++; } else { const { rel } = GAMES.Relations[id]; const relTime = rel.ps?.timestamp; if (relTime && relTime < Date.now()) { delete GAMES.Relations[id]; updateCount.delete ++; } } }); Logs.outDev('updateGamesRelation', updateCount); }) .catch(err => { Logs.out('updateGamesRelation', err.message); }) .finally(() => { setTimeout(updateGamesRelation, 60000); }); } updateGamesRelation(); const gamesRelationCleanup = () => { const relations = Object.values(GAMES.Relations); const expireTime = Date.now() - 1000*60; relations.forEach(item => { const { rel } = item; Object.keys(rel).forEach(platform => { const { evtime, sptime } = rel[platform]; if (evtime && evtime < expireTime) { delete rel[platform].events; delete rel[platform].evtime; } if (sptime && sptime < expireTime) { delete rel[platform].special; delete rel[platform].sptime; } }); }); } /** * 同步比赛结果 */ const syncGamesResult = async (result) => { if (IS_DEV) { return Logs.out('updateGamesResult', result); } axios.post(`${BASE_URL}/syncMatchResult`, result) .then(res => { // Logs.out('syncMatchResult', res.data); }) .catch(err => { Logs.out('syncMatchResult', err.message); }); } /** * 更新比赛结果 */ const updateGamesResult = (result) => { syncGamesResult(result); return Promise.resolve(); } /** * 同步中单方案 */ const syncSolutions = (solutions) => { if (IS_DEV) { return Logs.out('syncSolutions', solutions); } axios.post(`${BASE_URL}/syncDsOpportunity`, solutions) .then(res => { // Logs.out('syncSolutions', res.data); }) .catch(err => { Logs.out('syncSolutions', err.message); }); } /** * 更新中单方案 */ const getCprKey = (cpr) => { const { k, p, v } = cpr; return `${k}_${p}_${v}`; } const compareCpr = (cpr1, cpr2) => { const key1 = getCprKey(cpr1); const key2 = getCprKey(cpr2); return key1 === key2; } const updateSolutions = (solutions) => { if (solutions?.length) { const solutionsHistory = GAMES.Solutions; const updateIds = { add: [], update: [] } solutions.forEach(item => { const { sid, cpr, sol: { win_average } } = item; if (!solutionsHistory[sid]) { solutionsHistory[sid] = item; updateIds.add.push(sid); return; } const historySolution = solutionsHistory[sid]; if (historySolution.sol.win_average !== win_average || !compareCpr(historySolution.cpr, cpr)) { solutionsHistory[sid] = item; updateIds.update.push(sid); return; } const { timestamp } = item; solutionsHistory[sid].timestamp = timestamp; }); if (updateIds.add.length || updateIds.update.length) { const solutionUpdate = {}; Object.keys(updateIds).forEach(key => { solutionUpdate[key] = updateIds[key].map(sid => solutionsHistory[sid]); }); syncSolutions(solutionUpdate); // Logs.outDev('solutions history update', solutionUpdate); } } } /** * 获取中单方案 */ const getSolutions = async () => { const { minShowAmount } = await getSetting(); const solutionsList = Object.values(GAMES.Solutions); const gamesRelation = getGamesRelation(); const relationsMap = new Map(gamesRelation.map(item => [item.id, item.rel])); const solutions = solutionsList.sort((a, b) => b.sol.win_average - a.sol.win_average) .filter(item => { const { sol: { win_average } } = item; return win_average >= minShowAmount; }) .map(item => { const { info: { id } } = item; const relation = relationsMap.get(id); return { ...item, info: { id, ...relation } } }); const relIds = solutions.map(item => item.info.id); const gamesEvents = getGamesEvents({ relIds }); return { solutions, gamesEvents }; } /** * 清理中单方案 */ const solutionsCleanup = () => { const solutionsHistory = GAMES.Solutions; const updateIds = { remove: [] } Object.keys(solutionsHistory).forEach(sid => { const { timestamp } = solutionsHistory[sid]; const nowTime = Date.now(); if (nowTime - timestamp > 1000*60) { delete solutionsHistory[sid]; updateIds.remove.push(sid); return; } const solution = solutionsHistory[sid]; const eventTime = solution.info.timestamp; if (nowTime > eventTime) { delete solutionsHistory[sid]; updateIds.remove.push(sid); } }); if (updateIds.remove.length) { syncSolutions(updateIds); } } /** * 定时清理中单方案 * 定时清理盘口信息 */ setInterval(() => { solutionsCleanup(); gamesRelationCleanup(); }, 1000*30); /** * 获取综合利润 */ const getTotalProfit = async (sol1, sol2, inner_base, inner_rebate) => { const { innerDefaultAmount, innerRebateRatio } = await getSetting(); inner_base = inner_base ? +inner_base : innerDefaultAmount; inner_rebate = inner_rebate ? +inner_rebate : fixFloat(innerRebateRatio / 100, 3); const profit = calcTotalProfit(sol1, sol2, inner_base, inner_rebate); return profit; } /** * 通过 sid 获取综合利润 */ const getTotalProfitWithSid = async (sid1, sid2, inner_base, inner_rebate) => { const preSolution = GAMES.Solutions[sid1]; const subSolution = GAMES.Solutions[sid2]; const sol1 = preSolution?.sol; const sol2 = subSolution?.sol; if (!sol1) { return Promise.reject(new Error('sid1 已失效')); } if (!sol2) { return Promise.reject(new Error('sid2 已失效')); } const profit = await getTotalProfit(sol1, sol2, inner_base, inner_rebate); return { profit, solutions: [preSolution, subSolution] }; } /** * 通过盘口信息获取综合利润 */ const getTotalProfitWithBetInfo = async (betInfo1, betInfo2, fixed=false, inner_base, inner_rebate) => { const { innerDefaultAmount, innerRebateRatio } = await getSetting(); inner_base = inner_base ? +inner_base : innerDefaultAmount; inner_rebate = inner_rebate ? +inner_rebate : fixFloat(innerRebateRatio / 100, 3); if (fixed) { return calcTotalProfitWithFixedFirst(betInfo1, betInfo2, inner_base, inner_rebate); } const [sol1, sol2] = [betInfo1, betInfo2].map(betinfo => eventSolutions({...betinfo, inner_base, inner_rebate })); return getTotalProfit(sol1, sol2, inner_base, inner_rebate); } /** * 获取后台设置 */ 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 }); } /** * 向子进程发送数据 */ const postDataToChild = (type, data) => { events_child.send({ method: 'post', type, data }); } /** * 处理子进程消息 */ 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({ listEvents: 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]; } }); events_child.stderr?.on('data', data => { Logs.out('events_child stderr', data.toString()); }); Setting.onUpdate(fields => { postDataToChild('updateSetting', fields); }); /** * 保存GAMES数据到缓存文件 */ const saveGamesToCache = () => { Cache.setData(GamesCacheFile, GAMES, err => { if (err) { Logs.out('Failed to save games cache:', err.message); } else { Logs.out('Games cache saved successfully'); } }); } /** * 从缓存文件加载GAMES数据 */ const loadGamesFromCache = () => { const gamesCacheData = Cache.getData(GamesCacheFile, true); Object.assign(GAMES, gamesCacheData); Logs.out('Games cache loaded successfully'); } // 在模块加载时尝试从缓存恢复数据 loadGamesFromCache(); // 监听进程退出事件,保存GAMES数据 process.on('exit', saveGamesToCache); process.on('SIGINT', () => { process.exit(0); }); process.on('SIGTERM', () => { process.exit(0); }); process.on('SIGUSR2', () => { process.exit(0); }); module.exports = { updateLeaguesList, getFilteredLeagues, updateGamesList, updateGamesEvents, getGamesRelation, updateGamesResult, getSolutions, getTotalProfitWithSid, getTotalProfitWithBetInfo, }