Procházet zdrojové kódy

Merge branch 'dev'

flyzto před 2 dny
rodič
revize
21d4923185

+ 79 - 46
pinnacle/main.js

@@ -233,13 +233,22 @@ const getStraightOdds = async () => {
     }
     GLOBAL_DATA.straightOddsVersion = last;
     const games = leagues?.flatMap(league => league.events);
+    // return games;
     return games?.map(item => {
       const { periods, ...rest } = item;
-      const period = periods?.find(period => period.number == 0);
-      if (!period) {
-        return rest;
+      const straight = periods?.find(period => period.number == 0);
+      const straight1st = periods?.find(period => period.number == 1);
+      const gameInfo = rest;
+      if (straight || straight1st) {
+        gameInfo.periods = {};
       }
-      return { ...rest, period };
+      if (straight) {
+        gameInfo.periods.straight = straight;
+      }
+      if (straight1st) {
+        gameInfo.periods.straight1st = straight1st;
+      }
+      return gameInfo;
     }) ?? [];
   });
 }
@@ -251,10 +260,16 @@ const updateStraightOdds = async () => {
     if (games.length) {
       const { gamesMap } = GLOBAL_DATA;
       games.forEach(game => {
-        const { id, ...rest } = game;
+        const { id, periods, ...rest } = game;
         const localGame = gamesMap[id];
         if (localGame) {
           Object.assign(localGame, rest);
+          if (!localGame.periods && periods) {
+            localGame.periods = periods;
+          }
+          else if (periods) {
+            Object.assign(localGame.periods, periods);
+          }
         }
       });
     }
@@ -286,16 +301,16 @@ const getSpecialFixtures = async () => {
       const { specials } = league;
       return specials?.filter(special => special.event)
       .map(special => {
-        const { event: { id: eventId }, ...rest } = special ?? { event: {} };
-        return { eventId, ...rest };
+        const { event: { id: eventId, periodNumber }, ...rest } = special ?? { event: {} };
+        return { eventId, periodNumber, ...rest };
       }) ?? [];
     })
     .flat()
     .filter(special => {
-      if (special.name != 'Winning Margin' && special.name != 'Exact Total Goals') {
-        return false;
+      if (special.name.includes('Winning Margin') || special.name.includes('Exact Total Goals')) {
+        return true;
       }
-      return true;
+      return false;
     }) ?? [];
     const update = since == 0 ? 'full' : 'increment';
     return { specials, update };
@@ -323,6 +338,17 @@ const mergeContestants = (localContestants=[], remoteContestants=[]) => {
   return localContestants;
 }
 
+const mergeSpecials = (localSpecials, remoteSpecials, specialName) => {
+  if (localSpecials[specialName] && remoteSpecials[specialName]) {
+    const { contestants: specialContestants, ...specialRest } = remoteSpecials[specialName];
+    Object.assign(localSpecials[specialName], specialRest);
+    mergeContestants(localSpecials[specialName].contestants, specialContestants);
+  }
+  else if (remoteSpecials[specialName]) {
+    localSpecials[specialName] = remoteSpecials[specialName];
+  }
+}
+
 const updateSpecialFixtures = async () => {
   return getSpecialFixtures()
   .then(data => {
@@ -341,6 +367,12 @@ const updateSpecialFixtures = async () => {
         else if (special.name == 'Exact Total Goals') {
           gamesSpecialsMap[eventId].exactTotalGoals = special;
         }
+        else if (special.name == 'Winning Margin 1st Half') {
+          gamesSpecialsMap[eventId].winningMargin1st = special;
+        }
+        else if (special.name == 'Exact Total Goals 1st Half') {
+          gamesSpecialsMap[eventId].exactTotalGoals1st = special;
+        }
       });
 
       Object.keys(gamesSpecialsMap).forEach(eventId => {
@@ -355,31 +387,11 @@ const updateSpecialFixtures = async () => {
         const localSpecials = gamesMap[eventId].specials;
         const remoteSpecials = gamesSpecialsMap[eventId];
 
-        if (localSpecials.winningMargin && remoteSpecials.winningMargin) {
-          const { contestants: winningMarginContestants, ...winningMarginRest } = remoteSpecials.winningMargin;
-          Object.assign(localSpecials.winningMargin, winningMarginRest);
-          mergeContestants(localSpecials.winningMargin.contestants, winningMarginContestants);
-        }
-        // else if (localSpecials.winningMargin && !remoteSpecials.winningMargin) {
-        //   Logs.outDev('delete winningMargin', localSpecials.winningMargin);
-        //   delete localSpecials.winningMargin;
-        // }
-        else if (remoteSpecials.winningMargin) {
-          localSpecials.winningMargin = remoteSpecials.winningMargin;
-        }
+        mergeSpecials(localSpecials, remoteSpecials, 'winningMargin');
+        mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals');
+        mergeSpecials(localSpecials, remoteSpecials, 'winningMargin1st');
+        mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals1st');
 
-        if (localSpecials.exactTotalGoals && remoteSpecials.exactTotalGoals) {
-          const { contestants: exactTotalGoalsContestants, ...exactTotalGoalsRest } = remoteSpecials.exactTotalGoals;
-          Object.assign(localSpecials.exactTotalGoals, exactTotalGoalsRest);
-          mergeContestants(localSpecials.exactTotalGoals.contestants, exactTotalGoalsContestants);
-        }
-        // else if (localSpecials.exactTotalGoals && !remoteSpecials.exactTotalGoals) {
-        //   Logs.outDev('delete exactTotalGoals', localSpecials.exactTotalGoals);
-        //   delete localSpecials.exactTotalGoals;
-        // }
-        else if (remoteSpecials.exactTotalGoals) {
-          localSpecials.exactTotalGoals = remoteSpecials.exactTotalGoals;
-        }
       });
     }
   });
@@ -545,8 +557,11 @@ const parseTotals = (totals) => {
   return events;
 }
 
-const parsePeriod = (period, wm) => {
-  const { cutoff='', status=0, spreads=[], moneyline={}, totals=[] } = period;
+const parseStraight = (straight, wm) => {
+  if (!straight) {
+    return null;
+  }
+  const { cutoff='', status=0, spreads=[], moneyline={}, totals=[] } = straight;
   const cutoffTime = new Date(cutoff).getTime();
   const nowTime = Date.now();
 
@@ -563,6 +578,9 @@ const parsePeriod = (period, wm) => {
 }
 
 const parseWinningMargin = (winningMargin, home, away) => {
+  if (!winningMargin) {
+    return null;
+  }
   const { cutoff='', status='', contestants=[] } = winningMargin;
   const cutoffTime = new Date(cutoff).getTime();
   const nowTime = Date.now();
@@ -594,6 +612,9 @@ const parseWinningMargin = (winningMargin, home, away) => {
 }
 
 const parseExactTotalGoals = (exactTotalGoals) => {
+  if (!exactTotalGoals) {
+    return null;
+  }
   const { cutoff='', status='', contestants=[] } = exactTotalGoals;
   const cutoffTime = new Date(cutoff).getTime();
   const nowTime = Date.now();
@@ -630,17 +651,28 @@ const parseRbState = (state) => {
 }
 
 const parseGame = (game) => {
-  const { eventId=0, originId=0, period={}, specials={}, home, away, marketType, state, elapsed, homeScore=0, awayScore=0 } = game;
-  const { winningMargin={}, exactTotalGoals={} } = specials;
+  const { eventId=0, originId=0, periods={}, specials={}, home, away, marketType, state, elapsed, homeScore=0, awayScore=0 } = game;
+  const { straight, straight1st } = periods;
+  const { winningMargin={}, exactTotalGoals={}, winningMargin1st={}, exactTotalGoals1st={} } = specials;
+  const filtedGamesSet = new Set(GLOBAL_DATA.filtedGames);
   const wm = homeScore - awayScore;
   const score = `${homeScore}-${awayScore}`;
-  const events = parsePeriod(period, wm) ?? {};
+  const events = parseStraight(straight, wm) ?? {};
   const stage = parseRbState(state);
   const retime = elapsed ? `${elapsed}'` : '';
   const evtime = Date.now();
   Object.assign(events, parseWinningMargin(winningMargin, home, away));
   Object.assign(events, parseExactTotalGoals(exactTotalGoals));
-  return { eventId, originId, events, evtime, stage, retime, score, wm, marketType };
+  const gameInfos = [];
+  gameInfos.push({ eventId, originId, events, evtime, stage, retime, score, wm, marketType });
+  const halfEventId = eventId * -1;
+  if (filtedGamesSet.has(halfEventId)) {
+    const events = parseStraight(straight1st, wm) ?? {};
+    Object.assign(events, parseWinningMargin(winningMargin1st, home, away));
+    Object.assign(events, parseExactTotalGoals(exactTotalGoals1st));
+    gameInfos.push({ eventId: halfEventId, originId, events, evtime, stage, retime, score, wm, marketType });
+  }
+  return gameInfos;
 }
 
 
@@ -689,12 +721,13 @@ const getGames = () => {
     }
 
     if (actived) {
-      const gameInfo = parseGame(game);
-      const { marketType, ...rest } = gameInfo;
-      if (!gamesData[marketType]) {
-        gamesData[marketType] = [];
-      }
-      gamesData[marketType].push(rest);
+      parseGame(game).forEach(gameInfo => {
+        const { marketType, ...rest } = gameInfo;
+        if (!gamesData[marketType]) {
+          gamesData[marketType] = [];
+        }
+        gamesData[marketType].push(rest);
+      });
     }
   });
   return gamesData;

+ 3 - 2
server/init.js

@@ -20,12 +20,13 @@ const Logs = require('./libs/logs');
     hgMaxDiff: 0,
     pcRebateRatio: 0,
     pcRebateType: 0,
-    expireTimeEvents: 45000,
-    expireTimeSpecial: 60000,
     subsidyTime: 0,
     subsidyAmount: 0,
     subsidyRbWmAmount: 0,
     subsidyRbOtAmount: 0,
+    halfTimeActiveTime: 0,
+    expireTimeEvents: 45000,
+    expireTimeSpecial: 60000,
     syncSettingEnabled: false,
     runWorkerEnabled: false,
   })

+ 51 - 8
server/models/GamesPs.js

@@ -639,6 +639,9 @@ const fetchGamesRelation = async (mk='') => {
   }).then(({ data: resData }) => {
     if (resData.code == 0) {
       const nowTime = Date.now();
+      const halfTimeActiveTime = getSetting('halfTimeActiveTime');
+      const activeHalfTime = nowTime + halfTimeActiveTime * 60 * 60 * 1000;
+      const inactiveHalfTime = nowTime + 60 * 1000;
       const gamesRelation = resData.data?.filter?.((item) => {
         const timestamp = new Date(item.timestamp).getTime();
         const updated = new Date(item.updated_at).getTime();
@@ -702,8 +705,27 @@ const fetchGamesRelation = async (mk='') => {
             timestamp
           } : null
         };
-        return { id: ps_event_id, mk, rel, timestamp, updated };
-      }) ?? [];
+        const rels = [];
+        rels.push({ id: ps_event_id, mk, rel, timestamp, updated });
+        if (timestamp < activeHalfTime && timestamp > inactiveHalfTime) {
+          const halfRel = {};
+          Object.keys(rel).forEach(platform => {
+            const game = rel[platform];
+            if (game) {
+              const { eventId, ...gameInfo } = game;
+              halfRel[platform] = {
+                eventId: eventId*-1,
+                ...gameInfo
+              };
+            }
+            else {
+              halfRel[platform] = null;
+            }
+          });
+          rels.push({ id: ps_event_id*-1, mk, rel: halfRel, timestamp, updated });
+        }
+        return rels;
+      }).flat() ?? [];
       return gamesRelation;
     }
     return Promise.reject(new Error(resData.message));
