Files
xiaoxia-saas/apps/web/src/store/pointsStore.ts
T
xiaoxia 6155b4d21e
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m56s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m37s
CI/CD Pipeline / Validate - Style (push) Successful in 3m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m33s
CI/CD Pipeline / Integration Tests (push) Successful in 5m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m13s
CI/CD Pipeline / Validate - Security (push) Successful in 9m25s
CI/CD Pipeline / Unit Tests (push) Successful in 10m29s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Failing after 4h19m11s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 4h26m39s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 4h28m51s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 4h25m45s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 4h25m46s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 4h28m48s
CI/CD Pipeline / PR Build Web Image (push) Failing after 4h28m48s
CI/CD Pipeline / PR Build API Image (push) Failing after 4h28m48s
CI/CD Pipeline / Deploy Production (push) Failing after 4h18m14s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 4h18m19s
CI/CD Pipeline / Build Production Web Image (push) Failing after 4h18m19s
CI/CD Pipeline / Canary Release to Production (push) Failing after 4h18m14s
CI/CD Pipeline / Frontend Lint (push) Failing after 4h28m48s
CI/CD Pipeline / Build Production API Image (push) Failing after 4h54m12s
feat(points): 对齐后端积分/订阅 API 最终契约(types + API 层改造) (#1956)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-16 10:53:58 +08:00

131 lines
3.8 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.
/**
* 积分 & 会员状态管理
* 对齐后端 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 { 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 {
balance: PointsBalance | 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>
/** 乐观扣减 */
optimisticDeduct: (points: number) => void
/** 乐观增加 */
optimisticAdd: (points: number) => void
/** 清除积分状态(退出登录) */
reset: () => void
}
export const usePointsStore = create<PointsState>((set, get) => ({
balance: null,
dailyUsage: null,
membership: null,
rules: null,
subscription: null,
loading: false,
error: null,
init: async () => {
// 已加载过不重复拉取
if (get().balance && get().rules && get().subscription) return
set({ loading: true, error: null })
try {
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,
rules,
subscription,
dailyUsage,
membership,
loading: false,
})
} catch (err) {
set({ error: (err as Error).message || "加载积分信息失败", loading: false })
}
},
refreshBalance: async () => {
try {
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 { balance, membership } = get()
if (!balance) return
set({
balance: {
...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 { balance, membership } = get()
if (!balance) return
set({
balance: {
...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,
})
},
}))