Переглянути джерело

feat: list stored odds history games

flyzto 13 годин тому
батько
коміт
d04b3dbd3c

+ 8 - 1
server/models/GamesPs.js

@@ -1368,6 +1368,13 @@ const getOddsHistory = async (eventId) => {
   return OddsHistory.getGameOddsHistory(eventId);
 }
 
+/**
+ * 获取赔率历史比赛列表
+ */
+const getOddsHistoryGames = async () => {
+  return OddsHistory.getOddsHistoryGames();
+}
+
 /**
  * 清理中单方案
  */
@@ -1761,7 +1768,7 @@ module.exports = {
   updateGamesResult,
   updateOriginalData, getOriginalData,
   getSolutions, getGamesSolutions, getSolution, getSolutionsByIds,
-  getOddsHistory,
+  getOddsHistory, getOddsHistoryGames,
   getTotalProfitWithSid, getTotalProfitWithBetInfo, getTotalReplacement,
   notifyException,
 }

+ 19 - 0
server/models/OddsHistory.js

@@ -151,6 +151,24 @@ const getGameOddsHistory = async (eventId) => {
   };
 }
 
+const getOddsHistoryGames = async () => {
+  const histories = await OddsHistory.find()
+    .select('eventId leagueName teamHomeName teamAwayName startTime endTime markets updatedAt')
+    .sort({ startTime: -1 })
+    .lean({ flattenMaps: true });
+
+  return histories.map(history => ({
+    eventId: history.eventId,
+    leagueName: history.leagueName,
+    teamHomeName: history.teamHomeName,
+    teamAwayName: history.teamAwayName,
+    startTime: history.startTime,
+    endTime: history.endTime,
+    marketCount: Object.values(history.markets ?? {}).filter(points => points?.length).length,
+    updatedAt: history.updatedAt,
+  }));
+}
+
 const cleanupExpiredHistory = async () => {
   const expireTime = Date.now() - ODDS_HISTORY_RETENTION;
   return OddsHistory.deleteMany({ startTime: { $lt: expireTime } });
@@ -177,6 +195,7 @@ module.exports = {
   TRACKED_IOR_KEYS,
   cleanupExpiredHistory,
   getGameOddsHistory,
+  getOddsHistoryGames,
   isInsideTrackWindow,
   recordGameOdds,
   startCleanup,

+ 13 - 0
server/routes/pstery.js

@@ -188,6 +188,19 @@ router.get('/get_odds_history', (req, res) => {
   });
 });
 
+/**
+ * 获取赔率历史比赛列表
+ */
+router.get('/get_odds_history_games', (req, res) => {
+  Games.getOddsHistoryGames()
+  .then(games => {
+    res.sendSuccess(games);
+  })
+  .catch(err => {
+    res.badRequest(err.message);
+  });
+});
+
 /**
  * 通过比赛ID获取中单方案
  */

+ 36 - 34
web/apps/web-antd/src/views/match/odds-curve/index.vue

@@ -36,19 +36,15 @@ type OddsHistory = {
   teamHomeName?: string;
 };
 
