Преглед изворни кода

fix:修复sim卡信息加载卡死问题

Jerry Liang пре 1 месец
родитељ
комит
7be071596c

+ 5 - 1
service/controllers/simCardManageControllers.go

@@ -14,7 +14,11 @@ import (
 	"go.uber.org/zap"
 )
 
-var yearMonthRegexp = regexp.MustCompile(`^\d{4}-(0[1-9]|1[0-2])$`)
+// 兼容两个平台的月份格式:
+//   - SmartHub:    yyyy-MM(如 2026-09)
+//   - Acceleronix: yyyyMM (如 202609)
+// 连字符可选,月份固定两位且限定 01~12。
+var yearMonthRegexp = regexp.MustCompile(`^\d{4}-?(0[1-9]|1[0-2])$`)
 
 // 查询SIM卡信息
 // /simCardManage/info

+ 5 - 1
src/views/sim-card-manage/index.vue

@@ -121,7 +121,7 @@ import { useSimCardMonthlyCache, cacheKey } from './useSimCardMonthlyCache'
 const dialog = useDialog()
 const message = useMessage()
 
-const { memoryCache, loadingSet, computeDailyIncrement, onPageChange, getWithPriority } =
+const { memoryCache, loadingSet, failedSet, computeDailyIncrement, onPageChange, getWithPriority } =
   useSimCardMonthlyCache()
 
 const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
