GamesPs.js 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319
  1. const axios = require('axios');
  2. const Logs = require('../libs/logs');
  3. const Cache = require('../libs/cache');
  4. const Setting = require('./Setting');
  5. const { eventSolutions } = require('../triangle/eventSolutions');
  6. const { calcTotalProfit, calcTotalProfitWithFixedFirst } = require('../triangle/totalProfitCalc');
  7. const { getSetting, updateSetting } = require('../triangle/settings');
  8. const fs = require('fs');
  9. const path = require('path');
  10. const GamesCacheFile = path.join(__dirname, '../data/games.cache');
  11. const DevGameTastFile = path.join(__dirname, '../data/gameTast.json');
  12. const childOptions = process.env.NODE_ENV == 'development' ? {
  13. execArgv: ['--inspect=9230'],
  14. stdio: ['pipe', 'pipe', 'pipe', 'ipc']
  15. } : {};
  16. const { fork } = require('child_process');
  17. const events_child = fork('./triangle/eventsMatch.js', [], childOptions);
  18. const PS_IOR_KEYS = [
  19. ['0', 'ior_mh', 'ior_mn', 'ior_mc'],
  20. ['-1', 'ior_rh_15', 'ior_wmh_1', 'ior_rac_05'],
  21. ['-2', 'ior_rh_25', 'ior_wmh_2', 'ior_rac_15'],
  22. ['-3', 'ior_rh_35', 'ior_wmh_3', 'ior_rac_25'],
  23. ['-4', 'ior_rh_45', 'ior_wmh_4', 'ior_rac_35'],
  24. ['-5', 'ior_rh_55', 'ior_wmh_5', 'ior_rac_45'],
  25. ['+1', 'ior_rah_05', 'ior_wmc_1', 'ior_rc_15'],
  26. ['+2', 'ior_rah_15', 'ior_wmc_2', 'ior_rc_25'],
  27. ['+3', 'ior_rah_25', 'ior_wmc_3', 'ior_rc_35'],
  28. ['+4', 'ior_rah_35', 'ior_wmc_4', 'ior_rc_45'],
  29. ['+5', 'ior_rah_45', 'ior_wmc_5', 'ior_rc_55'],
  30. ['jqs', 'ior_ot_1', 'ior_ot_2', 'ior_ot_3', 'ior_ot_4', 'ior_ot_5', 'ior_ot_6', 'ior_ot_7'],
  31. ];
  32. const IOR_KEYS_TYPE = {
  33. A: 1, L: 1,
  34. D: 2, K: 2,
  35. }
  36. // 测试环境
  37. // const BASE_API_URL = 'https://dev.api.czxd8.com/api/p';
  38. const IS_DEV = process.env.NODE_ENV == 'development';
  39. const BASE_API_URL = IS_DEV ? 'https://api.qboss.vip/api' : 'http://172.17.222.37/api';
  40. const GAMES = {
  41. Leagues: {},
  42. Baselist: {},
  43. Relations: {},
  44. Solutions: {},
  45. ObOriginalData: {},
  46. UpdateTimestamp: {},
  47. };
  48. const Request = {
  49. callbacks: {},
  50. count: 0,
  51. }
  52. /**
  53. * 精确浮点数字
  54. * @param {number} number
  55. * @param {number} x
  56. * @returns {number}
  57. */
  58. const fixFloat = (number, x=2) => {
  59. return parseFloat(number.toFixed(x));
  60. }
  61. /**
  62. * 获取市场类型
  63. */
  64. const getMarketType = (mk) => {
  65. if (mk == 0) {
  66. return 'early';
  67. }
  68. else if (mk == 2) {
  69. return 'rollball';
  70. }
  71. return 'today';
  72. }
  73. /**
  74. * 获取策略类型
  75. */
  76. const getRuleType = (rule) => {
  77. const rulePrefix = rule.split(':')[0];
  78. return IOR_KEYS_TYPE[rulePrefix];
  79. }
  80. /**
  81. * 关键词匹配比赛
  82. */
  83. const matchGame = (relation, sk) => {
  84. const keys = [];
  85. Object.keys(relation).forEach(platform => {
  86. const { leagueName, teamHomeName, teamAwayName } = relation[platform];
  87. if (platform == 'ps') {
  88. keys.push(leagueName);
  89. }
  90. keys.push(teamHomeName, teamAwayName);
  91. });
  92. return keys.some(key => key.includes(sk));
  93. }
  94. /**
  95. * 同步联赛列表
  96. */
  97. const syncLeaguesList = ({ mk, leagues }) => {
  98. if (IS_DEV) {
  99. return Logs.out('syncLeaguesList', { mk, leagues });
  100. }
  101. axios.post(`${BASE_API_URL}/p/syncLeague`, { mk, leagues }, { proxy: false })
  102. .then(res => {
  103. // Logs.out('syncLeaguesList', res.data);
  104. })
  105. .catch(err => {
  106. Logs.out('syncLeaguesList', err.message);
  107. });
  108. }
  109. /**
  110. * 更新联赛列表
  111. */
  112. const updateLeaguesList = ({ mk, leagues, platform='ps' }) => {
  113. const { Leagues } = GAMES;
  114. if (!Leagues[platform]) {
  115. Leagues[platform] = {};
  116. }
  117. const leaguesMap = Leagues[platform];
  118. const nowTime = Date.now();
  119. const expireTime = nowTime - 1000 * 60 * 5;
  120. if (!leaguesMap[mk]) {
  121. leaguesMap[mk] = {
  122. timestamp: 0,
  123. leagues: [],
  124. };
  125. }
  126. if (leaguesMap[mk].timestamp < expireTime ||
  127. JSON.stringify(leaguesMap[mk].leagues) != JSON.stringify(leagues)) {
  128. leaguesMap[mk].leagues = leagues;
  129. leaguesMap[mk].timestamp = nowTime;
  130. if (platform == 'ps') {
  131. syncLeaguesList({ mk, leagues });
  132. }
  133. return leagues.length;
  134. }
  135. return 0;
  136. }
  137. /**
  138. * 获取筛选过的联赛
  139. */
  140. const getFilteredLeagues = async (mk) => {
  141. return axios.get(`${BASE_API_URL}/p/getLeagueTast?mk=${mk ?? ''}`, { proxy: false })
  142. .then(res => {
  143. if (res.data.code == 0) {
  144. return res.data.data;
  145. }
  146. return Promise.reject(new Error(res.data.message));
  147. });
  148. }
  149. /**
  150. * 更新OB原始数据
  151. */
  152. const updateOriginalData = ({ leagues, matches }) => {
  153. const { ObOriginalData } = GAMES;
  154. ObOriginalData.leagues = leagues;
  155. ObOriginalData.matches = matches;
  156. ObOriginalData.timestamp = Date.now();
  157. }
  158. /**
  159. * 获取OB原始数据
  160. */
  161. const getOriginalData = () => {
  162. const { ObOriginalData } = GAMES;
  163. return ObOriginalData;
  164. }
  165. /**
  166. * 同步比赛列表到服务器
  167. */
  168. const syncGamesList = ({ platform, mk, games }) => {
  169. if (IS_DEV) {
  170. return Logs.out('syncGamesList', { platform, mk, games });
  171. }
  172. axios.post(`${BASE_API_URL}/p/syncGames`, { platform, mk, games })
  173. .then(res => {
  174. // Logs.out('syncGamesList', { platform, mk, count: games.length }, res.data);
  175. })
  176. .catch(err => {
  177. Logs.out('syncGamesList', { platform, mk }, err.message);
  178. });
  179. }
  180. /**
  181. * 同步基准比赛列表
  182. */
  183. const syncBaseList = ({ marketType, games }) => {
  184. const baseList = GAMES.Baselist;
  185. // 直接创建新列表
  186. if (!baseList[marketType]) {
  187. baseList[marketType] = games;
  188. return;
  189. }
  190. const newMap = new Map(games.map(item => [item.eventId, item]));
  191. // 删除不存在的项
  192. for (let i = baseList[marketType].length - 1; i >= 0; i--) {
  193. if (!newMap.has(baseList[marketType][i].eventId)) {
  194. baseList[marketType].splice(i, 1);
  195. }
  196. }
  197. // 添加或更新
  198. const oldIds = new Set(baseList[marketType].map(item => item.eventId));
  199. games.forEach(game => {
  200. if (!oldIds.has(game.eventId)) {
  201. // 添加新项
  202. baseList[marketType].push(game);
  203. }
  204. });
  205. }
  206. /**
  207. * 清理基准比赛列表
  208. */
  209. // const cleanupBaseList = () => {
  210. // const baseList = GAMES.Baselist;
  211. // const nowTime = Date.now();
  212. // const expireTime = nowTime - 1000*60*60*3;
  213. // Object.keys(baseList).forEach(marketType => {
  214. // baseList[marketType] = baseList[marketType].filter(item => item.timestamp < expireTime);
  215. // });
  216. // }
  217. /**
  218. * 更新比赛列表
  219. */
  220. const updateGamesList = (({ platform, mk, games } = {}) => {
  221. return new Promise((resolve, reject) => {
  222. if (!platform || !games) {
  223. return reject(new Error('PLATFORM_GAMES_INVALID'));
  224. }
  225. syncGamesList({ platform, mk, games });
  226. resolve();
  227. });
  228. });
  229. /**
  230. * 提交盘口数据
  231. */
  232. // const submitOdds = ({ platform, mk, games }) => {
  233. // if (IS_DEV) {
  234. // return Logs.out('syncOdds', { platform, mk, games });
  235. // }
  236. // axios.post(`${BASE_API_URL}/p/syncOdds`, { platform, mk, games}, { proxy: false })
  237. // .then(res => {
  238. // // Logs.out('syncOdds', { platform, mk, count: games.length }, res.data);
  239. // })
  240. // .catch(err => {
  241. // Logs.out('syncOdds', { platform, mk }, err.message);
  242. // });
  243. // }
  244. /**
  245. * 同步基准盘口
  246. */
  247. const syncBaseEvents = ({ mk, games, outrights }) => {
  248. const {
  249. expireTimeEvents, expireTimeSpecial,
  250. subsidyTime, subsidyAmount,
  251. subsidyRbWmAmount, subsidyRbOtAmount
  252. } = getSetting();
  253. const nowTime = Date.now();
  254. const marketType = getMarketType(mk);
  255. const baseList = GAMES.Baselist;
  256. if (!baseList[marketType]) {
  257. return;
  258. }
  259. const baseMap = new Map(baseList[marketType].map(item => [item.eventId, item]));
  260. games?.forEach(game => {
  261. const { eventId, originId, stage, retime, score, wm, evtime, events } = game;
  262. const baseGame = baseMap.get(eventId);
  263. if (baseGame) {
  264. const { mk } = baseGame;
  265. const isRb = (mk == 2);
  266. if (isRb) {
  267. Object.keys(events).forEach(ior => {
  268. if ((ior.startsWith('ior_r') || ior.startsWith('ior_m')) && subsidyRbWmAmount) {
  269. const sourceOdds = events[ior].v;
  270. events[ior].v = fixFloat(sourceOdds * (1 + subsidyRbWmAmount), 3);
  271. events[ior].s = sourceOdds
  272. }
  273. });
  274. }
  275. baseGame.originId = originId;
  276. baseGame.stage = stage;
  277. baseGame.retime = retime;
  278. baseGame.score = score;
  279. baseGame.wm = wm;
  280. baseGame.evtime = evtime;
  281. baseGame.events = events;
  282. }
  283. });
  284. outrights?.forEach(outright => {
  285. const { parentId, sptime, special } = outright;
  286. const baseGame = baseMap.get(parentId);
  287. if (baseGame) { // 赛前特殊盘口
  288. const { timestamp } = baseGame;
  289. const isSubsidy = timestamp > nowTime && timestamp < nowTime + 1000*60*60*subsidyTime;
  290. if (isSubsidy) {
  291. Object.keys(special).forEach(ior => {
  292. if (ior.startsWith('ior_ot') && subsidyAmount) {
  293. const sourceOdds = special[ior].v;
  294. special[ior].v = fixFloat(sourceOdds * (1 + subsidyAmount), 3);
  295. special[ior].s = sourceOdds
  296. }
  297. });
  298. }
  299. baseGame.sptime = sptime;
  300. baseGame.special = special;
  301. }
  302. else {
  303. const originBaseMap = new Map(baseList[marketType].map(item => [item.originId, item]));
  304. const originBaseGame = originBaseMap.get(parentId);
  305. if (originBaseGame) { // 滚球特殊盘口
  306. Object.keys(special).forEach(ior => {
  307. if (ior.startsWith('ior_wm') && subsidyRbWmAmount) {
  308. const sourceOdds = special[ior].v;
  309. special[ior].v = fixFloat(sourceOdds * (1 + subsidyRbWmAmount), 3);
  310. special[ior].s = sourceOdds
  311. }
  312. else if (ior.startsWith('ior_ot') && subsidyRbOtAmount) {
  313. const sourceOdds = special[ior].v;
  314. special[ior].v = fixFloat(sourceOdds * (1 + subsidyRbOtAmount), 3);
  315. special[ior].s = sourceOdds
  316. }
  317. });
  318. originBaseGame.sptime = sptime;
  319. originBaseGame.special = special;
  320. }
  321. }
  322. });
  323. if (games?.length) {
  324. const gamesList = baseList[marketType]?.map(game => {
  325. const { evtime, events, sptime, special, ...gameInfo } = game;
  326. const expireTimeEv = nowTime - expireTimeEvents;
  327. const expireTimeSP = nowTime - expireTimeSpecial;
  328. let odds = {};
  329. if (evtime > expireTimeEv) {
  330. odds = { ...odds, ...events };
  331. }
  332. if (sptime > expireTimeSP) {
  333. odds = { ...odds, ...special };
  334. }
  335. const matches = PS_IOR_KEYS.map(([label, ...keys]) => {
  336. let match = keys.map(key => {
  337. return {
  338. key,
  339. value: odds[key]?.v ?? 0,
  340. origin: odds[key]?.r,
  341. source: odds[key]?.s,
  342. }
  343. });
  344. if (label == 'jqs') {
  345. match = match.filter(item => item.value !== 0);
  346. }
  347. return {
  348. label,
  349. match
  350. };
  351. }).filter(item => {
  352. if (item.label == 'jqs') {
  353. return item.match.length;
  354. }
  355. else {
  356. return item.match.every(entry => entry.value !== 0);
  357. }
  358. });
  359. game.matches = matches; // matches 也记录下来
  360. let uptime = evtime ?? 0;
  361. return { ...gameInfo, matches, uptime };
  362. });
  363. // if (gamesList.filter(item => item.uptime > 0).length) {
  364. // // if (mk == 2) {
  365. // // Logs.out('syncBaseEvents', JSON.stringify({ mk, gamesList }, null, 2));
  366. // // }
  367. // submitOdds({ platform: 'ps', mk, games: gamesList });
  368. // }
  369. const relatedGames = Object.values(GAMES.Relations).map(item => item.rel?.['ps'] ?? {});
  370. if (!relatedGames.length) {
  371. return 0;
  372. }
  373. let update = 0;
  374. const relatedMap = new Map(relatedGames.map(item => [item.eventId, item]));
  375. gamesList?.forEach(game => {
  376. const { eventId, matches, uptime, stage, retime, score, wm } = game;
  377. const relatedGame = relatedMap.get(eventId);
  378. if (relatedGame) {
  379. const events = {};
  380. matches.forEach(({ label, match }) => {
  381. match.forEach(({ key, value, origin, source }) => {
  382. events[key] = {
  383. v: value,
  384. r: origin,
  385. s: source
  386. };
  387. });
  388. });
  389. relatedGame.evtime = uptime;
  390. relatedGame.events = events;
  391. relatedGame.stage = stage;
  392. relatedGame.retime = retime;
  393. relatedGame.score = score;
  394. relatedGame.wm = wm;
  395. update ++;
  396. }
  397. });
  398. return update;
  399. }
  400. }
  401. const updateGamesEvents = ({ platform, mk, games, outrights, timestamp, tp }) => {
  402. return new Promise((resolve, reject) => {
  403. if (!platform || (!games && !outrights)) {
  404. return reject(new Error('PLATFORM_GAMES_INVALID'));
  405. }
  406. const { UpdateTimestamp } = GAMES;
  407. if (!UpdateTimestamp[tp] || UpdateTimestamp[tp] < timestamp) {
  408. // Logs.out('updateGamesEvents', { tp, timestamp });
  409. UpdateTimestamp[tp] = timestamp;
  410. }
  411. else {
  412. return resolve({ update: 0 });
  413. }
  414. if (platform == 'ps') {
  415. const update = syncBaseEvents({ mk, games, outrights });
  416. return resolve({ update });
  417. }
  418. const relatedGames = Object.values(GAMES.Relations).map(item => item.rel?.[platform] ?? {});
  419. if (!relatedGames.length) {
  420. return resolve({ update: 0 });
  421. }
  422. const updateCount = {
  423. update: 0
  424. };
  425. const relatedMap = new Map(relatedGames.map(item => [item.eventId, item]));
  426. games?.forEach(game => {
  427. const { eventId, evtime, events, stage, retime, score, wm } = game;
  428. const relatedGame = relatedMap.get(eventId);
  429. if (relatedGame) {
  430. relatedGame.evtime = evtime;
  431. relatedGame.events = events;
  432. relatedGame.stage = stage;
  433. relatedGame.retime = retime;
  434. relatedGame.score = score;
  435. relatedGame.wm = wm;
  436. updateCount.update ++;
  437. }
  438. });
  439. outrights?.forEach(outright => {
  440. const { parentId, sptime, special } = outright;
  441. const relatedGame = relatedMap.get(parentId);
  442. if (relatedGame) {
  443. relatedGame.sptime = sptime;
  444. relatedGame.special = special;
  445. updateCount.update ++;
  446. }
  447. });
  448. resolve(updateCount);
  449. });
  450. }
  451. /**
  452. * 获取比赛盘口
  453. */
  454. const getGamesEvents = ({ platform, relIds = [] } = {}) => {
  455. if (!relIds.length) {
  456. return null;
  457. }
  458. const idSet = new Set(relIds);
  459. const relations = { ...GAMES.Relations };
  460. Object.keys(relations).forEach(id => {
  461. if (idSet.size && !idSet.has(+id)) {
  462. delete relations[id];
  463. }
  464. });
  465. if (platform) {
  466. return Object.values(relations).map(rel => rel[platform] ?? {});
  467. }
  468. const gamesEvents = {};
  469. Object.values(relations).forEach(({ rel }) => {
  470. Object.keys(rel).forEach(platform => {
  471. const game = rel[platform] ?? {};
  472. const { eventId, events, special } = game;
  473. if (!gamesEvents[platform]) {
  474. gamesEvents[platform] = {};
  475. }
  476. gamesEvents[platform][eventId] = { ...events, ...special };
  477. });
  478. });
  479. return gamesEvents;
  480. }
  481. /**
  482. * 获取关联比赛
  483. */
  484. const getDevGameTast = () => {
  485. return new Promise((resolve) => {
  486. const data = Cache.getData(DevGameTastFile, true);
  487. resolve({data});
  488. });
  489. }
  490. const fetchGamesRelation = async (mk='') => {
  491. const getGameTast = Promise.all([
  492. getDevGameTast(),
  493. axios.get(`${BASE_API_URL}/p/getGameTast?mk=${mk}`, { proxy: false })
  494. ]);
  495. return getGameTast.then(([res1, res2]) => {
  496. const resData = res1.data ?? res2.data;
  497. if (resData.code == 0) {
  498. const nowTime = Date.now();
  499. const gamesRelation = resData.data?.filter(item => {
  500. const timestamp = new Date(item.timestamp).getTime();
  501. if (nowTime > timestamp) {
  502. item.mk = 2;
  503. }
  504. item.timestamp = timestamp;
  505. const expireTime = timestamp + 1000*60*60*3;
  506. return expireTime > nowTime;
  507. }).map(item => {
  508. const {
  509. id, mk, league_name,
  510. event_id: ps_event_id,
  511. league_id: ps_league_id,
  512. team_home_name: ps_team_home_name,
  513. team_away_name: ps_team_away_name,
  514. ob_event_id, ob_league_id,
  515. ob_team_home_name,
  516. ob_team_away_name,
  517. hg_event_id, hg_league_id,
  518. hg_team_home_name,
  519. hg_team_away_name,
  520. timestamp,
  521. } = item;
  522. const rel = {
  523. ps: {
  524. eventId: +ps_event_id,
  525. leagueId: +ps_league_id,
  526. leagueName: league_name,
  527. teamHomeName: ps_team_home_name,
  528. teamAwayName: ps_team_away_name,
  529. timestamp
  530. },
  531. ob: ob_event_id ? {
  532. eventId: +ob_event_id,
  533. leagueId: +ob_league_id,
  534. leagueName: league_name,
  535. teamHomeName: ob_team_home_name,
  536. teamAwayName: ob_team_away_name,
  537. timestamp
  538. } : null,
  539. hg: hg_event_id ? {
  540. eventId: +hg_event_id,
  541. leagueId: +hg_league_id,
  542. leagueName: league_name,
  543. teamHomeName: hg_team_home_name,
  544. teamAwayName: hg_team_away_name,
  545. timestamp
  546. } : null
  547. };
  548. return { id: ps_event_id, mk, rel, timestamp };
  549. }) ?? [];
  550. return gamesRelation;
  551. }
  552. return Promise.reject(new Error(resData.message));
  553. });
  554. }
  555. const getGamesRelation = ({ mk=-1, ids, listEvents } = {}) => {
  556. let idsSet = null;
  557. if (ids?.length) {
  558. idsSet = new Set(ids);
  559. }
  560. const relations = Object.values(GAMES.Relations).filter(item => {
  561. if (idsSet && !idsSet.has(item.id)) {
  562. return false;
  563. }
  564. return mk == -1 || item.mk == mk;
  565. }).sort((a, b) => a.timestamp - b.timestamp);
  566. if (listEvents) {
  567. return relations;
  568. }
  569. const gamesRelation = relations.map(item => {
  570. const { rel, ...relationInfo } = item;
  571. const tempRel = { ...rel };
  572. Object.keys(tempRel).forEach(platform => {
  573. const { events, evtime, sptime, special, ...gameInfo } = tempRel[platform];
  574. tempRel[platform] = gameInfo;
  575. });
  576. return { ...relationInfo, rel: tempRel };
  577. });
  578. return gamesRelation;
  579. }
  580. /**
  581. * 定时更新关联比赛列表
  582. */
  583. const updateGamesRelation = () => {
  584. fetchGamesRelation()
  585. .then(gamesRelation => {
  586. const baseList = {};
  587. gamesRelation.map(item => {
  588. const baseGame = item.rel?.['ps'] ?? {};
  589. return { ...baseGame, mk: item.mk };
  590. }).forEach(item => {
  591. const marketType = getMarketType(item.mk);
  592. if (!baseList[marketType]) {
  593. baseList[marketType] = [];
  594. }
  595. baseList[marketType].push(item);
  596. });
  597. Object.keys(baseList).forEach(marketType => {
  598. syncBaseList({ marketType, games: baseList[marketType] });
  599. });
  600. const updateCount = {
  601. add: 0,
  602. update: 0,
  603. delete: 0
  604. };
  605. gamesRelation.forEach(item => {
  606. const { id, mk } = item;
  607. const oldItem = GAMES.Relations[id];
  608. if (!oldItem) {
  609. GAMES.Relations[id] = item;
  610. updateCount.add ++;
  611. }
  612. else if (oldItem.mk != mk) {
  613. GAMES.Relations[id] = item;
  614. updateCount.update ++;
  615. }
  616. });
  617. const relations = new Set(gamesRelation.map(item => +item.id));
  618. Object.keys(GAMES.Relations).forEach(id => {
  619. if (!relations.has(+id)) {
  620. delete GAMES.Relations[id];
  621. updateCount.delete ++;
  622. }
  623. });
  624. if (updateCount.add || updateCount.update || updateCount.delete) {
  625. Logs.out('updateGamesRelation', updateCount);
  626. }
  627. else {
  628. Logs.outDev('updateGamesRelation', updateCount);
  629. }
  630. })
  631. .catch(err => {
  632. Logs.out('updateGamesRelation', err.message);
  633. })
  634. .finally(() => {
  635. setTimeout(updateGamesRelation, 60000);
  636. });
  637. }
  638. updateGamesRelation();
  639. const gamesRelationCleanup = () => {
  640. const relations = Object.values(GAMES.Relations);
  641. const expireTime = Date.now() - 1000*60;
  642. relations.forEach(item => {
  643. const { rel } = item;
  644. Object.keys(rel).forEach(platform => {
  645. const { evtime, sptime } = rel[platform];
  646. if (evtime && evtime < expireTime) {
  647. delete rel[platform].events;
  648. delete rel[platform].evtime;
  649. }
  650. if (sptime && sptime < expireTime) {
  651. delete rel[platform].special;
  652. delete rel[platform].sptime;
  653. }
  654. });
  655. });
  656. }
  657. /**
  658. * 同步比赛结果
  659. */
  660. const syncGamesResult = async (result) => {
  661. if (IS_DEV) {
  662. return Logs.out('updateGamesResult', result);
  663. }
  664. // axios.post(`${BASE_API_URL}/p/syncMatchResult`, result, { proxy: false })
  665. // .then(res => {
  666. // // Logs.out('syncMatchResult', res.data);
  667. // })
  668. // .catch(err => {
  669. // Logs.out('syncMatchResult', err.message);
  670. // });
  671. }
  672. /**
  673. * 更新比赛结果
  674. */
  675. const updateGamesResult = (result) => {
  676. syncGamesResult(result);
  677. return Promise.resolve();
  678. }
  679. /**
  680. * 同步中单方案
  681. */
  682. // const syncSolutions = (solutions) => {
  683. // if (IS_DEV) {
  684. // return Logs.out('syncSolutions', solutions);
  685. // }
  686. // axios.post(`${BASE_API_URL}/p/syncDsOpportunity`, solutions, { proxy: false })
  687. // .then(res => {
  688. // // Logs.out('syncSolutions', res.data);
  689. // })
  690. // .catch(err => {
  691. // Logs.out('syncSolutions', err.message);
  692. // });
  693. // }
  694. /**
  695. * 更新中单方案
  696. */
  697. const getCprKey = (cpr) => {
  698. const { k, p, v } = cpr;
  699. return `${k}_${p}_${v}`;
  700. }
  701. const compareCpr = (cpr1, cpr2) => {
  702. const key1 = getCprKey(cpr1);
  703. const key2 = getCprKey(cpr2);
  704. return key1 === key2;
  705. }
  706. const updateSolutions = (solutions, eventsLogsMap) => {
  707. if (solutions?.length) {
  708. const solutionsHistory = GAMES.Solutions;
  709. const updateIds = { add: [], update: [], retain: [], remove: [] }
  710. solutions.forEach(item => {
  711. const { sid, cpr, sol: { win_average, win_profit_rate } } = item;
  712. if (!solutionsHistory[sid]) {
  713. solutionsHistory[sid] = item;
  714. updateIds.add.push(sid);
  715. return;
  716. }
  717. const historySolution = solutionsHistory[sid];
  718. if (historySolution.sol.win_average !== win_average ||
  719. historySolution.sol.win_profit_rate !== win_profit_rate ||
  720. !compareCpr(historySolution.cpr, cpr)) {
  721. solutionsHistory[sid] = item;
  722. updateIds.update.push(sid);
  723. return;
  724. }
  725. const { timestamp } = item;
  726. historySolution.timestamp = timestamp;
  727. updateIds.retain.push(sid);
  728. });
  729. const solutionsMap = new Map(solutions.map(item => [item.sid, item]));
  730. Object.keys(solutionsHistory).forEach(sid => {
  731. if (!solutionsMap.has(sid)) {
  732. delete solutionsHistory[sid];
  733. updateIds.remove.push(sid);
  734. }
  735. });
  736. const solutionUpdate = {};
  737. Object.keys(updateIds).forEach(key => {
  738. if (key == 'retain' || key == 'remove') {
  739. solutionUpdate[key] = updateIds[key];
  740. }
  741. else {
  742. solutionUpdate[key] = updateIds[key].map(sid => solutionsHistory[sid]);
  743. }
  744. });
  745. // syncSolutions(solutionUpdate);
  746. if (updateIds.add.length / solutions.length > 0.25 ||
  747. updateIds.remove.length / solutions.length > 0.25
  748. ) {
  749. const { expireEvents, removeEvents } = eventsLogsMap;
  750. const expireEvemtsMap = {};
  751. expireEvents.forEach(item => {
  752. const { mk, platform, info, evExpire, spExpire, evtime, sptime } = item;
  753. if (!expireEvemtsMap[mk]) {
  754. expireEvemtsMap[mk] = {};
  755. }
  756. if (!expireEvemtsMap[mk][platform]) {
  757. expireEvemtsMap[mk][platform] = {};
  758. }
  759. if (!expireEvemtsMap[mk][platform].list) {
  760. expireEvemtsMap[mk][platform].list = [];
  761. }
  762. if (!expireEvemtsMap[mk][platform].evtime) {
  763. expireEvemtsMap[mk][platform].evtime = evtime;
  764. }
  765. if (!expireEvemtsMap[mk][platform].sptime) {
  766. expireEvemtsMap[mk][platform].sptime = sptime;
  767. }
  768. expireEvemtsMap[mk][platform].list.push({ info, evExpire, spExpire, evtime, sptime });
  769. });
  770. Object.keys(expireEvemtsMap).forEach(mk => {
  771. Object.keys(expireEvemtsMap[mk]).forEach(platform => {
  772. Logs.out('invalid events, mk %d, platform %s, expire %d, evtime %d, sptime %d',
  773. mk, platform,
  774. expireEvemtsMap[mk][platform].list.length,
  775. expireEvemtsMap[mk][platform].evtime,
  776. expireEvemtsMap[mk][platform].sptime,
  777. )
  778. });
  779. });
  780. Logs.out('solutions add %d, update %d, retain %d, remove %d',
  781. updateIds.add.length, updateIds.update.length, updateIds.retain.length, updateIds.remove.length);
  782. }
  783. else {
  784. Logs.outDev('solutions update complete', updateIds);
  785. }
  786. }
  787. }
  788. /**
  789. * 获取中单方案
  790. */
  791. const getSolutions = async ({ win_min, with_events, mk=-1 }) => {
  792. // Logs.out('getSolutions', win_min);
  793. const filterMarketType = +mk;
  794. const { minShowAmount } = getSetting();
  795. const solutionsList = Object.values(GAMES.Solutions);
  796. const gamesRelation = getGamesRelation();
  797. const relationsMap = new Map(gamesRelation.map(item => [item.id, item]));
  798. const mkCount = {
  799. all: 0,
  800. rollball: 0,
  801. today: 0,
  802. early: 0,
  803. }
  804. let solutions = solutionsList.filter(item => {
  805. const { sol: { win_average } } = item;
  806. return win_average >= (win_min ?? minShowAmount);
  807. })
  808. .map(item => {
  809. const { info: { id } } = item;
  810. const { mk, rel } = relationsMap.get(id);
  811. const marketType = getMarketType(mk);
  812. mkCount.all ++;
  813. mkCount[marketType] ++;
  814. return {
  815. ...item,
  816. info: { id, mk, ...rel }
  817. }
  818. });
  819. if (mk >= 0) {
  820. solutions = solutions.filter(item => {
  821. const { info: { mk } } = item;
  822. return mk == filterMarketType;
  823. });
  824. }
  825. solutions = solutions.sort((a, b) => b.sol.win_average - a.sol.win_average);
  826. const relIds = solutions.map(item => item.info.id);
  827. let gamesEvents;
  828. if (with_events) {
  829. gamesEvents = getGamesEvents({ relIds });
  830. }
  831. return { solutions, gamesEvents, mkCount };
  832. }
  833. /**
  834. * 获取中单方案并按照比赛分组
  835. */
  836. const getGamesSolutions = async ({ win_min, with_events, mk=-1, tp=0, sk }) => {
  837. const filterMarketType = +mk;
  838. const filterDataType = +tp;
  839. const { minShowAmount } = getSetting();
  840. const solutionsList = Object.values(GAMES.Solutions);
  841. const gamesRelation = getGamesRelation({ listEvents: with_events });
  842. const relationsMap = new Map(gamesRelation.map(item => [item.id, item]));
  843. const mkCount = {
  844. all: 0,
  845. rollball: 0,
  846. today: 0,
  847. early: 0,
  848. }
  849. const solutionsMap = {};
  850. solutionsList.forEach(item => {
  851. const { info: { id }, ...solution } = item;
  852. const { rule, sol: { win_average } } = solution;
  853. const ruleType = getRuleType(rule);
  854. if ((filterDataType == 0 || filterDataType == ruleType) && (!!sk || win_average >= (win_min ?? minShowAmount))) {
  855. const gameRelation = relationsMap.get(id);
  856. if (!solutionsMap[id]) {
  857. solutionsMap[id] = { ...gameRelation, solutions: [] };
  858. }
  859. solutionsMap[id].solutions.push(solution);
  860. }
  861. })
  862. const gamesSolutions = Object.values(solutionsMap)
  863. .filter(item => {
  864. const { mk, rel, solutions } = item;
  865. const marketType = getMarketType(mk);
  866. if (!sk || matchGame(rel, sk)) {
  867. mkCount.all ++;
  868. mkCount[marketType] ++;
  869. solutions.sort((a, b) => b.sol.win_average - a.sol.win_average);
  870. return filterMarketType == -1 || filterMarketType == mk;
  871. }
  872. return false;
  873. })
  874. .sort((a, b) => b.solutions[0].sol.win_average - a.solutions[0].sol.win_average);
  875. return { gamesSolutions, mkCount };
  876. }
  877. /**
  878. * 获取单个中单方案
  879. */
  880. const getSolution = async (sid) => {
  881. if (!sid) {
  882. return Promise.reject(new Error('sid is required'));
  883. }
  884. const solution = GAMES.Solutions[sid];
  885. if (!solution) {
  886. return Promise.reject(new Error('solution not found'));
  887. }
  888. return solution;
  889. }
  890. /**
  891. * 通过比赛 ID 获取中单方案
  892. */
  893. const getSolutionsByIds = async (ids) => {
  894. const baseList = Object.values(GAMES.Baselist).flat();
  895. const baseMap = new Map(baseList.map(item => [item.eventId, item]));
  896. const result = {};
  897. ids.forEach(id => {
  898. const baseGame = baseMap.get(id);
  899. result[id] = {};
  900. result[id].matches = baseGame?.matches ?? [];
  901. result[id].sols = [];
  902. });
  903. Object.values(GAMES.Solutions).forEach(item => {
  904. const { info: { id } } = item;
  905. if (result[id]) {
  906. result[id].sols.push(item);
  907. }
  908. });
  909. return result;
  910. }
  911. /**
  912. * 清理中单方案
  913. */
  914. const solutionsCleanup = () => {
  915. const solutionsHistory = GAMES.Solutions;
  916. const updateIds = { remove: [] }
  917. Object.keys(solutionsHistory).forEach(sid => {
  918. const { timestamp } = solutionsHistory[sid];
  919. const nowTime = Date.now();
  920. if (nowTime - timestamp > 1000*60) {
  921. delete solutionsHistory[sid];
  922. updateIds.remove.push(sid);
  923. return;
  924. }
  925. const solution = solutionsHistory[sid];
  926. const eventTime = solution.info.timestamp;
  927. if (nowTime > eventTime) {
  928. delete solutionsHistory[sid];
  929. updateIds.remove.push(sid);
  930. }
  931. });
  932. // if (updateIds.remove.length) {
  933. // syncSolutions(updateIds);
  934. // }
  935. }
  936. /**
  937. * 清理更新时间戳
  938. */
  939. const cleanupUpdateTimestamp = () => {
  940. const updateTimestamp = GAMES.UpdateTimestamp;
  941. const nowTime = Date.now();
  942. const expireTime = nowTime - 1000*60;
  943. Object.keys(updateTimestamp).forEach(key => {
  944. if (updateTimestamp[key] < expireTime) {
  945. delete updateTimestamp[key];
  946. }
  947. });
  948. }
  949. /**
  950. * 定时清理中单方案
  951. * 定时清理盘口信息
  952. */
  953. setInterval(() => {
  954. // cleanupBaseList();
  955. solutionsCleanup();
  956. gamesRelationCleanup();
  957. cleanupUpdateTimestamp();
  958. }, 1000*30);
  959. /**
  960. * 获取综合利润
  961. */
  962. const getTotalProfit = async (sol1, sol2, inner_base, inner_rebate) => {
  963. const { innerDefaultAmount, innerRebateRatio } = getSetting();
  964. inner_base = inner_base ? +inner_base : innerDefaultAmount;
  965. inner_rebate = inner_rebate ? +inner_rebate : fixFloat(innerRebateRatio / 100, 3);
  966. const profit = calcTotalProfit(sol1, sol2, inner_base, inner_rebate);
  967. return profit;
  968. }
  969. /**
  970. * 通过 sid 获取综合利润
  971. */
  972. const getTotalProfitWithSid = async (sid1, sid2, inner_base, inner_rebate) => {
  973. const preSolution = GAMES.Solutions[sid1];
  974. const subSolution = GAMES.Solutions[sid2];
  975. const sol1 = preSolution?.sol;
  976. const sol2 = subSolution?.sol;
  977. if (!sol1) {
  978. return Promise.reject(new Error('sid1 已失效'));
  979. }
  980. if (!sol2) {
  981. return Promise.reject(new Error('sid2 已失效'));
  982. }
  983. const profit = await getTotalProfit(sol1, sol2, inner_base, inner_rebate);
  984. return { profit, solutions: [preSolution, subSolution] };
  985. }
  986. /**
  987. * 通过盘口信息获取综合利润
  988. */
  989. const getTotalProfitWithBetInfo = async (betInfo1, betInfo2, fixed=false, inner_base, inner_rebate) => {
  990. if (!betInfo1?.cross_type) {
  991. return Promise.reject(new Error('第一个下注信息无效'));
  992. }
  993. if (!betInfo2?.cross_type) {
  994. return Promise.reject(new Error('第二个下注信息无效'));
  995. }
  996. const { innerDefaultAmount, innerRebateRatio } = getSetting();
  997. inner_base = inner_base ? +inner_base : innerDefaultAmount;
  998. inner_rebate = inner_rebate ? +inner_rebate : fixFloat(innerRebateRatio / 100, 3);
  999. if (fixed) {
  1000. return calcTotalProfitWithFixedFirst(betInfo1, betInfo2, inner_base, inner_rebate);
  1001. }
  1002. const [sol1, sol2] = [betInfo1, betInfo2].map(betinfo => eventSolutions({...betinfo, inner_base, inner_rebate }));
  1003. return getTotalProfit(sol1, sol2, inner_base, inner_rebate);
  1004. }
  1005. /**
  1006. * 同步Qboss平台配置
  1007. */
  1008. const syncQbossConfig = () => {
  1009. const setting = getSetting();
  1010. if (!setting.syncSettingEnabled) {
  1011. // Logs.outDev('syncQbossConfig disabled');
  1012. return setTimeout(syncQbossConfig, 1000*5);
  1013. }
  1014. axios.get(`${BASE_API_URL}/p/QbossSystemConfig`, { proxy: false })
  1015. .then(res => {
  1016. const { data } = res;
  1017. if (!data?.data) {
  1018. throw new Error('syncQbossConfig data is empty');
  1019. }
  1020. const {
  1021. qboss_return_ratio,
  1022. ob_return_ratio, ob_return_type,
  1023. hg_return_ratio, hg_return_type,
  1024. qboss_jq_add_odds, qboss_jq_add_hours,
  1025. qboss_gq_add_dy_odds, qboss_gq_add_jq_odds,
  1026. } = data.data;
  1027. const qbossSetting = {
  1028. innerRebateRatio: +qboss_return_ratio,
  1029. obRebateRatio: +ob_return_ratio,
  1030. obRebateType: +ob_return_type,
  1031. hgRebateRatio: +hg_return_ratio,
  1032. hgRebateType: +hg_return_type,
  1033. subsidyTime: +qboss_jq_add_hours,
  1034. subsidyAmount: +qboss_jq_add_odds,
  1035. subsidyRbWmAmount: +qboss_gq_add_dy_odds,
  1036. subsidyRbOtAmount: +qboss_gq_add_jq_odds,
  1037. };
  1038. const settingFields = {};
  1039. let needUpdate = false;
  1040. Object.keys(qbossSetting).forEach(key => {
  1041. if (qbossSetting[key] !== setting[key]) {
  1042. settingFields[key] = qbossSetting[key];
  1043. needUpdate = true;
  1044. }
  1045. });
  1046. if (needUpdate) {
  1047. Setting.update(settingFields);
  1048. // Logs.outDev('syncQbossConfig', settingFields);
  1049. }
  1050. // else {
  1051. // Logs.outDev('syncQbossConfig no change');
  1052. // }
  1053. })
  1054. .catch(err => {
  1055. Logs.out('syncQbossConfig error', err.message);
  1056. })
  1057. .finally(() => {
  1058. setTimeout(syncQbossConfig, 1000*15);
  1059. });
  1060. }
  1061. syncQbossConfig();
  1062. /**
  1063. * 异常通知
  1064. */
  1065. const notifyException = (message) => {
  1066. if (IS_DEV) {
  1067. return Logs.out('notifyException', { message });
  1068. }
  1069. const chat_id = -1003032820471;
  1070. axios.get(`${BASE_API_URL}/telegram/jump`, { params: { chat_id, message } }, { proxy: false })
  1071. .then(() => {
  1072. Logs.out('notifyException', '通知成功');
  1073. })
  1074. .catch(err => {
  1075. Logs.out('notifyException', err.message);
  1076. });
  1077. }
  1078. /**
  1079. * 从子进程获取数据
  1080. */
  1081. const getDataFromChild = (type, callback) => {
  1082. const id = ++Request.count;
  1083. Request.callbacks[id] = callback;
  1084. events_child.send({ method: 'get', id, type });
  1085. }
  1086. /**
  1087. * 向子进程发送数据
  1088. */
  1089. const postDataToChild = (type, data) => {
  1090. events_child.send({ method: 'post', type, data });
  1091. }
  1092. /**
  1093. * 处理子进程消息
  1094. */
  1095. events_child.on('message', async (message) => {
  1096. const { callbacks } = Request;
  1097. const { method, id, type, data } = message;
  1098. if (method == 'get' && id) {
  1099. let responseData = null;
  1100. if (type == 'getGamesRelation') {
  1101. responseData = getGamesRelation({ listEvents: true });
  1102. }
  1103. else if (type == 'getSetting') {
  1104. responseData = getSetting();
  1105. }
  1106. // else if (type == 'getSolutionHistory') {
  1107. // responseData = getSolutionHistory();
  1108. // }
  1109. events_child.send({ type: 'response', id, data: responseData });
  1110. }
  1111. else if (method == 'post') {
  1112. if (type == 'updateSolutions') {
  1113. updateSolutions(data.solutions, data.eventsLogsMap);
  1114. }
  1115. }
  1116. else if (method == 'response' && id && callbacks[id]) {
  1117. callbacks[id](data);
  1118. delete callbacks[id];
  1119. }
  1120. });
  1121. events_child.stderr?.on('data', data => {
  1122. Logs.out('events_child stderr', data.toString());
  1123. });
  1124. const settingUpdate = (fields) => {
  1125. updateSetting(fields);
  1126. postDataToChild('updateSetting', fields);
  1127. }
  1128. const syncSetting = async () => {
  1129. const setting = await Setting.get();
  1130. settingUpdate(setting.toObject());
  1131. }
  1132. syncSetting();
  1133. Setting.onUpdate(settingUpdate);
  1134. /**
  1135. * 保存GAMES数据到缓存文件
  1136. */
  1137. const saveGamesToCache = () => {
  1138. Cache.setData(GamesCacheFile, GAMES, err => {
  1139. if (err) {
  1140. Logs.out('Failed to save games cache:', err.message);
  1141. }
  1142. else {
  1143. Logs.out('Games cache saved successfully');
  1144. }
  1145. });
  1146. }
  1147. /**
  1148. * 从缓存文件加载GAMES数据
  1149. */
  1150. const loadGamesFromCache = () => {
  1151. const gamesCacheData = Cache.getData(GamesCacheFile, true);
  1152. Object.keys(GAMES).forEach(key => {
  1153. if (gamesCacheData[key]) {
  1154. GAMES[key] = gamesCacheData[key];
  1155. }
  1156. });
  1157. Logs.out('Games cache loaded successfully');
  1158. }
  1159. // 在模块加载时尝试从缓存恢复数据
  1160. loadGamesFromCache();
  1161. // 监听进程退出事件,保存GAMES数据
  1162. process.on('exit', saveGamesToCache);
  1163. process.on('SIGINT', () => {
  1164. process.exit(0);
  1165. });
  1166. process.on('SIGTERM', () => {
  1167. process.exit(0);
  1168. });
  1169. process.on('SIGUSR2', () => {
  1170. process.exit(0);
  1171. });
  1172. module.exports = {
  1173. updateLeaguesList, getFilteredLeagues,
  1174. updateGamesList, updateGamesEvents,
  1175. getGamesRelation,
  1176. updateGamesResult,
  1177. updateOriginalData, getOriginalData,
  1178. getSolutions, getGamesSolutions, getSolution, getSolutionsByIds,
  1179. getTotalProfitWithSid,
  1180. getTotalProfitWithBetInfo,
  1181. notifyException,
  1182. }