feat: implement user balance management with hooks and store

This commit is contained in:
imeepos
2026-01-26 12:57:21 +08:00
parent 2757b68756
commit 3fd445bb6e
7 changed files with 489 additions and 215 deletions

162
stores/userBalanceStore.ts Normal file
View 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
}
},
}))