@@ -790,7 +812,6 @@ const getGamesRelation = ({ mk=-1, ids, listEvents=false, listPC=false } = {}) =
 const updateGamesRelation = () => {
   fetchGamesRelation()
   .then(gamesRelation => {
-    // Logs.out('updateGamesRelation', gamesRelation);
     const baseList = {};
     gamesRelation.map(item => {
       const baseGame = item.rel?.['ps'] ?? {};
@@ -1080,9 +1101,19 @@ const getSolutions = async ({ win_min, with_events, show_lower=false, mk=-1 }) =
 /**
  * 获取中单方案并按照比赛分组
  */
-const getGamesSolutions = async ({ win_min, with_events, show_lower=false, mk=-1, tp=0, sk }) => {
+const getGamePeriod = (id) => {
+  if (id > 0) {
+    return 0;
+  }
+  else if (id < 0) {
+    return 1;
+  }
+  return -1;
+}
+const getGamesSolutions = async ({ win_min, with_events, show_lower=false, mk=-1, gp=-1, tp=0, sk }) => {
 
   const filterMarketType = +mk;
+  const filterGamePeriod = +gp;
   const filterDataType = +tp;
   const { minShowAmount } = getSetting();
   const solutionsList = Object.values(GAMES.Solutions);
@@ -1105,7 +1136,10 @@ const getGamesSolutions = async ({ win_min, with_events, show_lower=false, mk=-1
     if (!show_lower && lower) {
       return false;
     }
-    if ((filterDataType == 0 || filterDataType == ruleType) && (!!sk || win_average >= (win_min ?? minShowAmount))) {
+    if ((filterDataType == 0 || filterDataType == ruleType) &&
+      (!!sk || win_average >= (win_min ?? minShowAmount)) &&
+      (filterGamePeriod == -1 || filterGamePeriod == getGamePeriod(id))
+    ) {
       const gameRelation = relationsMap.get(id);
       if (!solutionsMap[id]) {
         solutionsMap[id] = { ...gameRelation, solutions: [] };
@@ -1154,9 +1188,18 @@ const getSolutionsByIds = async (ids) => {
   const result = {};
   ids.forEach(id => {
     const baseGame = baseMap.get(id);
-    result[id] = {};
-    result[id].matches = baseGame?.matches ?? [];
-    result[id].sols = [];
+    if (baseGame) {
+      result[id] = {};
+      result[id].matches = baseGame.matches ?? [];
+      result[id].sols = [];
+    }
+    const haflId = id * -1;
+    const halfGame = baseMap.get(haflId);
+    if (halfGame) {
+      result[haflId] = {};
+      result[haflId].matches = halfGame.matches ?? [];
+      result[haflId].sols = [];
+    }
   });
   Object.values(GAMES.Solutions).forEach(item => {
     const { info: { id }, lower } = item;

+ 14 - 9
server/models/Setting.js

@@ -86,35 +86,40 @@ const systemSettingSchema = new Schema({
     required: true,
     default: 0
   },
-  expireTimeEvents: {
+  subsidyTime: {
     type: Number,
     required: true,
-    default: 45000
+    default: 0
   },
-  expireTimeSpecial: {
+  subsidyAmount: {
     type: Number,
     required: true,
-    default: 60000
+    default: 0
   },
-  subsidyTime: {
+  subsidyRbWmAmount: {
     type: Number,
     required: true,
     default: 0
   },
-  subsidyAmount: {
+  subsidyRbOtAmount: {
     type: Number,
     required: true,
     default: 0
   },
-  subsidyRbWmAmount: {
+  halfTimeActiveTime: {
     type: Number,
     required: true,
     default: 0
   },
-  subsidyRbOtAmount: {
+  expireTimeEvents: {
     type: Number,
     required: true,
-    default: 0
+    default: 45000
+  },
+  expireTimeSpecial: {
+    type: Number,
+    required: true,
+    default: 60000
   },
   syncSettingEnabled: {
     type: Boolean,

+ 56 - 18
server/routes/pstery.js

@@ -3,7 +3,9 @@ const router = express.Router();
 
 const Games = require('../models/GamesPs');
 
-// 更新比赛列表
+/**
+ * 更新比赛列表
+ */
 router.post('/update_games_list', (req, res) => {
   const { platform, mk, games } = req.body ?? {};
   Games.updateGamesList({ platform, mk, games })
@@ -15,7 +17,9 @@ router.post('/update_games_list', (req, res) => {
   })
 });
 
-// 更新比赛盘口
+/**
+ * 更新比赛盘口
+ */
 router.post('/update_games_events', (req, res) => {
   const { platform, mk, games, outrights, timestamp, tp } = req.body ?? {};
   Games.updateGamesEvents({ platform, mk, games, outrights, timestamp, tp })
@@ -27,7 +31,9 @@ router.post('/update_games_events', (req, res) => {
   })
 });
 
-// 更新内盘盘口
+/**
+ * 更新内盘盘口
+ */
 router.post('/update_base_events', (req, res) => {
   const { games, timestamp, tp } = req.body ?? {};
   Games.updateBaseEvents({ games, timestamp, tp })
@@ -39,14 +45,19 @@ router.post('/update_base_events', (req, res) => {
   });
 });
 
-// 更新联赛列表
+
+/**
+ * 更新联赛列表
+ */
 router.post('/update_leagues_list', (req, res) => {
   const { mk, leagues, platform } = req.body ?? {};
   const updateCount = Games.updateLeaguesList({ mk, leagues, platform });
   res.sendSuccess({ updateCount });
 });
 
-// 更新比赛结果
+/**
+ * 更新比赛结果
+ */
 router.post('/update_games_result', (req, res) => {
   const { date, list } = req.body ?? {};
   Games.updateGamesResult({ date, list })
@@ -58,7 +69,9 @@ router.post('/update_games_result', (req, res) => {
   });
 });
 
-// 获取筛选过的联赛
+/**
+ * 获取筛选过的联赛
+ */
 router.get('/get_filtered_leagues', (req, res) => {
   const { mk } = req.query;
   Games.getFilteredLeagues(mk)
@@ -70,20 +83,26 @@ router.get('/get_filtered_leagues', (req, res) => {
   });
 });
 
-// 更新OB原始数据
+/**
+ * 更新OB原始数据
+ */
 router.post('/update_original_data', (req, res) => {
   const { leagues, matches, platform } = req.body ?? {};
   Games.updateOriginalData({ leagues, matches });
   res.sendSuccess();
 });
 
-// 获取OB原始数据
+/**
+ * 获取OB原始数据
+ */
 router.get('/get_original_data', (req, res) => {
   const obOriginalData = Games.getOriginalData();
   res.sendSuccess(obOriginalData);
 });
 
-// 获取关联列表
+/**
+ * 获取关联列表
+ */
 router.get('/get_games_relation', (req, res) => {
   const { mk, ids, le, lp } = req.query;
   const gamesRelation = Games.getGamesRelation({
@@ -95,7 +114,10 @@ router.get('/get_games_relation', (req, res) => {
   res.sendSuccess(gamesRelation);
 });
 
-// 获取中单方案
+/**
+ * 获取中单方案
+ * 返回中单方案列表
+ */
 router.get('/get_solutions', (req, res) => {
   const { win_min, with_events, show_lower, mk } = req.query;
   Games.getSolutions({
@@ -112,14 +134,18 @@ router.get('/get_solutions', (req, res) => {
   });
 });
 
-// 获取中单方案并按照比赛分组
+/**
+ * 获取中单方案
+ * 返回按照比赛分组的中单方案列表
+ */
 router.get('/get_games_solutions', (req, res) => {
-  const { win_min, with_events, show_lower, mk, tp, sk } = req.query;
+  const { win_min, with_events, show_lower, mk, gp, tp, sk } = req.query;
   Games.getGamesSolutions({
     win_min: win_min ? +win_min : undefined,
     with_events: with_events === 'true',
     show_lower: show_lower === 'true',
     mk: mk ? +mk : -1,
+    gp: gp ? +gp : -1,
     tp: tp ? +tp : 0,
     sk: sk.trim() ? sk.trim() : undefined,
   })
@@ -131,7 +157,9 @@ router.get('/get_games_solutions', (req, res) => {
   });
 });
 
-// 获取单个中单方案
+/**
+ * 通过sid获取单个中单方案
+ */
 router.get('/get_solution', (req, res) => {
   const { sid } = req.query;
   Games.getSolution(sid)
@@ -143,7 +171,9 @@ router.get('/get_solution', (req, res) => {
   });
 });
 
-// 通过比赛 ID 获取中单方案
+/**
+ * 通过比赛ID获取中单方案
+ */
 router.post('/solutions_by_ids', (req, res) => {
   const { ids } = req.body ?? {};
   Games.getSolutionsByIds(ids)
@@ -155,7 +185,9 @@ router.post('/solutions_by_ids', (req, res) => {
   });
 });
 
-// 获取综合利润方案
+/**
+ * 计算综合利润方案
+ */
 router.post('/calc_total_profit', (req, res) => {
   const data = req.body;
   let sid1, sid2, inner_base, inner_rebate;
@@ -178,7 +210,9 @@ router.post('/calc_total_profit', (req, res) => {
   });
 });
 
-// 获取自定义综合利润
+/**
+ * 计算自定义综合利润方案
+ */
 router.post('/calc_custom_total_profit', (req, res) => {
   // const [bet_info_1, bet_info_2, fixed, inner_base, inner_rebate] = req.body;
   const data = req.body;
@@ -202,7 +236,9 @@ router.post('/calc_custom_total_profit', (req, res) => {
   });
 });
 
-// 补单计算
+/**
+ * 计算补单方案
+ */
 router.post('/calc_total_replacement', (req, res) => {
   Games.getTotalReplacement(req.body)
   .then(totalProfit => {
@@ -213,7 +249,9 @@ router.post('/calc_total_replacement', (req, res) => {
   });
 });
 
-// 异常通知
+/**
+ * 异常通知
+ */
 router.post('/notify_exception', (req, res) => {
   const { message } = req.body;
   Games.notifyException(message);

+ 1 - 0
server/triangle/settings.js

@@ -21,6 +21,7 @@ const SETTING = {
   subsidyAmount: 0,
   subsidyRbWmAmount: 0,
   subsidyRbOtAmount: 0,
+  halfTimeActiveTime: 0,
   expireTimeEvents: 45000,
   expireTimeSpecial: 60000,
   syncSettingEnabled: false,

+ 21 - 4
web/apps/web-antd/src/views/match/components/solution_item.vue

@@ -36,7 +36,12 @@ const emit = defineEmits(['toggle']);
 const selectedIndex = ref(0);
 
 const parseIorKey = (iorKey) => {
-  const [, type, accept, side, , ratioString] = iorKey.match(/^ior_(r|ou|m|wm|ot|os)(a?)(h|c|n)?(_([\d-]+))?$/);
+  const iorKeyMatch = iorKey.match(/^ior_(r|ou|m|wm|ot|os)(a?)(h|c|n)?(_([\d-]+))?$/);
+  if (!iorKeyMatch) {
+    console.log('no iorKeyMatch', iorKey);
+    return null;
+  }
+  const [, type, accept, side, , ratioString] = iorKeyMatch;
   let ratio = 0;
   if (type === 'ot' || type === 'os') {
     ratio = ratioString;
@@ -91,7 +96,10 @@ const formatPsEvents = (events) => {
 const formatEvents = (events) => {
   const eventsMap = {};
   Object.keys(events).forEach(key => {
-    const { type, side, ratio } = parseIorKey(key);
+    const { type, side, ratio } = parseIorKey(key) ?? {};
+    if (!type) {
+      return;
+    }
     let ratioKey, index;
     if (type === 'r') {
       if (side === 'h') {
@@ -197,9 +205,9 @@ const currentSolution = computed(() => {
 const currentRelation = computed(() => {
   const cpr = currentSolution.value.cpr;
   const rel = props.rel;
-  const { ps: { leagueName, timestamp, stage, retime, score } } = rel;
+  const { ps: { eventId, leagueName, timestamp, stage, retime, score } } = rel;
   const dateTime = dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss');
-  const relation = { leagueName, timestamp, dateTime, stage, retime, score };
+  const relation = { id: eventId, leagueName, timestamp, dateTime, stage, retime, score };
   Object.keys(rel).forEach(platform => {
     const { eventId, teamHomeName, teamAwayName, events, special } = rel[platform] ?? {};
     if (!relation.rel) {
@@ -219,6 +227,10 @@ const currentRelation = computed(() => {
   return relation;
 });
 
+const isHalf = computed(() => {
+  return currentRelation.value.id < 0;
+});
+
 const ps = computed(() => {
   return currentRelation.value.rel.ps;
 });
@@ -244,6 +256,7 @@ const im = computed(() => {
     <div class="stage" v-if="currentRelation.stage">[{{ currentRelation.stage }}{{ currentRelation.retime ? ` ${currentRelation.retime}` : '' }}]</div>
     <div class="score" v-if="currentRelation.stage">[{{ currentRelation.score }}]</div>
     <div class="league-name">{{ currentRelation.leagueName }}</div>
+    <div class="period-half" v-if="isHalf">[上半场]</div>
     <div class="date-time">{{ currentRelation.dateTime }}</div>
     <div class="switch-btns" v-if="solutions.length">
       <Tooltip v-for="({sol}, index) in solutions" :key="index"
@@ -323,6 +336,10 @@ const im = computed(() => {
     font-size: 16px;
     font-weight: 400;
   }
+  .period-half {
+    margin-right: 10px;
+    color: hsl(var(--primary));
+  }
   .date-time {
     text-align: right;
     font-size: 14px;

+ 42 - 40
web/apps/web-antd/src/views/match/solutions/index.vue

@@ -1,6 +1,6 @@
 <script setup>
 import { requestClient } from '#/api/request';
-import { Button, message, Form, InputNumber, RadioGroup, Radio, Checkbox, Drawer, Input, Switch } from 'ant-design-vue';
+import { Button, message, Form, InputNumber, Select, SelectOption, RadioGroup, Radio, Checkbox, Drawer, Input, Switch } from 'ant-design-vue';
 import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
 import dayjs from 'dayjs';
 
@@ -19,6 +19,7 @@ const loopTimer = ref(null);
 const updateTimer = ref(null);
 const minProfitRate = ref(2);
 const marketType = ref(-1);
+const gamePeriod = ref(-1);
 const dataType = ref(0);
 const showLower = ref(false);
 const searchValue = ref('');
@@ -52,12 +53,13 @@ const solutionsList = computed(() => {
 const getSolutions = async () => {
   try {
     const mk = marketType.value;
+    const gp = gamePeriod.value;
     const tp = dataType.value;
     const sk = searchValue.value.trim();
     const win_min = !!sk ? -99999 : minProfitRate.value * 100;
     const with_events = true;
     const show_lower = showLower.value;
-    const data = await requestClient.get('/pstery/get_games_solutions', { params: { win_min, mk, tp, sk, with_events, show_lower } });
+    const data = await requestClient.get('/pstery/get_games_solutions', { params: { win_min, mk, gp, tp, sk, with_events, show_lower } });
     return data;
   }
   catch (error) {
@@ -145,61 +147,51 @@ const getLocalStorage = (key) => {
   return value ? JSON.parse(value) : null;
 }
 
-watch(searchValue, (newVal, oldVal) => {
-  if (newVal.trim() == oldVal.trim()) {
-    return;
+const updateDataList = ({ showLoading=false, loadingMessage='数据更新中...', localKey, localValue }={}) => {
+  if (showLoading && !updateLoaderHide.value) {
+    updateLoaderHide.value = message.loading(loadingMessage, 0);
   }
   clearTimeout(updateTimer.value);
   updateTimer.value = setTimeout(() => {
     updateSolutions();
+    if (localKey) {
+      setLocalStorage(localKey, localValue);
+    }
   }, 1000);
+}
+
+watch(searchValue, (newVal, oldVal) => {
+  if (newVal.trim() == oldVal.trim()) {
+    return;
+  }
+  updateDataList();
 });
 
 watch(minProfitRate, (newVal) => {
-  clearTimeout(updateTimer.value);
-  updateTimer.value = setTimeout(() => {
-    setLocalStorage('minProfitRate', newVal);
-    updateSolutions();
-  }, 1000);
+  updateDataList({ localKey: 'minProfitRate', localValue: newVal });
 });
 
 watch(marketType, (newVal) => {
-  if (!updateLoaderHide.value) {
-    updateLoaderHide.value = message.loading('数据更新中...', 0);
-  }
-  clearTimeout(updateTimer.value);
-  updateTimer.value = setTimeout(() => {
-    setLocalStorage('marketType', newVal);
-    updateSolutions();
-  }, 1000);
+  updateDataList({ showLoading: true, localKey: 'marketType', localValue: newVal });
+});
+
+watch(gamePeriod, (newVal) => {
+  updateDataList({ showLoading: true, localKey: 'gamePeriod', localValue: newVal });
 });
 
 watch(dataType, (newVal) => {
-  if (!updateLoaderHide.value) {
-    updateLoaderHide.value = message.loading('数据更新中...', 0);
-  }
-  clearTimeout(updateTimer.value);
-  updateTimer.value = setTimeout(() => {
-    setLocalStorage('dataType', newVal);
-    updateSolutions();
-  }, 1000);
+  updateDataList({ showLoading: true, localKey: 'dataType', localValue: newVal });
 });
 
 watch(showLower, (newVal) => {
-  if (!updateLoaderHide.value) {
-    updateLoaderHide.value = message.loading('数据更新中...', 0);
-  }
-  clearTimeout(updateTimer.value);
-  updateTimer.value = setTimeout(() => {
-    setLocalStorage('showLower', newVal);
-    updateSolutions();
-  }, 1000);
+  updateDataList({ showLoading: true, localKey: 'showLower', localValue: newVal });
 });
 
 onMounted(() => {
   loopActive.value = true;
   const min_win_rate = getLocalStorage('minProfitRate');
   const mk = getLocalStorage('marketType');
+  const gp = getLocalStorage('gamePeriod');
   const tp = getLocalStorage('dataType');
   const show_lower = getLocalStorage('showLower');
   if (min_win_rate !== null) {
@@ -208,6 +200,9 @@ onMounted(() => {
   if (mk !== null) {
     marketType.value = mk;
   }
+  if (gp !== null) {
+    gamePeriod.value = gp;
+  }
   if (tp !== null) {
     dataType.value = tp;
   }
@@ -240,12 +235,19 @@ onUnmounted(() => {
               <Radio :value="0">早盘({{ markCount?.early ?? 0 }})</Radio>
             </RadioGroup>
           </Form.Item>
-          <Form.Item label="盘口类型" class="sol-opt-item">
-            <RadioGroup v-model:value="dataType">
-              <Radio :value="0">全部</Radio>
-              <Radio :value="1">让球</Radio>
-              <Radio :value="2">大小</Radio>
-            </RadioGroup>
+          <Form.Item label="比赛周期" class="sol-opt-item input-item">
+            <Select v-model:value="gamePeriod">
+              <SelectOption :value="-1">全部</SelectOption>
+              <SelectOption :value="0">全场</SelectOption>
+              <SelectOption :value="1" style="font-size: 12px;">上半场</SelectOption>
+            </Select>
+          </Form.Item>
+          <Form.Item label="盘口类型" class="sol-opt-item input-item">
+            <Select v-model:value="dataType">
+              <SelectOption :value="0">全部</SelectOption>
+              <SelectOption :value="1">让球</SelectOption>
+              <SelectOption :value="2">大小</SelectOption>
+            </Select>
           </Form.Item>
           <Form.Item label="最小利润率(%)" class="sol-opt-item input-item" :class="{ 'disabled': !!searchValue.trim() }">
             <InputNumber class="number-input" size="small" max="100" min="-100" step="0.1" placeholder="最小利润率(%)" v-model:value="minProfitRate"/>

+ 8 - 0
web/apps/web-antd/src/views/system/parameter/index.vue

@@ -25,6 +25,7 @@ const initialFormState = {
   subsidyAmount: 0,
   subsidyRbWmAmount: 0,
   subsidyRbOtAmount: 0,
+  halfTimeActiveTime: 0,
   expireTimeEvents: 0,
   expireTimeSpecial: 0,
   syncSettingEnabled: false,
@@ -264,6 +265,13 @@ onUnmounted(() => {
         <InputNumber :disabled="formState.syncSettingEnabled" v-model:value="formState.subsidyRbOtAmount" :min="0" :step="0.01" style="width: 200px" />
       </Form.Item>
 
+      <Form.Item
+        label="上半场激活时间(-h)"
+        name="halfTimeActiveTime"
+      >
+        <InputNumber v-model:value="formState.halfTimeActiveTime" :min="0" :max="24" :step="0.5" style="width: 200px" />
+      </Form.Item>
+
       <Form.Item
         label="普通盘过期(ms)"
         name="expireTimeEvents"