feat: implement user balance management with hooks and store
This commit is contained in:
162
stores/userBalanceStore.ts
Normal file
162
stores/userBalanceStore.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
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
|
||||
}
|
||||
},
|
||||
}))
|
||||
Reference in New Issue
Block a user