flyzto 7 месяцев назад
Родитель
Сommit
41b9da9770

+ 32 - 10
server/models/Games.js

@@ -1,5 +1,7 @@
 const { fork } = require('child_process');
-const events_child = fork('./triangle/eventsMatch.js');
+const events_child = fork('./triangle/eventsMatch.js', [], {
+  execArgv: ['--inspect=9228']
+});
 
 const Logs = require('../libs/logs');
 const Relation = require('./Relation');
@@ -14,12 +16,14 @@ const GAMES = {
   Relations: {}
 };
 
-const updateGamesList = (({ platform, games } = {}) => {
+const updateGamesList = (({ platform, type, games } = {}) => {
   return new Promise((resolve, reject) => {
     if (!platform || !games) {
       return reject(new Error('PLATFORM_GAMES_INVALID'));
     }
 
+    const marketType = type == 0 ? 'early' : 'today';
+
     let gamesList = games;
 
     if (platform == 'jc') {
@@ -36,19 +40,22 @@ const updateGamesList = (({ platform, games } = {}) => {
       });
 
       updateGamesEvents({ platform, games: gamesEvents, outrights: gamesOutrights });
-
     }
 
+
     const timestamp = Date.now();
 
     const GAMES_LIST = GAMES.List;
 
     if (!GAMES_LIST[platform]) {
-      GAMES_LIST[platform] = { games: gamesList, timestamp };
+      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].games;
+    const oldGames = GAMES_LIST[platform][marketType].games;
     const newGames = gamesList;
 
     const updateCount = {
@@ -72,20 +79,19 @@ const updateGamesList = (({ platform, games } = {}) => {
       }
     });
 
-    GAMES_LIST[platform].timestamp = timestamp;
+    GAMES_LIST[platform][marketType].timestamp = timestamp;
 
     resolve(updateCount);
   });
 });
 
-const updateGamesEvents = ({ platform, games, outrights }) => {
+const updateGamesEvents = ({ platform, type, games, outrights }) => {
   return new Promise((resolve, reject) => {
     if (!platform || (!games && !outrights)) {
       return reject(new Error('PLATFORM_GAMES_INVALID'));
     }
 
-    // const GAMES_LIST = GAMES.List;
-    const oldGames = Object.values(GAMES.Relations).map(rel => rel[platform]);
+    const oldGames = Object.values(GAMES.Relations).map(rel => rel[platform] ?? {}).flat();
 
     if (!oldGames.length) {
       return resolve({ update: 0 });
@@ -122,7 +128,22 @@ const updateGamesEvents = ({ platform, games, outrights }) => {
 }
 
 const getGamesList = () => {
-  return GAMES.List;
+  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 updateGamesRelation = async (relation) => {
@@ -168,6 +189,7 @@ const getGamesRelation = async (listEvents) => {
       delete game.evtime;
       delete game.special;
       delete game.sptime;
+      rel[platform] = game;
     });
     return { id, rel };
   });

+ 1 - 1
server/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "main": "server.js",
   "scripts": {
-    "dev": "nodemon server.js",
+    "dev": "nodemon --inspect server.js",
     "start": "pm2 start server.js --name sporttery"
   },
   "keywords": [],

+ 4 - 4
server/routes/triangle.js

@@ -7,8 +7,8 @@ const Games = require('../models/Games');
 
 // 更新比赛列表
 router.post('/update_games_list', (req, res) => {
-  const { platform, games } = req.body ?? {};
-  Games.updateGamesList({ platform, games })
+  const { platform, type, games } = req.body ?? {};
+  Games.updateGamesList({ platform, type, games })
   .then(updateCount => {
     res.sendSuccess({ updateCount });
   })
@@ -19,8 +19,8 @@ router.post('/update_games_list', (req, res) => {
 
 // 更新比赛盘口
 router.post('/update_games_events', (req, res) => {
-  const { platform, games, outrights } = req.body ?? {};
-  Games.updateGamesEvents({ platform, games, outrights })
+  const { platform, type, games, outrights } = req.body ?? {};
+  Games.updateGamesEvents({ platform, type, games, outrights })
   .then(updateCount => {
     res.sendSuccess({ updateCount });
   })

+ 36 - 9
server/triangle/eventsMatch.js

@@ -6,8 +6,8 @@ const Request = {
   count: 0,
 }
 
-const WIN_STEP = 15;
-const SOL_FREEZ_TIME = 1000 * 30;
+// const WIN_STEP = 15;
+// const SOL_FREEZ_TIME = 1000 * 30;
 
 const GLOBAL_DATA = {
   relationLength: 0,
@@ -40,6 +40,16 @@ process.on('message', (message) => {
   }
 });
 
+/**
+ * 精确浮点数字
+ * @param {number} number
+ * @param {number} x
+ * @returns {number}
+ */
+const fixFloat = (number, x=2) => {
+  return parseFloat(number.toFixed(x));
+}
+
 const getGamesRelation = () => {
   return new Promise(resolve => {
     getDataFromParent('getGamesRelation', (relations) => {
@@ -134,24 +144,35 @@ const eventMatch = () => {
       eventsMap.odds = oddsMap;
       return eventsMap;
     });
+
     const solutions = eventsCombination(passableEvents);
     if (solutions?.length) {
       const solutionsHistory = GLOBAL_DATA.solutions;
+      const updateIds = { add: [], update: [] }
       solutions.forEach(item => {
-        const { sid, timestamp, sol: { win_average } } = item;
+        const { sid, sol: { win_average } } = item;
+
         if (!solutionsHistory[sid]) {
           solutionsHistory[sid] = item;
-          Logs.out(JSON.stringify(item, null, 2));
+          updateIds.add.push({ sid, win_average });
           return;
         }
 
-        const historyTimestamp = solutionsHistory[sid].timestamp;
         const historyWinAverage = solutionsHistory[sid].sol.win_average;
-        if (win_average - historyWinAverage > WIN_STEP && timestamp - historyTimestamp > SOL_FREEZ_TIME) {
+        if (win_average != historyWinAverage) {
           solutionsHistory[sid] = item;
-          Logs.out(JSON.stringify(item, null, 2));
+          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.out('solutions history update', solutionsList, updateIds);
+      }
     }
   })
   .finally(() => {
@@ -164,12 +185,18 @@ const eventMatch = () => {
 const solutionsCleanup = () => {
   const solutionsHistory = GLOBAL_DATA.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;
-    const nowTime = Date.now();
     if (nowTime > eventTime) {
       delete solutionsHistory[sid];
-      Logs.out('solution history cleanup', sid);
+      Logs.out('solution history expired', sid);
     }
   });
 }

+ 50 - 50
server/triangle/iorKeys.js

@@ -1,81 +1,81 @@
 module.exports = {
   A: [
-    ['ior_rh_05', 'ior_rac_025', 'ior_mn', 'la_wh_wa', false],
-    ['ior_rc_05', 'ior_rah_025', 'ior_mn', 'la_wh_wa', true],
+    ['ior_rh_05', 'ior_rac_025', 'ior_mn', 'la_wh_wa'],
+    ['ior_rc_05', 'ior_rah_025', 'ior_mn', 'la_wh_wa'],
 
-    ['ior_rh_05', 'ior_rc_0', 'ior_mn', 'la_dr_wa', false],
-    ['ior_rc_05', 'ior_rh_0', 'ior_mn', 'la_dr_wa', true],
+    ['ior_rh_05', 'ior_rc_0', 'ior_mn', 'la_dr_wa'],
+    ['ior_rc_05', 'ior_rh_0', 'ior_mn', 'la_dr_wa'],
 
-    ['ior_rh_05', 'ior_rc_025', 'ior_mn', 'la_lh_wa', false],
-    ['ior_rc_05', 'ior_rh_025', 'ior_mn', 'la_lh_wa', true],
+    ['ior_rh_05', 'ior_rc_025', 'ior_mn', 'la_lh_wa'],
+    ['ior_rc_05', 'ior_rh_025', 'ior_mn', 'la_lh_wa'],
 
-    ['ior_rh_05', 'ior_rc_05', 'ior_mn', 'la_la_wa', false],
+    ['ior_rh_05', 'ior_rc_05', 'ior_mn', 'la_la_wa'],
 
-    ['ior_rh_025', 'ior_rc_0', 'ior_mn', 'lh_dr_wa', false],
-    ['ior_rc_025', 'ior_rh_0', 'ior_mn', 'lh_dr_wa', true],
+    ['ior_rh_025', 'ior_rc_0', 'ior_mn', 'lh_dr_wa'],
+    ['ior_rc_025', 'ior_rh_0', 'ior_mn', 'lh_dr_wa'],
 
-    ['ior_rh_025', 'ior_rc_025', 'ior_mn', 'lh_lh_wa', false],
+    ['ior_rh_025', 'ior_rc_025', 'ior_mn', 'lh_lh_wa'],
   ],
   B: [
-    ['ior_rh_15', 'ior_rac_125', 'ior_wmh_1', 'la_wh_wa', false],
-    ['ior_rc_15', 'ior_rah_125', 'ior_wmc_1', 'la_wh_wa', true],
+    ['ior_rh_15', 'ior_rac_125', 'ior_wmh_1', 'la_wh_wa'],
+    ['ior_rc_15', 'ior_rah_125', 'ior_wmc_1', 'la_wh_wa'],
 
-    ['ior_rh_15', 'ior_rac_1', 'ior_wmh_1', 'la_dr_wa', false],
-    ['ior_rc_15', 'ior_rah_1', 'ior_wmc_1', 'la_dr_wa', true],
+    ['ior_rh_15', 'ior_rac_1', 'ior_wmh_1', 'la_dr_wa'],
+    ['ior_rc_15', 'ior_rah_1', 'ior_wmc_1', 'la_dr_wa'],
 
-    ['ior_rh_15', 'ior_rac_075', 'ior_wmh_1', 'la_lh_wa', false],
-    ['ior_rc_15', 'ior_rah_075', 'ior_wmc_1', 'la_lh_wa', true],
+    ['ior_rh_15', 'ior_rac_075', 'ior_wmh_1', 'la_lh_wa'],
+    ['ior_rc_15', 'ior_rah_075', 'ior_wmc_1', 'la_lh_wa'],
 
-    ['ior_rh_15', 'ior_rac_05', 'ior_wmh_1', 'la_la_wa', false],
-    ['ior_rc_15', 'ior_rah_05', 'ior_wmc_1', 'la_la_wa', true],
+    ['ior_rh_15', 'ior_rac_05', 'ior_wmh_1', 'la_la_wa'],
+    ['ior_rc_15', 'ior_rah_05', 'ior_wmc_1', 'la_la_wa'],
 
-    ['ior_rah_05', 'ior_rc_075', 'ior_wmc_1', 'la_wh_wa', false],
-    ['ior_rac_05', 'ior_rh_075', 'ior_wmh_1', 'la_wh_wa', true],
+    ['ior_rah_05', 'ior_rc_075', 'ior_wmc_1', 'la_wh_wa'],
+    ['ior_rac_05', 'ior_rh_075', 'ior_wmh_1', 'la_wh_wa'],
 
-    ['ior_rah_05', 'ior_rc_1', 'ior_wmc_1', 'la_dr_wa', false],
-    ['ior_rac_05', 'ior_rh_1', 'ior_wmh_1', 'la_dr_wa', true],
+    ['ior_rah_05', 'ior_rc_1', 'ior_wmc_1', 'la_dr_wa'],
+    ['ior_rac_05', 'ior_rh_1', 'ior_wmh_1', 'la_dr_wa'],
 
-    ['ior_rah_05', 'ior_rc_125', 'ior_wmc_1', 'la_lh_wa', false],
-    ['ior_rac_05', 'ior_rh_125', 'ior_wmh_1', 'la_lh_wa', true],
+    ['ior_rah_05', 'ior_rc_125', 'ior_wmc_1', 'la_lh_wa'],
+    ['ior_rac_05', 'ior_rh_125', 'ior_wmh_1', 'la_lh_wa'],
 
-    ['ior_rah_075', 'ior_rc_1', 'ior_wmc_1', 'lh_dr_wa', false],
-    ['ior_rac_075', 'ior_rh_1', 'ior_wmh_1', 'lh_dr_wa', true],
+    ['ior_rah_075', 'ior_rc_1', 'ior_wmc_1', 'lh_dr_wa'],
+    ['ior_rac_075', 'ior_rh_1', 'ior_wmh_1', 'lh_dr_wa'],
 
-    ['ior_rh_125', 'ior_rac_1', 'ior_wmh_1', 'lh_dr_wa', false],
-    ['ior_rc_125', 'ior_rah_1', 'ior_wmc_1', 'lh_dr_wa', true],
+    ['ior_rh_125', 'ior_rac_1', 'ior_wmh_1', 'lh_dr_wa'],
+    ['ior_rc_125', 'ior_rah_1', 'ior_wmc_1', 'lh_dr_wa'],
 
-    ['ior_rh_125', 'ior_rac_075', 'ior_wmh_1', 'lh_lh_wa', false],
-    ['ior_rc_125', 'ior_rah_075', 'ior_wmc_1', 'lh_lh_wa', true],
+    ['ior_rh_125', 'ior_rac_075', 'ior_wmh_1', 'lh_lh_wa'],
+    ['ior_rc_125', 'ior_rah_075', 'ior_wmc_1', 'lh_lh_wa'],
   ],
   C: [
-    ['ior_rh_25', 'ior_rac_225', 'ior_wmh_2', 'la_wh_wa', false],
-    ['ior_rc_25', 'ior_rah_225', 'ior_wmc_2', 'la_wh_wa', true],
+    ['ior_rh_25', 'ior_rac_225', 'ior_wmh_2', 'la_wh_wa'],
+    ['ior_rc_25', 'ior_rah_225', 'ior_wmc_2', 'la_wh_wa'],
 
-    ['ior_rh_25', 'ior_rac_2', 'ior_wmh_2', 'la_dr_wa', false],
-    ['ior_rc_25', 'ior_rah_2', 'ior_wmc_2', 'la_dr_wa', true],
+    ['ior_rh_25', 'ior_rac_2', 'ior_wmh_2', 'la_dr_wa'],
+    ['ior_rc_25', 'ior_rah_2', 'ior_wmc_2', 'la_dr_wa'],
 
-    ['ior_rh_25', 'ior_rac_175', 'ior_wmh_2', 'la_lh_wa', false],
-    ['ior_rc_25', 'ior_rah_175', 'ior_wmc_2', 'la_lh_wa', true],
+    ['ior_rh_25', 'ior_rac_175', 'ior_wmh_2', 'la_lh_wa'],
+    ['ior_rc_25', 'ior_rah_175', 'ior_wmc_2', 'la_lh_wa'],
 
-    ['ior_rh_25', 'ior_rac_15', 'ior_wmh_2', 'la_la_wa', false],
-    ['ior_rc_25', 'ior_rah_15', 'ior_wmc_2', 'la_la_wa', true],
+    ['ior_rh_25', 'ior_rac_15', 'ior_wmh_2', 'la_la_wa'],
+    ['ior_rc_25', 'ior_rah_15', 'ior_wmc_2', 'la_la_wa'],
 
-    ['ior_rah_15', 'ior_rc_175', 'ior_wmc_2', 'la_wh_wa', false],
-    ['ior_rac_15', 'ior_rh_175', 'ior_wmh_2', 'la_wh_wa', true],
+    ['ior_rah_15', 'ior_rc_175', 'ior_wmc_2', 'la_wh_wa'],
+    ['ior_rac_15', 'ior_rh_175', 'ior_wmh_2', 'la_wh_wa'],
 
-    ['ior_rah_15', 'ior_rc_2', 'ior_wmc_2', 'la_dr_wa', false],
-    ['ior_rac_15', 'ior_rh_2', 'ior_wmh_2', 'la_dr_wa', true],
+    ['ior_rah_15', 'ior_rc_2', 'ior_wmc_2', 'la_dr_wa'],
+    ['ior_rac_15', 'ior_rh_2', 'ior_wmh_2', 'la_dr_wa'],
 
-    ['ior_rah_15', 'ior_rc_225', 'ior_wmc_2', 'la_lh_wa', false],
-    ['ior_rac_15', 'ior_rh_225', 'ior_wmh_2', 'la_lh_wa', true],
+    ['ior_rah_15', 'ior_rc_225', 'ior_wmc_2', 'la_lh_wa'],
+    ['ior_rac_15', 'ior_rh_225', 'ior_wmh_2', 'la_lh_wa'],
 
-    ['ior_rah_175', 'ior_rc_2', 'ior_wmc_2', 'lh_dr_wa', false],
-    ['ior_rac_175', 'ior_rh_2', 'ior_wmh_2', 'lh_dr_wa', true],
+    ['ior_rah_175', 'ior_rc_2', 'ior_wmc_2', 'lh_dr_wa'],
+    ['ior_rac_175', 'ior_rh_2', 'ior_wmh_2', 'lh_dr_wa'],
 
-    ['ior_rh_225', 'ior_rac_2', 'ior_wmh_2', 'lh_dr_wa', false],
-    ['ior_rc_225', 'ior_rah_2', 'ior_wmc_2', 'lh_dr_wa', true],
+    ['ior_rh_225', 'ior_rac_2', 'ior_wmh_2', 'lh_dr_wa'],
+    ['ior_rc_225', 'ior_rah_2', 'ior_wmc_2', 'lh_dr_wa'],
 
-    ['ior_rh_225', 'ior_rac_175', 'ior_wmh_2', 'lh_lh_wa', false],
-    ['ior_rc_225', 'ior_rah_175', 'ior_wmc_2', 'lh_lh_wa', true],
+    ['ior_rh_225', 'ior_rac_175', 'ior_wmh_2', 'lh_lh_wa'],
+    ['ior_rc_225', 'ior_rah_175', 'ior_wmc_2', 'lh_lh_wa'],
   ],
 }

+ 194 - 0
server/triangle/totalProfitCalc.js

@@ -0,0 +1,194 @@
+const fixFloat = (number, x = 2) => {
+  return parseFloat(number.toFixed(x));
+}
+
+const HandicapCalc = function (data) {
+  const { i, g, a, b, c, w, l } = data;
+  const t = w + l;
+
+  const calcTemplate = (handlers) => {
+    if (i > 2 || i < 0) {
+      return {};
+    }
+    if (i === 2) {
+      const z = g;
+      const x = (b + 1) * (t + z) / (a * b - 1);
+      const y = (a + 1) * (t + z) / (a * b - 1);
+      return { x, y, z };
+    };
+    return handlers[i]?.() ?? {};
+  };
+
+  return {
+    la_wh_wa() {
+      return calcTemplate([
+        () => {
+          const x = g;
+          const z = (t + x) / (2 * c + 1);
+          const y = (c + 1) * (t + x) / (c + 0.5) / b;
+          return { x, y, z };
+        },
+        () => {
+          const y = g;
+          const z = ((a + 1) * t + (1 - a * b / 2) * y) / (a * c - 1);
+          const x = ((c + 1) * t + (c - b / 2) * y) / (a * c - 1);
+          return { x, y, z };
+        }
+      ]);
+    },
+    la_dr_wa() {
+      return calcTemplate([
+        () => {
+          const x = g;
+          const z = (t + x) / c;
+          const y = (t + x + z) / b;
+          return { x, y, z };
+        },
+        () => {
+          const y = g;
+          const z = ((a + 1) * t + y) / (a * c - 1);
+          const x = ((c + 1) * t + c * y) / (a * c - 1);
+          return { x, y, z };
+        }
+      ]);
+    },
+    la_lh_wa() {
+      return calcTemplate([
+        () => {
+          const x = g;
+          const z = (2 * b + 1) * (t + x) / (2 * b * c - 1);
+          const y = (c + 1) * (t + x) / (b * c - 0.5);
+          return { x, y, z };
+        },
+        () => {
+          const y = g;
+          const z = ((a + 1) * t + (a / 2 + 1) * y) / (a * c - 1);
+          const x = ((c + 1) * t + (c + 0.5) * y) / (a * c - 1);
+          return { x, y, z };
+        }
+      ]);
+    },
+    lh_dr_wa() {
+      return calcTemplate([
+        () => {
+          const x = g;
+          const z = (t + x / 2) / c;
+          const y = (t + x + z) / b;
+          return { x, y, z };
+        },
+        () => {
+          const y = g;
+          const z = ((2 * a + 1) * t + y) / (2 * a * c - 1);
+          const x = ((c + 1) * t + c * y) / (a * c - 0.5);
+          return { x, y, z };
+        }
+      ]);
+    },
+    lh_lh_wa() {
+      return calcTemplate([
+        () => {
+          const x = g;
+          const z = ((2 * b + 1) * t + (b + 1) * x) / (2 * b * c - 1);
+          const y = ((c + 1) * t + (c + 0.5) * x) / (b * c - 0.5);
+        },
+        () => {
+          const y = g;
+          const z = ((2 * a + 1) * t + (a + 1) * y) / (2 * a * c - 1);
+          const x = ((c + 1) * t + (c + 0.5) * y) / (a * c - 0.5);
+          return { x, y, z };
+        }
+      ]);
+    },
+    la_la_wa() {
+      return calcTemplate([
+        () => {
+          const x = g;
+          const z = (b + 1) * (t + x) / (b * c - 1);
+          const y = (c + 1) * (t + x) / (b * c - 1);
+          return { x, y, z };
+        },
+        () => {
+          const y = g;
+          const z = (a + 1) * (t + y) / (a * c - 1);
+          const x = (c + 1) * (t + y) / (a * c - 1);
+          return { x, y, z };
+        }
+      ]);
+    }
+  }
+}
+
+const calcExternalHandicap = (data) => {
+  const { gold_side_jc: g, odds_side_a: a, odds_side_b: b, odds_side_m: c, jc_index: i, cross_type: t, win_target: w, pre_loss } = data;
+  const l = pre_loss ?? 0;
+  const calc = new HandicapCalc({ i, g, a, b, c, w, l });
+  const { x, y, z } = calc?.[t]() ?? {};
+  return {
+    gold_side_a: fixFloat(x),
+    gold_side_b: fixFloat(y),
+    gold_side_m: fixFloat(z),
+    jc_index: i,
+  }
+
+}
+
+const calcGoldsWithWinTarget = (data) => {
+  const { gold_side_jc, win_target, sol1, sol2 } = data;
+  const preInfo = calcExternalHandicap({ ...sol1, gold_side_jc, win_target });
+  const { gold_side_a: goldA1, gold_side_b: goldB1, gold_side_m: goldM1, jc_index: jc_index_1 } = preInfo;
+  let pre_loss = 0;
+  switch (jc_index_1) {
+    case 0:
+      pre_loss = goldB1 + goldM1;
+      break;
+    case 1:
+      pre_loss = goldA1 + goldM1;
+      break;
+    case 2:
+      pre_loss = goldA1 + goldB1;
+      break;
+  }
+  const nextInfo = calcExternalHandicap({ ...sol2, gold_side_jc, win_target, pre_loss });
+  const { gold_side_a: goldA2, gold_side_b: goldB2, gold_side_m: goldM2, jc_index: jc_index_2 } = nextInfo;
+  const jcWin = fixFloat(10000 * (sol1.odds_side_a + 1) * (sol2.odds_side_a + 1) - goldB1 - goldM1 - goldB2 - goldM2 - 10000);
+  return {
+    jcWin,
+    goldA1,
+    goldB1,
+    goldM1,
+    goldA2,
+    goldB2,
+    goldM2,
+    jc_index_1,
+    jc_index_2,
+  }
+}
+
+export const calcTotalProfit = (sol1, sol2, gold_side_jc) => {
+
+  const winTarget1 = sol1.win_average;
+  const winTarget2 = sol2.win_average;
+  const winTarget = fixFloat(Math.min(winTarget1, winTarget2), 2);
+
+  const jcWin1 = calcGoldsWithWinTarget({ gold_side_jc, win_target: winTarget1, sol1, sol2 })?.jcWin;
+  const jcWin2 = calcGoldsWithWinTarget({ gold_side_jc, win_target: winTarget2, sol1, sol2 })?.jcWin;
+  const jcWin = fixFloat(Math.max(jcWin1, jcWin2), 2);
+
+  const start = Math.max(winTarget, jcWin);
+  const end = Math.min(winTarget, jcWin);
+  const result = [];
+
+  for (let i = start; i > end; i--) {
+    const win_target = i;
+    const goldsInfo = calcGoldsWithWinTarget({ gold_side_jc, win_target, sol1, sol2 });
+    const win_diff = Math.abs(fixFloat(win_target - goldsInfo.jcWin));
+    const lastResult = result.at(-1);
+    if (!lastResult?.win_diff || win_diff < lastResult.win_diff) {
+      result.push({ win_target: fixFloat(win_target), win_diff, ...goldsInfo });
+    }
+    else {
+      break;
+    }
+  }
+  return result.at(-1);
+}

+ 59 - 44
server/triangle/trangleCalc.js

@@ -1,9 +1,9 @@
 const Logs = require('../libs/logs');
 const IOR_KEYS_MAP = require('./iorKeys');
 
-const GOLD_BASE = 1000;
-const WIN_MIN = process.env.NODE_ENV == 'development' ? -30 : -10;
-const JC_REBATE_RATIO = 0.07;
+const GOLD_BASE = 10000;
+const WIN_MIN = process.env.NODE_ENV == 'development' ? -10000 : 100;
+const JC_REBATE_RATIO = 0;
 
 /**
  * 筛选最优赔率
@@ -18,7 +18,6 @@ function getOptimalSelections(data, combos) {
       const jcIndex = i;
       const selection = [];
       let isValid = true;
-
       for (let j = 0; j < 3; j++) {
         const key = rule[j];
         const item = data[key];
@@ -88,12 +87,12 @@ const fixFloat = (number, x=2) => {
  */
 const triangleProfitCalc = (goldsInfo, oddsOption) => {
   const {
-    gold_home: x,
-    gold_away: y,
-    gold_special: z,
-    odds_home: a,
-    odds_away: b,
-    odds_special: c
+    gold_side_a: x,
+    gold_side_b: y,
+    gold_side_m: z,
+    odds_side_a: a,
+    odds_side_b: b,
+    odds_side_m: c
   } = goldsInfo;
 
   const { crossType, jcIndex } = oddsOption;
@@ -118,45 +117,47 @@ const triangleProfitCalc = (goldsInfo, oddsOption) => {
     jc_rebate = z * JC_REBATE_RATIO;
   }
 
-  let win_home = 0, win_away = 0, win_special = 0;
-  win_home = fixFloat(a * x - y - z + jc_rebate);
-  win_away = fixFloat(b * y - x - z + jc_rebate);
+  let win_side_a = 0, win_side_b = 0, win_side_m = 0;
+  win_side_a = a * x - y - z;
+  win_side_b = b * y - x - z;
 
   switch (crossType) {
     case 'la_wh_wa': // 全输 半赢 全赢
-      win_special = c*z - x + b*y/2;
+      win_side_m = c*z - x + b*y/2;
       break;
     case 'la_dr_wa': // 全输 和局 全赢
-      win_special = c*z - x;
+      win_side_m = c*z - x;
       break;
     case 'la_lh_wa': // 全输 半输 全赢
-      win_special = c*z - x - y/2;
+      win_side_m = c*z - x - y/2;
       break;
     case 'lh_dr_wa': // 半输 和局 全赢
-      win_special = c*z - x/2;
+      win_side_m = c*z - x/2;
       break;
     case 'lh_lh_wa': // 半输 半输 全赢
-      win_special = c*z - x/2 - y/2;
+      win_side_m = c*z - x/2 - y/2;
       break;
     case 'la_la_wa': // 全输 全输 全赢
-      win_special = c*z - x - y;
+      win_side_m = c*z - x - y;
       break;
   }
 
-  win_special = fixFloat(win_special + jc_rebate);
-  const win_average = fixFloat((win_home + win_away + win_special) / 3);
+  win_side_a = fixFloat(win_side_a + jc_rebate);
+  win_side_b = fixFloat(win_side_b + jc_rebate);
+  win_side_m = fixFloat(win_side_m + jc_rebate);
+  const win_average = fixFloat((win_side_a + win_side_b + win_side_m) / 3);
 
-  return { win_home, win_away, win_special, win_average }
+  return { win_side_a, win_side_b, win_side_m, win_average }
 }
 
 const triangleGoldCalc = (oddsInfo, oddsOption) => {
-  const { odds_home: a, odds_away: b, odds_special: c } = oddsInfo;
+  const { odds_side_a: a, odds_side_b: b, odds_side_m: c } = oddsInfo;
   if (!a || !b || !c) {
     return;
   }
   const { crossType, jcIndex } = oddsOption;
-  const x = GOLD_BASE;
-  const y = (a + 1) * x / (b + 1);
+  let x = GOLD_BASE;
+  let y = (a + 1) * x / (b + 1);
   let z;
   switch (crossType) {
     case 'la_wh_wa': // 全输 半赢 全赢
@@ -181,28 +182,42 @@ const triangleGoldCalc = (oddsInfo, oddsOption) => {
       z = 0;
   }
 
+  if (jcIndex == 1) {
+    const scale = GOLD_BASE / y;
+    x = x * scale;
+    y = GOLD_BASE;
+    z = z * scale;
+  }
+  else if (jcIndex == 2) {
+    const scale = GOLD_BASE / z;
+    x = x * scale;
+    y = y * scale;
+    z = GOLD_BASE;
+  }
+
   return {
-    gold_home: x,
-    gold_away: fixFloat(y),
-    gold_special: fixFloat(z),
-    odds_home: a,
-    odds_away: b,
-    odds_special: c
+    gold_side_a: x,
+    gold_side_b: fixFloat(y),
+    gold_side_m: fixFloat(z),
+    odds_side_a: a,
+    odds_side_b: b,
+    odds_side_m: c,
   };
 
 }
 
 const eventSolutions = (oddsInfo, oddsOption) => {
-  const { reverse } = oddsOption;
   const goldsInfo = triangleGoldCalc(oddsInfo, oddsOption);
   if (!goldsInfo) {
     return;
   }
   const profitInfo = triangleProfitCalc(goldsInfo, oddsOption);
+  // console.log(goldsInfo, profitInfo);
   return {
     ...goldsInfo,
     ...profitInfo,
-    reverse,
+    cross_type: oddsOption.crossType,
+    jc_index: oddsOption.jcIndex,
   }
 }
 
@@ -215,26 +230,26 @@ const eventsCombination = (passableEvents) => {
       const optimalSelections = getOptimalSelections(odds, rules);
       optimalSelections.forEach(selection => {
         const { rule, iors, index } = selection;
-        const [, , , crossType, reverse] = rule;
+        const [, , , crossType] = rule;
 
-        const oddsHome = iors[0];
-        const oddsAway = iors[1];
-        const oddsSpecial = iors[2];
+        const oddsSideA = iors[0];
+        const oddsSideB = iors[1];
+        const oddsSideM = iors[2];
         const jcIndex = iors.findIndex(item => item.p == 'jc');
-        if (!oddsHome || !oddsAway || !oddsSpecial) {
+        if (!oddsSideA || !oddsSideB || !oddsSideM) {
           return;
         }
-        const cpr = [ oddsHome, oddsAway, oddsSpecial ];
+        const cpr = [ oddsSideA, oddsSideB, oddsSideM ];
         const oddsInfo = {
-          odds_home: fixFloat(oddsHome.v - 1),
-          odds_away: fixFloat(oddsAway.v - 1),
-          odds_special: fixFloat(oddsSpecial.v - 1)
+          odds_side_a: fixFloat(oddsSideA.v - 1),
+          odds_side_b: fixFloat(oddsSideB.v - 1),
+          odds_side_m: fixFloat(oddsSideM.v - 1)
         };
-        const oddsOption = { crossType, reverse, jcIndex };
+        const oddsOption = { crossType, jcIndex };
         const sol = eventSolutions(oddsInfo, oddsOption);
         if (sol?.win_average > WIN_MIN) {
           const id = info.id;
-          const keys = cpr.map(item => item.k).join('_');
+          const keys = cpr.map(item => `${item.k}`).join('_');
           const sid = `${id}_${keys}`;
           const timestamp = Date.now();
           solutions.push({sid, sol, cpr, info, rule: `${group}:${index}`, timestamp});

+ 2 - 2
spider/index.js

@@ -51,7 +51,7 @@ const getGamesEvents = async () => {
       return;
     }
     const { matchInfoList } = value;
-    const matchList = matchInfoList.map(item => {
+    const matchList = matchInfoList?.map(item => {
       const { subMatchList } = item;
       return subMatchList.map(match => {
         const { leagueAllName, leagueId,
@@ -82,7 +82,7 @@ const getGamesEvents = async () => {
         }
       });
     });
-    return matchList.flat();
+    return matchList?.flat() ?? [];
   })
 }
 

+ 3 - 1
web/apps/web-antd/src/locales/langs/zh-CN/page.json

@@ -14,6 +14,8 @@
   "match": {
     "title": "比赛管理",
     "related": "关联比赛",
-    "relatedDesc": "管理比赛关联信息"
+    "relatedDesc": "管理比赛关联信息",
+    "centerOrder": "中单记录",
+    "centerOrderDesc": "管理中单记录信息"
   }
 }

+ 11 - 1
web/apps/web-antd/src/router/routes/modules/match.ts

@@ -7,7 +7,7 @@ const routes: RouteRecordRaw[] = [
     meta: {
       icon: 'ion:football-outline',
       order: 3,
-      title: $t('page.match.related'),
+      title: $t('page.match.title'),
     },
     name: 'Match',
     path: '/match',
@@ -22,6 +22,16 @@ const routes: RouteRecordRaw[] = [
           roles: ['admin'], // Only users with admin role can access this page
         },
       },
+      {
+        name: 'CenterOrder',
+        path: '/center-order',
+        component: () => import('#/views/match/center-order/index.vue'),
+        meta: {
+          icon: 'ion:receipt-outline',
+          title: $t('page.match.centerOrder'),
+          roles: ['admin'], // Only users with admin role can access this page
+        },
+      },
     ],
   },
 ];

+ 339 - 0
web/apps/web-antd/src/views/match/center-order/index.vue

@@ -0,0 +1,339 @@
+<script setup>
+import { Page } from '@vben/common-ui';
+import { ref } from 'vue';
+import { Tag, Card, Typography, Divider, Row, Col } from 'ant-design-vue';
+
+const { Text } = Typography;
+
+// 样例数据
+const centerOrders = ref([
+  {
+    "sid": "2031696_ior_rh_025_ior_rc_0_ior_mn",
+    "sol": {
+      "gold_home": 1000,
+      "gold_away": 678.97,
+      "gold_special": 200.31,
+      "odds_home": 0.84,
+      "odds_away": 1.71,
+      "odds_special": 2.3,
+      "win_home": -39.28,
+      "win_away": -39.27,
+      "win_special": -39.29,
+      "win_average": -39.28,
+    },
+    "cpr": [
+      {
+        "k": "ior_rh_025",
+        "p": "ob",
+        "v": 1.84,
+        "o": {
+          "ps": 1.806,
+          "ob": 1.84
+        }
+      },
+      {
+        "k": "ior_rc_0",
+        "p": "ps",
+        "v": 2.71,
+        "o": {
+          "ps": 2.71,
+          "ob": 2.51
+        }
+      },
+      {
+        "k": "ior_mn",
+        "p": "jc",
+        "v": 3.3,
+        "o": {
+          "jc": 3.3,
+          "ps": 3.44,
+          "ob": 3.65
+        }
+      }
+    ],
+    "info": {
+      "leagueName": "意大利甲级联赛",
+      "teamHomeName": "亚特兰大",
+      "teamAwayName": "罗马",
+      "id": "2031696",
+      "timestamp": 1747075500000
+    },
+    "rule": "A:7",
+    "timestamp": 1747023599690
+  },
+  {
+    "sid": "2031697_ior_rh_025_ior_rc_0_ior_mn",
+    "sol": {
+      "gold_home": 800,
+      "gold_away": 578.97,
+      "gold_special": 300.21,
+      "odds_home": 0.92,
+      "odds_away": 1.65,
+      "odds_special": 2.1,
+      "win_home": -42.28,
+      "win_away": -42.27,
+      "win_special": -42.29,
+      "win_average": -42.28,
+
+    },
+    "cpr": [
+      {
+        "k": "ior_rh_025",
+        "p": "ob",
+        "v": 1.92,
+        "o": {
+          "ps": 1.86,
+          "ob": 1.92
+        }
+      },
+      {
+        "k": "ior_rc_0",
+        "p": "ps",
+        "v": 2.65,
+        "o": {
+          "ps": 2.65,
+          "ob": 2.45
+        }
+      },
+      {
+        "k": "ior_mn",
+        "p": "jc",
+        "v": 3.1,
+        "o": {
+          "jc": 3.1,
+          "ps": 3.24,
+          "ob": 3.35
+        }
+      }
+    ],
+    "info": {
+      "leagueName": "英格兰超级联赛",
+      "teamHomeName": "曼城",
+      "teamAwayName": "阿森纳",
+      "id": "2031697",
+      "timestamp": 1747175500000
+    },
+    "rule": "A:5",
+    "timestamp": 1747023599690
+  }
+]);
+
+// 格式化时间戳为日期
+function formatDate(timestamp) {
+  const date = new Date(timestamp);
+  return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
+}
+
+// 获取平台标识
+function getPlatformName(code) {
+  const platforms = {
+    'ps': '平台A',
+    'ob': '平台B',
+    'jc': '竞彩'
+  };
+  return platforms[code] || code;
+}
+
+// 获取玩法名称
+function getBetTypeName(type) {
+  const types = {
+    'ior_rh_025': '让球(+0.25)',
+    'ior_rc_0': '大小球(0)',
+    'ior_mn': '独赢'
+  };
+  return types[type] || type;
+}
+</script>
+
+<template>
+  <Page>
+    <div class="center-order-container">
+      <div class="center-order-header">
+        <div class="center-order-header-title">
+          <span>中单记录</span>
+        </div>
+      </div>
+      <div class="center-order-content">
+        <Card v-for="(order, index) in centerOrders" :key="order.sid" class="order-card" :bordered="true">
+          <div class="order-header">
+            <div class="match-teams">
+              <Tag color="blue">{{ order.info.leagueName }}</Tag>
+              <span class="team-names">{{ order.info.teamHomeName }} vs {{ order.info.teamAwayName }}</span>
+            </div>
+            <div class="match-info">
+              <Tag color="green">ID: {{ order.info.id }}</Tag>
+              <Tag color="orange">规则: {{ order.rule }}</Tag>
+              <Tag color="purple">比赛时间: {{ formatDate(order.info.timestamp) }}</Tag>
+              <Tag color="cyan">记录时间: {{ formatDate(order.timestamp) }}</Tag>
+            </div>
+          </div>
+
+          <Divider>投入金额与赔率</Divider>
+
+          <Row class="sol-info">
+            <Col :span="8">
+            <div class="sol-item">
+              <Text type="secondary">主队投入:</Text>
+              <Text strong>{{ order.sol.gold_home }}</Text>
+            </div>
+            <div class="sol-item">
+              <Text type="secondary">主队赔率:</Text>
+              <Text strong>{{ order.sol.odds_home }}</Text>
+            </div>
+            <div class="sol-item">
+              <Text type="secondary">主队收益:</Text>
+              <Text :type="order.sol.win_home >= 0 ? 'success' : 'danger'">{{ order.sol.win_home }}</Text>
+            </div>
+            </Col>
+            <Col :span="8">
+            <div class="sol-item">
+              <Text type="secondary">客队投入:</Text>
+              <Text strong>{{ order.sol.gold_away }}</Text>
+            </div>
+            <div class="sol-item">
+              <Text type="secondary">客队赔率:</Text>
+              <Text strong>{{ order.sol.odds_away }}</Text>
+            </div>
+            <div class="sol-item">
+              <Text type="secondary">客队收益:</Text>
+              <Text :type="order.sol.win_away >= 0 ? 'success' : 'danger'">{{ order.sol.win_away }}</Text>
+            </div>
+            </Col>
+            <Col :span="8">
+            <div class="sol-item">
+              <Text type="secondary">特殊投入:</Text>
+              <Text strong>{{ order.sol.gold_special }}</Text>
+            </div>
+            <div class="sol-item">
+              <Text type="secondary">特殊赔率:</Text>
+              <Text strong>{{ order.sol.odds_special }}</Text>
+            </div>
+            <div class="sol-item">
+              <Text type="secondary">特殊收益:</Text>
+              <Text :type="order.sol.win_special >= 0 ? 'success' : 'danger'">{{ order.sol.win_special }}</Text>
+            </div>
+            </Col>
+          </Row>
+
+          <div class="sol-summary">
+            <Text type="secondary">平均收益:</Text>
+            <Text :type="order.sol.win_average >= 0 ? 'success' : 'danger'" strong>{{ order.sol.win_average }}</Text>
+            <Text v-if="order.sol.reverse" type="warning" class="reverse-flag">逆向</Text>
+          </div>
+
+          <Divider>比赛投注数据</Divider>
+
+          <div class="cpr-container">
+            <Card v-for="(bet, betIndex) in order.cpr" :key="betIndex" class="bet-card" size="small">
+              <div class="bet-header">
+                <Tag color="blue">{{ getBetTypeName(bet.k) }}</Tag>
+                <Tag color="green">{{ getPlatformName(bet.p) }}</Tag>
+                <Text strong>赔率: {{ bet.v }}</Text>
+              </div>
+              <div class="bet-details">
+                <div v-for="(oddValue, oddKey) in bet.o" :key="oddKey" class="odd-item">
+                  <Text type="secondary">{{ getPlatformName(oddKey) }}:</Text>
+                  <Text :strong="oddKey === bet.p">{{ oddValue }}</Text>
+                </div>
+              </div>
+            </Card>
+          </div>
+        </Card>
+      </div>
+    </div>
+  </Page>
+</template>
+
+<style lang="scss" scoped>
+.center-order-container {
+  padding: 16px;
+
+  .order-card {
+    margin-bottom: 20px;
+
+    .order-header {
+      display: flex;
+      justify-content: space-between;
+      flex-wrap: wrap;
+      margin-bottom: 12px;
+
+      .match-teams {
+        font-size: 16px;
+
+        .team-names {
+          margin-left: 8px;
+          font-weight: bold;
+        }
+      }
+
+      .match-info {
+        :deep(.ant-tag) {
+          margin-right: 8px;
+        }
+      }
+    }
+
+    .sol-info {
+      margin-bottom: 16px;
+
+      .sol-item {
+        margin-bottom: 8px;
+        display: flex;
+        justify-content: space-between;
+        padding: 0 16px;
+      }
+    }
+
+    .sol-summary {
+      text-align: right;
+      font-size: 16px;
+      margin-top: 8px;
+      padding: 8px 16px;
+      background-color: #f8f8f8;
+      border-radius: 4px;
+
+      .reverse-flag {
+        margin-left: 16px;
+        font-weight: bold;
+      }
+    }
+
+    .cpr-container {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 16px;
+
+      .bet-card {
+        width: calc(33.33% - 11px);
+
+        .bet-header {
+          margin-bottom: 8px;
+          display: flex;
+          align-items: center;
+          gap: 8px;
+        }
+
+        .bet-details {
+          .odd-item {
+            display: flex;
+            justify-content: space-between;
+            margin-bottom: 4px;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .center-order-container {
+    .order-card {
+      .cpr-container {
+        .bet-card {
+          width: 100%;
+        }
+      }
+    }
+  }
+}
+</style>

+ 0 - 67
web/apps/web-antd/src/views/match/components/game_item.vue

@@ -1,67 +0,0 @@
-<script>
-export default {
-  data() {
-    return {
-    }
-  },
-  props: {
-    eventId: {
-      type: Number
-    },
-    leagueName: {
-      type: String
-    },
-    teamHomeName: {
-      type: String
-    },
-    teamAwayName: {
-      type: String
-    },
-    dateTime: {
-      type: String
-    },
-    selected: {
-      type: Boolean
-    },
-    disabled: {
-      type: Boolean
-    },
-    autoScroll: {
-      type: Boolean
-    },
-    hideControl: {
-      type: Boolean
-    }
-  },
-  watch: {
-    selected(newVlaue) {
-      if (this.autoScroll && newVlaue) {
-        this.$refs['gameItem'].scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
-      }
-    }
-  },
-  computed: {
-  },
-  methods: {
-    hideItem() {
-      this.$emit('hide');
-    }
-  },
-  mounted() {
-  }
-}
-</script>
-
-<template>
-  <div class="game-item" :class="{ selected, disabled }" ref="gameItem">
-    <div class="league-name">{{ leagueName }}</div>
-    <div class="team-name team-home">{{ teamHomeName }}</div>
-    <div class="team-name team-away">{{ teamAwayName }}</div>
-    <div class="date-time">{{ dateTime }}</div>
-    <div class="hide-control" v-if="hideControl">
-      <button type="button" @click.stop="hideItem">隐藏</button>
-    </div>
-  </div>
-</template>
-
-<style scoped></style>

+ 91 - 0
web/apps/web-antd/src/views/match/components/match_item.vue

@@ -0,0 +1,91 @@
+<script>
+export default {
+  data() {
+    return {
+    }
+  },
+  props: {
+    eventId: {
+      type: Number
+    },
+    leagueName: {
+      type: String
+    },
+    teamHomeName: {
+      type: String
+    },
+    teamAwayName: {
+      type: String
+    },
+    dateTime: {
+      type: String
+    },
+    selected: {
+      type: Boolean
+    },
+    disabled: {
+      type: Boolean
+    },
+    autoScroll: {
+      type: Boolean
+    },
+  },
+  watch: {
+    selected(newVlaue) {
+      if (this.autoScroll && newVlaue) {
+        this.$refs['gameItem'].scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
+      }
+    }
+  },
+  computed: {},
+  methods: {},
+  mounted() {
+  }
+}
+</script>
+
+<template>
+  <div class="match-item" :class="{ selected, disabled }" ref="gameItem">
+    <div class="match-league">{{ leagueName }}</div>
+    <div class="match-team team-home">{{ teamHomeName }}</div>
+    <div class="match-team team-away">{{ teamAwayName }}</div>
+    <div class="match-time">{{ dateTime }}</div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+
+.match-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  padding: 10px 15px;
+  &.selected {
+    background-color: #e9f3fe;
+  }
+}
+
+.match-league {
+  font-size: 16px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 4px;
+}
+
+.match-team {
+  font-size: 14px;
+  line-height: 20px;
+  &.team-home {
+    color: #0958d9;
+  }
+  &.team-away {
+    color: #f5222d;
+  }
+}
+
+.match-time {
+  font-size: 12px;
+  color: #888;
+  margin-top: 4px;
+}
+</style>

+ 367 - 41
web/apps/web-antd/src/views/match/related/index.vue

@@ -1,100 +1,426 @@
 <script setup>
 import { Page } from '@vben/common-ui';
 import { requestClient } from '#/api/request';
-import { Button, message } from 'ant-design-vue';
-import { ref, onMounted } from 'vue';
+import { Button, Modal, message } from 'ant-design-vue';
+import { ref, reactive, computed, onMounted, useTemplateRef } from 'vue';
 import dayjs from 'dayjs';
-import gameItem from '../components/game_item.vue';
-
-const gamesRelations = ref([]);
-const gamesInfo = ref({});
-const gamesTime = ref({});
-const currentRelation = ref({});
-const selectedTime = ref(-1);
-const selectedKeyword = ref('');
+import MatchItem from '../components/match_item.vue';
+
+const gamesList = reactive({});
+const gamesRelations = reactive({});
+const currentRelation = reactive({});
 const selectedInfo = ref(null);
 
-async function getGamesInfo() {
+const formatDate = (timestamp) => {
+  return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss');
+}
+
+const formatGameItem = (game, platform) => {
+  const selected = currentRelation[platform]?.eventId == game.eventId;
+  const { timestamp } = game;
+  const dateTime = formatDate(timestamp);
+  return { ...game, dateTime, selected };
+}
+
+const setGameOrderWeight= (game) => {
+  const { t, l, h, a } = selectedInfo.value ?? {};
+  game.orderWeight = 0;
+  const { leagueName, teamHomeName, teamAwayName, timestamp } = game;
+  if (timestamp == t) {
+    game.orderWeight += 1;
+  }
+  if (leagueName.startsWith(l)) {
+    game.orderWeight += 1;
+  }
+  if (teamHomeName.startsWith(h)) {
+    game.orderWeight += 1;
+  }
+  if (teamAwayName.startsWith(a)) {
+    game.orderWeight += 1;
+  }
+  return game;
+}
+
+const getGameOrderList = (platform) => {
+  let games = gamesList[platform]?.games ?? [];
+  const relatedGames = new Set(Object.values(gamesRelations).map(item => item[platform]?.eventId));
+  games = games.map(game => formatGameItem(game, platform));
+  if (platform == 'jc') {
+    return games.filter(game => !relatedGames.has(game.eventId))
+    .sort((a, b) => {
+      if (a.selected) {
+        return -1;
+      }
+      return 1;
+    });
+  }
+  return games.map(setGameOrderWeight)
+  .sort((a, b) => b.orderWeight - a.orderWeight)
+  .filter(game => {
+    if (game.orderWeight > 0 && !relatedGames.has(game.eventId)) {
+      return true;
+    }
+    return false;
+  });
+}
+
+const showRelationButton = computed(() => {
+  return currentRelation.ps || currentRelation.ob;
+});
+
+const relationsList = computed(() => {
+  return Object.keys(gamesRelations).map(id => {
+    const rel = gamesRelations[id];
+    Object.values(rel).forEach(item => {
+      item.dateTime = formatDate(item.timestamp);
+    });
+    return { id, rel };
+  });
+});
+
+const jcGamesList = computed(() => {
+  return getGameOrderList('jc');
+});
+
+const psGamesList = computed(() => {
+  return getGameOrderList('ps');
+});
+
+const obGamesList = computed(() => {
+  return getGameOrderList('ob');
+});
+
+const getGamesList = async () => {
   try {
     const data = await requestClient.get('/triangle/get_games_list');
     return data;
-  } catch (error) {
+  }
+  catch (error) {
     console.error('Failed to fetch games info:', error);
     message.error('获取比赛信息失败');
     return [];
   }
 }
 
-async function getGamesRelations() {
+const getGamesRelations = async () => {
   try {
     const data = await requestClient.get('/triangle/get_games_relation');
     return data;
-  } catch (error) {
+  }
+  catch (error) {
     console.error('Failed to fetch game relations:', error);
     message.error('获取比赛关系失败');
     return [];
   }
 }
 
-async function setGamesRelation() {
-  const rel = currentRelation.value;
+const setGamesRelation = () => {
+  const rel = currentRelation;
+  Object.keys(rel).forEach(key => {
+    if (!rel[key]) {
+      delete rel[key];
+    }
+  });
   Object.values(rel).forEach(item => {
-    console.log(item);
     delete item.orderWeight;
   });
   const id = rel['jc']?.eventId;
   if (!id) {
-    return Promise.reject('没有选择竞彩的比赛');
+    console.log('没有选择竞彩的比赛');
+    message.warn('设置比赛关系失败');
   }
-  try {
-    await requestClient.post('/triangle/update_games_relation', { id, rel });
-    return id;
-  } catch (error) {
+  requestClient.post('/triangle/update_games_relation', { id, rel })
+  .then(res => {
+    console.log('设置比赛关系成功', res);
+    message.success('设置比赛关系成功');
+    selectedInfo.value = null;
+    currentRelation.jc = currentRelation.ps = currentRelation.ob = null;
+    updateGamesRelations();
+  })
+  .catch(error => {
     console.error('Failed to set game relation:', error);
     message.error('设置比赛关系失败');
-  }
+  });
 }
 
-async function removeGamesRelation(id) {
-  try {
-    await requestClient.post('/triangle/remove_games_relation', { id });
-  } catch (error) {
+const removeGamesRelation = (id) => {
+  requestClient.post('/triangle/remove_games_relation', { id })
+  .then(res => {
+    console.log('删除比赛关系成功', res);
+    message.success('删除比赛关系成功');
+    updateGamesRelations();
+  })
+  .catch(error => {
     console.error('Failed to remove game relation:', error);
     message.error('删除比赛关系失败');
-  }
+  });
 }
 
-function updateGamesInfo() {
-  getGamesInfo().then((data) => {
-    gamesInfo.value = data;
+const openRelationModal = () => {
+  relationModalVisible.value = true;
+}
+
+const updateGamesList = async () => {
+  const data = await getGamesList();
+  Object.keys(data).forEach(key => {
+    gamesList[key] = data[key];
   });
 }
 
-function updateGamesRelations() {
-  getGamesRelations().then((data) => {
-    gamesRelations.value = data;
+const updateGamesRelations = async () => {
+  const data = await getGamesRelations();
+  data.forEach(item => {
+    const { id, rel } = item;
+    gamesRelations[id] = rel;
+  });
+  const newIds = new Set(data.map(item => item.id));
+  Object.keys(gamesRelations).forEach(id => {
+    if (!newIds.has(id)) {
+      delete gamesRelations[id];
+    }
   });
 }
 
+const selectGame = (platform, game) => {
+  const { leagueId, eventId, timestamp, leagueName, teamHomeName, teamAwayName, selected } = game;
+  if (selected) {
+    currentRelation[platform] = null;
+  }
+  else {
+    currentRelation[platform] = { leagueId, eventId, timestamp, leagueName, teamHomeName, teamAwayName };
+  }
+  if (platform == 'jc') {
+    currentRelation.ps = null;
+    currentRelation.ob = null;
+    if (selected) {
+      selectedInfo.value = null;
+    }
+    else {
+      selectedInfo.value = {
+        t: timestamp,
+        l: leagueName,
+        h: teamHomeName,
+        a: teamAwayName,
+      }
+    }
+  }
+}
+
+const getTopPanelPosition = () => {
+  const topPanel = useTemplateRef('topPanel');
+}
+
 onMounted(() => {
-  updateGamesInfo();
+  updateGamesList();
   updateGamesRelations();
+  getTopPanelPosition();
 });
-
-
 </script>
 
 <template>
   <Page>
-    <div class="match-related-container">
-      <div class="match-related-header">
-        <div class="match-related-header-title">
-          <span>比赛关系</span>
+    <div class="top-panel" ref="topPanel">
+      <span>竞彩</span>
+      <span>平博</span>
+      <span>OB</span>
+      <i>{{ relationsList.length }}</i>
+    </div>
+
+    <div class="match-list" v-if="relationsList.length">
+      <div class="match-row" v-for="({ id, rel }) in relationsList" :key="id">
+        <MatchItem
+          :eventId="rel.jc.eventId"
+          :leagueName="rel.jc.leagueName"
+          :teamHomeName="rel.jc.teamHomeName"
+          :teamAwayName="rel.jc.teamAwayName"
+          :dateTime="rel.jc.dateTime" />
+
+        <MatchItem  v-if="rel.ps"
+          :eventId="rel.ps.eventId"
+          :leagueName="rel.ps.leagueName"
+          :teamHomeName="rel.ps.teamHomeName"
+          :teamAwayName="rel.ps.teamAwayName"
+          :dateTime="rel.ps.dateTime" />
+        <div class="match-item match-item-holder" v-else></div>
+
+        <MatchItem v-if="rel.ob"
+          :eventId="rel.ob.eventId"
+          :leagueName="rel.ob.leagueName"
+          :teamHomeName="rel.ob.teamHomeName"
+          :teamAwayName="rel.ob.teamAwayName"
+          :dateTime="rel.ob.dateTime" />
+        <div class="match-item match-item-holder" v-else></div>
+
+        <div class="match-action">
+          <Button type="link" class="action-btn" @click="removeGamesRelation(id)">
+            <i class="delete-icon"></i>
+          </Button>
         </div>
       </div>
     </div>
+    <div class="match-list col-list" v-if="jcGamesList.length">
+      <div class="match-col">
+        <MatchItem v-for="game in jcGamesList" :key="game.id"
+          :eventId="game.eventId"
+          :leagueName="game.leagueName"
+          :teamHomeName="game.teamHomeName"
+          :teamAwayName="game.teamAwayName"
+          :dateTime="game.dateTime"
+          :selected="game.selected"
+          @click="selectGame('jc', game)" />
+      </div>
+      <div class="match-col">
+        <MatchItem v-for="game in psGamesList" :key="game.id"
+          :eventId="game.eventId"
+          :leagueName="game.leagueName"
+          :teamHomeName="game.teamHomeName"
+          :teamAwayName="game.teamAwayName"
+          :dateTime="game.dateTime"
+          :selected="game.selected"
+          @click="selectGame('ps', game)" />
+      </div>
+      <div class="match-col">
+        <MatchItem v-for="game in obGamesList" :key="game.id"
+          :eventId="game.eventId"
+          :leagueName="game.leagueName"
+          :teamHomeName="game.teamHomeName"
+          :teamAwayName="game.teamAwayName"
+          :dateTime="game.dateTime"
+          :selected="game.selected"
+          @click="selectGame('ob', game)" />
+      </div>
+      <div class="match-action">
+        <Button type="link" class="action-btn" v-if="showRelationButton" @click="setGamesRelation">
+          <i class="link-icon"></i>
+        </Button>
+      </div>
+    </div>
+
+
+    <div class="list-empty" v-if="!relationsList.length && !jcGamesList.length">暂无数据</div>
+
   </Page>
 </template>
 
 <style lang="scss" scoped>
+.top-panel {
+  display: flex;
+  margin-bottom: 5px;
+  border: 1px solid #e8e8e8;
+  border-radius: 6px;
+  background-color: #fff;
+  span, i {
+    display: block;
+  }
+  span {
+    flex: 1;
+    text-align: center;
+    &:not(:last-child) {
+      border-right: 1px solid #e8e8e8;
+    }
+  }
+  i {
+    width: 50px;
+    text-align: center;
+    font-style: normal;
+  }
+}
+
+.match-list {
+  margin-bottom: 5px;
+  border: 1px solid #e8e8e8;
+  border-radius: 6px;
+  background-color: #fff;
+  overflow: hidden;
+  &.col-list {
+    display: flex;
+  }
+}
+
+.match-row {
+  display: flex;
+  flex-wrap: wrap;
+  &:not(:last-child) {
+    border-bottom: 1px solid #e8e8e8;
+  }
+  .match-item {
+    &:not(:last-child) {
+      border-right: 1px solid #e8e8e8;
+    }
+  }
+}
+
+.match-col {
+  flex: 1;
+  &:not(:last-child) {
+    border-right: 1px solid #e8e8e8;
+  }
+  .match-item {
+    border-bottom: 1px solid #e8e8e8;
+    &:last-child {
+      margin-bottom: -1px;
+    }
+  }
+}
+
+.match-item-holder {
+  flex: 1;
+  padding: 10px 15px;
+}
+
+.match-action {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 50px;
+}
+
+.action-btn {
+  padding: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  .col-list & {
+    position: relative;
+    top: 40px;
+    align-self: flex-start;
+  }
+}
+
+.link-icon {
+  display: inline-block;
+  width: 16px;
+  height: 16px;
+  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>');
+  background-size: cover;
+  opacity: 0.5;
+}
+
+.delete-icon {
+  display: inline-block;
+  width: 16px;
+  height: 16px;
+  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>');
+  background-size: cover;
+  opacity: 0.5;
+}
+
+.list-empty {
+  text-align: center;
+  padding: 10px;
+  font-size: 18px;
+  color: #999;
+}
+
+@media (max-width: 768px) {
+  .match-item {
+    min-width: calc(50% - 16px);
+  }
+}
+
+@media (max-width: 480px) {
+  .match-item {
+    min-width: calc(100% - 16px);
+  }
+}
 </style>