chore(points): align types/api with final backend contract - apps/web/src/store/pointsStore.ts
This commit is contained in:
@@ -1,83 +1,130 @@
|
||||
/**
|
||||
* 积分 & 会员状态管理(Zustand)
|
||||
* - 启动时拉取余额 & 订阅信息
|
||||
* - 提供刷新、余额扣减(乐观更新)等工具
|
||||
* 积分 & 会员状态管理
|
||||
* 对齐后端 staging 最终契约(2026-09-16):
|
||||
* - balance: GET /points/balance(无 free_clips_*)
|
||||
* - dailyUsage: GET /usage/daily(每日免费额度)
|
||||
* - membership: GET /points/subscription/membership(聚合会员信息)
|
||||
* - rules: GET /points/rules(base_points + free_user_multiplier)
|
||||
* - subscription: GET /subscription/current(plan_id + billing_cycle)
|
||||
*/
|
||||
import { create } from "zustand"
|
||||
import type { PointsBalance, PointsRulesResponse, SubscriptionCurrent } from "@/api/points/types"
|
||||
import { getCurrentSubscription, getPointsBalance, getPointsRules } from "@/api/points"
|
||||
import { getPointsBalance, getPointsRules, getDailyUsage, getMembership } from "@/api/points"
|
||||
import { getCurrentSubscription } from "@/api/subscription"
|
||||
import type {
|
||||
PointsBalance,
|
||||
PointsRulesResponse,
|
||||
DailyUsage,
|
||||
MembershipResponse,
|
||||
} from "@/api/points/types"
|
||||
import type { SubscriptionInfo } from "@/api/subscription/types"
|
||||
|
||||
interface PointsState {
|
||||
/** 积分余额 & 会员状态(来自 /points/balance) */
|
||||
balance: PointsBalance | null
|
||||
/** 订阅详情(来自 /subscription/current) */
|
||||
subscription: SubscriptionCurrent | null
|
||||
/** 积分消耗规则缓存 */
|
||||
dailyUsage: DailyUsage | null
|
||||
membership: MembershipResponse | null
|
||||
rules: PointsRulesResponse | null
|
||||
subscription: SubscriptionInfo | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
/** 初始化:拉取余额 + 订阅信息 + 规则 */
|
||||
/** 初始化:并行拉取 balance / rules / subscription / dailyUsage / membership */
|
||||
init: () => Promise<void>
|
||||
/** 强制刷新余额 */
|
||||
/** 刷新余额(充值/消费后调用) */
|
||||
refreshBalance: () => Promise<void>
|
||||
/** 乐观扣减:在支付/业务发起前调用,失败时用 refreshBalance 兜底 */
|
||||
/** 乐观扣减 */
|
||||
optimisticDeduct: (points: number) => void
|
||||
/** 乐观增加(充值成功后调用) */
|
||||
/** 乐观增加 */
|
||||
optimisticAdd: (points: number) => void
|
||||
/** 清除积分状态(退出登录) */
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const usePointsStore = create<PointsState>((set, get) => ({
|
||||
balance: null,
|
||||
subscription: null,
|
||||
dailyUsage: null,
|
||||
membership: null,
|
||||
rules: null,
|
||||
subscription: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
init: async () => {
|
||||
if (get().loading) return
|
||||
// 已加载过不重复拉取
|
||||
if (get().balance && get().rules && get().subscription) return
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const [balance, sub, rules] = await Promise.all([
|
||||
getPointsBalance(),
|
||||
getCurrentSubscription(),
|
||||
getPointsRules(),
|
||||
const [balance, rules, subscription, dailyUsage, membership] = await Promise.all([
|
||||
getPointsBalance().catch(() => null),
|
||||
getPointsRules().catch(() => null),
|
||||
getCurrentSubscription().catch(() => null),
|
||||
getDailyUsage().catch(() => null),
|
||||
getMembership().catch(() => null),
|
||||
])
|
||||
set({ balance, subscription: sub, rules, loading: false })
|
||||
} catch (e) {
|
||||
set({ error: (e as Error).message, loading: false })
|
||||
set({
|
||||
balance,
|
||||
rules,
|
||||
subscription,
|
||||
dailyUsage,
|
||||
membership,
|
||||
loading: false,
|
||||
})
|
||||
} catch (err) {
|
||||
set({ error: (err as Error).message || "加载积分信息失败", loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
refreshBalance: async () => {
|
||||
try {
|
||||
const balance = await getPointsBalance()
|
||||
set({ balance })
|
||||
} catch (e) {
|
||||
set({ error: (e as Error).message })
|
||||
const [balance, dailyUsage, membership, subscription] = await Promise.all([
|
||||
getPointsBalance(),
|
||||
getDailyUsage().catch(() => null),
|
||||
getMembership().catch(() => null),
|
||||
getCurrentSubscription().catch(() => get().subscription),
|
||||
])
|
||||
set({ balance, dailyUsage, membership, subscription })
|
||||
} catch (err) {
|
||||
set({ error: (err as Error).message || "刷新积分失败" })
|
||||
}
|
||||
},
|
||||
|
||||
optimisticDeduct: (points: number) => {
|
||||
const b = get().balance
|
||||
if (!b) return
|
||||
const { balance, membership } = get()
|
||||
if (!balance) return
|
||||
set({
|
||||
balance: {
|
||||
...b,
|
||||
balance: Math.max(0, b.balance - points),
|
||||
total_spent: b.total_spent + points,
|
||||
...balance,
|
||||
balance: Math.max(0, balance.balance - points),
|
||||
total_spent: balance.total_spent + points,
|
||||
},
|
||||
membership: membership
|
||||
? { ...membership, points_balance: Math.max(0, membership.points_balance - points) }
|
||||
: null,
|
||||
})
|
||||
},
|
||||
|
||||
optimisticAdd: (points: number) => {
|
||||
const b = get().balance
|
||||
if (!b) return
|
||||
const { balance, membership } = get()
|
||||
if (!balance) return
|
||||
set({
|
||||
balance: {
|
||||
...b,
|
||||
balance: b.balance + points,
|
||||
total_earned: b.total_earned + points,
|
||||
...balance,
|
||||
balance: balance.balance + points,
|
||||
total_earned: balance.total_earned + points,
|
||||
},
|
||||
membership: membership
|
||||
? { ...membership, points_balance: membership.points_balance + points }
|
||||
: null,
|
||||
})
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
balance: null,
|
||||
dailyUsage: null,
|
||||
membership: null,
|
||||
rules: null,
|
||||
subscription: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user