| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- import { reactive } from 'vue'
- import { getSimCardMonthlyData } from '@/service/simCardManage'
- import { simCardCacheDB } from '@/service/simCardManage/simCardCache'
- import type { SimCardMonthlyData, SimCardInfo } from '@/service/simCardManage/interface'
- /** 响应式内存缓存 key: `${giccid}_${month}` */
- 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
- let isProcessing = false
- // ─── 工具函数 ───
- export function cacheKey(giccid: string, month: string): string {
- return `${giccid}_${month}`
- }
- /** 获取上月 month 字符串 (输入 "2026-08" → 输出 "2026-07") */
- function getPrevMonth(month: string): string {
- const [year, mon] = month.split('-').map(Number)
- const date = new Date(year, mon - 2, 1)
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
- }
- // ─── 核心:请求单条数据并写入双层缓存 ───
- function fetchAndCache(giccid: string, month: string): Promise<SimCardMonthlyData> {
- const key = cacheKey(giccid, month)
- // 1. 内存缓存命中
- const memHit = memoryCache.get(key)
- if (memHit) return Promise.resolve(memHit)
- // 2. IndexedDB 缓存命中(带惰性过期)→ 命中则写入内存后返回;未命中再走网络请求
- return simCardCacheDB.get(giccid, month).then((dbHit) => {
- if (dbHit) {
- memoryCache.set(key, dbHit)
- return dbHit
- }
- // 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)
- })
- })
- }
- // ─── 队列处理循环 ───
- /**
- * 处理队列中的单个 item:请求数据,并在必要时追加上月数据到队列。
- * 单条失败仅记录日志,返回一个必定 resolve 的 Promise,绝不 reject,
- * 从而保证队列不会因单条请求出错而中断。
- */
- function processQueueItem(item: { giccid: string; month: string }): Promise<void> {
- const key = cacheKey(item.giccid, item.month)
- // 已在内存中则跳过
- if (memoryCache.has(key)) return Promise.resolve()
- // 记录发起请求时的版本,用于异步返回后判断队列是否已被重建
- const version = currentVersion
- return fetchAndCache(item.giccid, item.month)
- .then(() => {
- // 版本已变(页面/搜索条件切换)→ 当前项结果保留在缓存中,
- // 但不再基于旧上下文追加「上月数据」。
- if (currentVersion !== version) return
- // 如果获取到的数据最新日是1号,追加上月数据到队列
- const data = memoryCache.get(key)
- if (!data) return
- const days = Object.keys(data.dailyUsage).map(Number)
- 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)
- }
- step().finally(() => {
- isProcessing = false
- })
- }
- // ─── 计算日增量 ───
- /**
- * 计算某 SIM 卡当月的日增量
- * - 取最新一天的用量 - 前一天的用量
- * - 1号时需要上月最后一天数据
- * - 前一日数据不存在返回 null(展示 "-")
- */
- function computeDailyIncrement(giccid: string, month: string): number | null {
- const key = cacheKey(giccid, month)
- const data = memoryCache.get(key)
- if (!data) return null
- const days = Object.keys(data.dailyUsage).map(Number).sort((a, b) => a - b)
- if (days.length === 0) return null
- const latestDay = days.at(-1)!
- const latestUsage = data.dailyUsage[latestDay].dataUsage
- if (latestDay === 1) {
- // 1日:读取上月最后一天
- const prevMonth = getPrevMonth(month)
- const prevKey = cacheKey(giccid, prevMonth)
- const prevData = memoryCache.get(prevKey)
- if (!prevData) return null
- const prevDays = Object.keys(prevData.dailyUsage).map(Number).sort((a, b) => a - b)
- if (prevDays.length === 0) return null
- const lastDayOfPrevMonth = prevDays.at(-1)!
- return latestUsage - prevData.dailyUsage[lastDayOfPrevMonth].dataUsage
- } else {
- // 非1日:取前一天
- const prevDayData = data.dailyUsage[latestDay - 1]
- if (!prevDayData) return null
- return latestUsage - prevDayData.dataUsage
- }
- }
- // ─── 对外暴露的方法 ───
- /**
- * 翻页 / 搜索后调用:重建预加载队列
- * 已缓存的数据会被跳过,只补充未缓存的
- */
- function onPageChange(simCards: SimCardInfo[]) {
- // 递增版本号:正在跑的循环会在下一轮迭代感知到并切换到新队列
- currentVersion++
- // 重建队列
- fetchQueue = []
- for (const sim of simCards) {
- const key = cacheKey(sim.giccid, sim.month)
- // 已缓存 / 正在请求 / 已知失败的都跳过,避免重复请求与无谓的重复失败
- if (!memoryCache.has(key) && !loadingSet.has(key) && !failedSet.has(key)) {
- fetchQueue.push({ giccid: sim.giccid, month: sim.month })
- }
- }
- // 若没有循环在跑则启动;若已有循环在跑,它会自动接管新队列
- processQueue()
- }
- /**
- * 点击查看月数据时调用(优先级最高)
- * 直接发请求,不等待队列
- */
- function getWithPriority(giccid: string, month: string): Promise<SimCardMonthlyData> {
- return fetchAndCache(giccid, month)
- }
- export function useSimCardMonthlyCache() {
- return {
- memoryCache,
- loadingSet,
- failedSet,
- cacheKey,
- computeDailyIncrement,
- onPageChange,
- getWithPriority,
- }
- }
|