@@ -309,6 +309,10 @@ function createColumns(): DataTableColumns<SimCardInfo> {
       render(rowData) {
         const key = cacheKey(rowData.giccid, rowData.month)
 
+        // 请求失败:停止转圈,显示 "-"
+        if (failedSet.has(key)) return '-'
+
+        // 正在加载 或 尚未开始加载:显示加载态
         if (loadingSet.has(key) || !memoryCache.has(key)) {
           return h(NSpin, { size: 'small' })
         }

+ 104 - 53
src/views/sim-card-manage/useSimCardMonthlyCache.ts

@@ -9,6 +9,12 @@ const memoryCache = reactive(new Map<string, SimCardMonthlyData>())
 /** 正在请求中的 key 集合(响应式,驱动表格 loading 状态) */
 const loadingSet = reactive(new Set<string>())
 
+/**
+ * 请求失败的 key 集合(响应式)。
+ * 用于让失败项停止转圈、展示为 "-",而不是因未写入 memoryCache 而永久 loading。
+ */
+const failedSet = reactive(new Set<string>())
+
 // ─── 队列控制 ───
 let fetchQueue: Array<{ giccid: string; month: string }> = []
 let currentVersion = 0
@@ -29,71 +35,113 @@ function getPrevMonth(month: string): string {
 
 // ─── 核心:请求单条数据并写入双层缓存 ───
 
-async function fetchAndCache(giccid: string, month: string): Promise<SimCardMonthlyData> {
+function fetchAndCache(giccid: string, month: string): Promise<SimCardMonthlyData> {
   const key = cacheKey(giccid, month)
 
   // 1. 内存缓存命中
   const memHit = memoryCache.get(key)
-  if (memHit) return memHit
+  if (memHit) return Promise.resolve(memHit)
 
-  // 2. IndexedDB 缓存命中(带惰性过期)
-  const dbHit = await simCardCacheDB.get(giccid, month)
-  if (dbHit) {
-    memoryCache.set(key, dbHit)
-    return dbHit
-  }
+  // 2. IndexedDB 缓存命中(带惰性过期)→ 命中则写入内存后返回;未命中再走网络请求
+  return simCardCacheDB.get(giccid, month).then((dbHit) => {
+    if (dbHit) {
+      memoryCache.set(key, dbHit)
+      return dbHit
+    }
 
-  // 3. 网络请求
-  loadingSet.add(key)
-  try {
-    const data = await getSimCardMonthlyData(giccid, month)
-    memoryCache.set(key, data)
-    await simCardCacheDB.set(giccid, month, data)
-    return data
-  } finally {
-    loadingSet.delete(key)
-  }
+    // 3. 网络请求
+    loadingSet.add(key)
+    // 发起请求前清除历史失败标记(可能是重试)
+    failedSet.delete(key)
+    return getSimCardMonthlyData(giccid, month)
+      .then((data) => {
+        memoryCache.set(key, data)
+        // 写入 IndexedDB 后再返回,保持与原逻辑一致
+        return simCardCacheDB.set(giccid, month, data).then(() => data)
+      })
+      .catch((err: unknown) => {
+        // 标记失败:让列渲染停止转圈、显示 "-"
+        failedSet.add(key)
+        // 继续向上抛出,交由调用方(队列 catch / 点击处理)决定后续
+        throw err
+      })
+      .finally(() => {
+        loadingSet.delete(key)
+      })
+  })
 }
 
 // ─── 队列处理循环 ───
 
-async function processQueue(version: number) {
-  if (isProcessing) return
-  isProcessing = true
-
-  while (fetchQueue.length > 0) {
-    // 版本号不匹配 → 当前请求完成后停止
-    if (currentVersion !== version) break
+/**
+ * 处理队列中的单个 item:请求数据,并在必要时追加上月数据到队列。
+ * 单条失败仅记录日志,返回一个必定 resolve 的 Promise,绝不 reject,
+ * 从而保证队列不会因单条请求出错而中断。
+ */
+function processQueueItem(item: { giccid: string; month: string }): Promise<void> {
+  const key = cacheKey(item.giccid, item.month)
 
-    const item = fetchQueue.shift()!
-    const key = cacheKey(item.giccid, item.month)
+  // 已在内存中则跳过
+  if (memoryCache.has(key)) return Promise.resolve()
 
-    // 已在内存中则跳过
-    if (memoryCache.has(key)) continue
+  // 记录发起请求时的版本,用于异步返回后判断队列是否已被重建
+  const version = currentVersion
 
-    await fetchAndCache(item.giccid, item.month)
+  return fetchAndCache(item.giccid, item.month)
+    .then(() => {
+      // 版本已变(页面/搜索条件切换)→ 当前项结果保留在缓存中,
+      // 但不再基于旧上下文追加「上月数据」。
+      if (currentVersion !== version) return
 
-    // 检查版本号是否仍有效(fetchAndCache 是异步的)
-    if (currentVersion !== version) break
+      // 如果获取到的数据最新日是1号,追加上月数据到队列
+      const data = memoryCache.get(key)
+      if (!data) return
 
-    // 如果获取到的数据最新日是1号,追加上月数据到队列
-    const data = memoryCache.get(key)
-    if (data) {
       const days = Object.keys(data.dailyUsage).map(Number)
-      if (days.length > 0) {
-        const latestDay = Math.max(...days)
-        if (latestDay === 1) {
-          const prevMonth = getPrevMonth(item.month)
-          const prevKey = cacheKey(item.giccid, prevMonth)
-          if (!memoryCache.has(prevKey)) {
-            fetchQueue.push({ giccid: item.giccid, month: prevMonth })
-          }
-        }
+      if (days.length === 0) return
+
+      const latestDay = Math.max(...days)
+      if (latestDay !== 1) return
+
+      const prevMonth = getPrevMonth(item.month)
+      const prevKey = cacheKey(item.giccid, prevMonth)
+      if (!memoryCache.has(prevKey)) {
+        fetchQueue.push({ giccid: item.giccid, month: prevMonth })
       }
-    }
+    })
+    .catch((err: unknown) => {
+      // 单条请求失败不能中断整个队列,否则后续项会永久卡在 loading。
+      console.error('[SimCardCache] 预加载失败:', key, err)
+    })
+}
+
+/**
+ * 处理预加载队列(递归式 Promise 链,不使用 async/await 与 try)。
+ *
+ * 设计要点(修复「切换 pageSize / 翻页后加载卡住」的竞态死锁):
+ * - 全局同一时刻只允许一个链在跑(isProcessing 保证)。
+ * - 链不绑定某个固定版本,而是始终跟随全局的 fetchQueue。
+ *   当 onPageChange 递增 currentVersion 并重建 fetchQueue 后:
+ *     · 若已有链在跑 → 它处理完当前项后会读取到新的 fetchQueue,无缝接管;
+ *     · 若没有链在跑 → onPageChange 调用 processQueue() 启动新链。
+ *   这样绝不会出现「旧链退出、新链被 isProcessing 挡住」的死锁。
+ * - processQueueItem 内部已吞掉单条错误,链本身不会 reject;
+ *   仍用 .finally 兜底复位 isProcessing。
+ */
+function processQueue() {
+  if (isProcessing) return
+  isProcessing = true
+
+  // 递归步进:取出一项处理,完成后再处理下一项,队列空则收尾
+  const step = (): Promise<void> => {
+    const item = fetchQueue.shift()
+    if (!item) return Promise.resolve()
+    return processQueueItem(item).then(step)
   }
 
-  isProcessing = false
+  step().finally(() => {
+    isProcessing = false
+  })
 }
 
 // ─── 计算日增量 ───
@@ -112,7 +160,7 @@ function computeDailyIncrement(giccid: string, month: string): number | null {
   const days = Object.keys(data.dailyUsage).map(Number).sort((a, b) => a - b)
   if (days.length === 0) return null
 
-  const latestDay = days[days.length - 1]
+  const latestDay = days.at(-1)!
   const latestUsage = data.dailyUsage[latestDay].dataUsage
 
   if (latestDay === 1) {
@@ -125,7 +173,7 @@ function computeDailyIncrement(giccid: string, month: string): number | null {
     const prevDays = Object.keys(prevData.dailyUsage).map(Number).sort((a, b) => a - b)
     if (prevDays.length === 0) return null
 
-    const lastDayOfPrevMonth = prevDays[prevDays.length - 1]
+    const lastDayOfPrevMonth = prevDays.at(-1)!
     return latestUsage - prevData.dailyUsage[lastDayOfPrevMonth].dataUsage
   } else {
     // 非1日:取前一天
@@ -142,33 +190,36 @@ function computeDailyIncrement(giccid: string, month: string): number | null {
  * 已缓存的数据会被跳过,只补充未缓存的
  */
 function onPageChange(simCards: SimCardInfo[]) {
+  // 递增版本号:正在跑的循环会在下一轮迭代感知到并切换到新队列
   currentVersion++
-  const version = currentVersion
 
   // 重建队列
   fetchQueue = []
   for (const sim of simCards) {
     const key = cacheKey(sim.giccid, sim.month)
-    if (!memoryCache.has(key) && !loadingSet.has(key)) {
+    // 已缓存 / 正在请求 / 已知失败的都跳过,避免重复请求与无谓的重复失败
+    if (!memoryCache.has(key) && !loadingSet.has(key) && !failedSet.has(key)) {
       fetchQueue.push({ giccid: sim.giccid, month: sim.month })
     }
   }
 
-  processQueue(version)
+  // 若没有循环在跑则启动;若已有循环在跑,它会自动接管新队列
+  processQueue()
 }
 
 /**
  * 点击查看月数据时调用(优先级最高)
  * 直接发请求,不等待队列
  */
-async function getWithPriority(giccid: string, month: string): Promise<SimCardMonthlyData> {
-  return await fetchAndCache(giccid, month)
+function getWithPriority(giccid: string, month: string): Promise<SimCardMonthlyData> {
+  return fetchAndCache(giccid, month)
 }
 
 export function useSimCardMonthlyCache() {
   return {
     memoryCache,
     loadingSet,
+    failedSet,
     cacheKey,
     computeDailyIncrement,
     onPageChange,