-type GameRelation = {
-  id: number;
-  mk: number;
-  rel?: {
-    ps?: {
-      eventId: number;
-      leagueName: string;
-      teamAwayName: string;
-      teamHomeName: string;
-      timestamp: number;
-    };
-  };
-  timestamp: number;
+type HistoryGame = {
+  endTime: number;
+  eventId: number;
+  leagueName?: string;
+  marketCount: number;
+  startTime: number;
+  teamAwayName?: string;
+  teamHomeName?: string;
+  updatedAt?: string;
 };
 
 const MARKET_GROUPS = [
@@ -98,7 +94,7 @@ const DEFAULT_MARKET_KEYS = [
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);
 
-const games = ref<GameRelation[]>([]);
+const games = ref<HistoryGame[]>([]);
 const history = ref<OddsHistory | null>(null);
 const selectedEventId = ref<number>();
 const selectedMarkets = ref<string[]>([]);
@@ -115,18 +111,21 @@ const availableMarketKeys = computed(() => {
 const hasChartData = computed(() => !!history.value && availableMarketKeys.value.length > 0);
 
 const selectedGame = computed(() => {
-  return games.value.find((item) => item.id === selectedEventId.value);
+  return games.value.find((item) => item.eventId === selectedEventId.value);
 });
 
 const filteredGames = computed(() => {
   const keyword = searchValue.value.trim();
+  const now = Date.now();
   return games.value.filter((item) => {
-    const ps = item.rel?.ps;
-    if (!ps) {
+    if (marketType.value === 1 && item.endTime < now) {
+      return false;
+    }
+    if (marketType.value === 2 && item.endTime >= now) {
       return false;
     }
     if (keyword) {
-      const text = `${item.id} ${ps.leagueName} ${ps.teamHomeName} ${ps.teamAwayName}`;
+      const text = `${item.eventId} ${item.leagueName} ${item.teamHomeName} ${item.teamAwayName}`;
       if (!text.includes(keyword)) {
         return false;
       }
@@ -136,7 +135,7 @@ const filteredGames = computed(() => {
 });
 
 const historyTitle = computed(() => {
-  const source = history.value ?? selectedGame.value?.rel?.ps;
+  const source = history.value ?? selectedGame.value;
   if (!source) {
     return '未选择比赛';
   }
@@ -157,8 +156,12 @@ const marketOptions = computed(() => {
   })).filter((group) => group.options.length);
 });
 
+const isGroupSelected = (keys: string[]) => {
+  return keys.length > 0 && keys.every((key) => selectedMarkets.value.includes(key));
+};
+
 const selectMarketGroup = (keys: string[]) => {
-  selectedMarkets.value = [...keys];
+  selectedMarkets.value = isGroupSelected(keys) ? [] : [...keys];
 };
 
 const formatTime = (time?: number) => {
@@ -289,12 +292,10 @@ const renderChart = () => {
 const fetchGames = async () => {
   loadingGames.value = true;
   try {
-    const data = await requestClient.get<GameRelation[]>('/pstery/get_games_relation', {
-      params: { hb: false, hh: true, mk: marketType.value },
-    });
+    const data = await requestClient.get<HistoryGame[]>('/pstery/get_odds_history_games');
     games.value = data ?? [];
     if (!selectedEventId.value && games.value.length) {
-      selectedEventId.value = games.value[0]?.id;
+      selectedEventId.value = games.value[0]?.eventId;
     }
     if (selectedEventId.value) {
       await fetchHistory(selectedEventId.value);
@@ -360,9 +361,8 @@ onMounted(() => {
       <div class="panel-toolbar">
         <RadioGroup v-model:value="marketType" size="small">
           <Radio :value="-1">全部</Radio>
-          <Radio :value="2">滚球</Radio>
-          <Radio :value="1">今日</Radio>
-          <Radio :value="0">早盘</Radio>
+          <Radio :value="1">进行中</Radio>
+          <Radio :value="2">已结束</Radio>
         </RadioGroup>
         <Button size="small" :loading="loadingGames" @click="fetchGames">刷新</Button>
       </div>
@@ -376,15 +376,15 @@ onMounted(() => {
         <div class="match-list" v-if="filteredGames.length">
           <button
             v-for="game in filteredGames"
-            :key="game.id"
+            :key="game.eventId"
             class="match-item"
-            :class="{ active: game.id === selectedEventId }"
+            :class="{ active: game.eventId === selectedEventId }"
             type="button"
-            @click="selectGame(game.id)"
+            @click="selectGame(game.eventId)"
           >
-            <span class="league">{{ game.rel?.ps?.leagueName }}</span>
-            <span class="teams">{{ game.rel?.ps?.teamHomeName }} vs {{ game.rel?.ps?.teamAwayName }}</span>
-            <span class="meta">ID {{ game.id }} · {{ formatGameTime(game.timestamp) }}</span>
+            <span class="league">{{ game.leagueName }}</span>
+            <span class="teams">{{ game.teamHomeName }} vs {{ game.teamAwayName }}</span>
+            <span class="meta">ID {{ game.eventId }} · {{ formatGameTime(game.startTime) }} · {{ game.marketCount }}盘</span>
           </button>
         </div>
         <Empty v-else class="list-empty" description="暂无比赛" />
@@ -397,7 +397,7 @@ onMounted(() => {
           <div class="curve-title">{{ historyTitle }}</div>
           <div class="curve-subtitle">
             <span>赛事ID:{{ selectedEventId ?? '-' }}</span>
-            <span>开赛:{{ formatTime(history?.startTime ?? selectedGame?.timestamp) }}</span>
+            <span>开赛:{{ formatTime(history?.startTime ?? selectedGame?.startTime) }}</span>
             <span>记录盘口:{{ availableMarketKeys.length }}</span>
           </div>
         </div>
@@ -407,6 +407,7 @@ onMounted(() => {
         <div v-for="group in marketOptions" :key="group.label" class="market-group">
           <button
             class="market-group-label"
+            :class="{ active: isGroupSelected(group.availableKeys) }"
             type="button"
             @click="selectMarketGroup(group.availableKeys)"
           >
@@ -551,6 +552,7 @@ onMounted(() => {
   text-align: left;
 }
 
+.market-group-label.active,
 .market-group-label:hover {
   color: hsl(var(--primary));
 }