expo-popcore-app/stores/userBalanceStore.ts

163 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AppState, type AppStateStatus } from 'react-native'
import { create } from 'zustand'
import { subscription } from '@/lib/auth'
import type { ApiError } from '@/lib/types'
interface UserBalanceState {
balance: number
loading: boolean
error: ApiError | null
lastLoadTime: number
pollingEnabled: boolean
}
interface UserBalanceActions {
load: (force?: boolean) => Promise<void>
setBalance: (balance: number) => void
deductBalance: (amount: number) => void
startPolling: () => void
stopPolling: () => void
restartPolling: () => void
reset: () => void
}
type UserBalanceStore = UserBalanceState & UserBalanceActions
const POLLING_INTERVAL = 60e3 // 60秒轮询一次
const DEBOUNCE_TIME = 5e3 // 5秒防抖
let pollingInterval: ReturnType<typeof setInterval> | null = null
let loadingPromise: Promise<void> | null = null
let appStateSubscription: { remove: () => void } | null = null
export const useUserBalanceStore = create<UserBalanceStore>((set, get) => ({
// State
balance: 0,
loading: false,
error: null,
lastLoadTime: 0,
pollingEnabled: false,
// Actions
load: async (force = false) => {
const state = get()
const now = Date.now()
const timeSinceLastLoad = now - state.lastLoadTime
// 如果不是强制刷新且距离上次调用少于5秒则跳过
if (!force && timeSinceLastLoad < DEBOUNCE_TIME) {
console.log('跳过余额加载,距离上次调用时间过短:', timeSinceLastLoad + 'ms')
return
}
// 如果已经在加载中且不是强制刷新,直接返回
if (state.loading && !force) {
console.log('余额加载中,跳过重复请求')
return loadingPromise || undefined
}
set({ lastLoadTime: now, loading: true })
loadingPromise = (async () => {
try {
const { data, error } = await subscription.list()
if (error) {
set({ error, loading: false })
return
}
const meteredSubscriptions = data?.filter((sub) => sub.type === 'metered') || []
const creditBalance = meteredSubscriptions[0]?.creditBalance?.remainingTokenBalance || 0
set({ balance: creditBalance, error: null, loading: false })
} catch (e) {
console.error('加载余额失败:', e)
set({ loading: false })
} finally {
loadingPromise = null
}
})()
return loadingPromise
},
setBalance: (balance: number) => {
set({ balance })
},
deductBalance: (amount: number) => {
set((state) => ({ balance: Math.max(0, state.balance - amount) }))
},
startPolling: () => {
const { stopPolling, load } = get()
// 先清理可能存在的旧轮询状态
stopPolling()
console.log('开始余额轮询,间隔:', POLLING_INTERVAL / 1000, '秒')
set({ pollingEnabled: true })
// 立即执行一次
load(false)
// 设置定时器
pollingInterval = setInterval(() => {
const state = get()
if (state.pollingEnabled) {
load(false)
}
}, POLLING_INTERVAL)
// 设置应用状态监听
if (!appStateSubscription) {
appStateSubscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
const state = get()
if (nextAppState === 'active' && state.pollingEnabled) {
console.log('应用回到前台,重启余额轮询')
get().restartPolling()
} else if (nextAppState === 'background' || nextAppState === 'inactive') {
console.log('应用进入后台,暂停余额轮询')
get().stopPolling()
}
})
}
},
stopPolling: () => {
console.log('停止余额轮询')
set({ pollingEnabled: false })
if (pollingInterval) {
clearInterval(pollingInterval)
pollingInterval = null
}
},
restartPolling: () => {
const { stopPolling, startPolling } = get()
stopPolling()
setTimeout(() => startPolling(), 1000) // 延迟1秒重启
},
reset: () => {
const { stopPolling } = get()
stopPolling()
set({
balance: 0,
loading: false,
error: null,
lastLoadTime: 0,
pollingEnabled: false,
})
// 清理应用状态监听器
if (appStateSubscription) {
appStateSubscription.remove()
appStateSubscription = null
}
},
}))