dcd0c56827
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 52s
CI/CD Pipeline / Build Staging API Image (push) Successful in 54s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 36s
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m53s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m0s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m55s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m7s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m29s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m34s
CI/CD Pipeline / Validate - Style (push) Successful in 4m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 46s
AI Code Review / AI Code Review (pull_request) Successful in 6m53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m24s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Validate - Security (push) Successful in 8m57s
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
/**
|
||
* 积分 & 会员状态管理
|
||
* 对齐后端 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 { ENABLE_CREDIT_SYSTEM } from "@/config/features"
|
||
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 () => {
|
||
// 已加载过不重复拉取
|
||
// 积分系统关闭时:只要 subscription/membership 已有值就跳过;开启时需 balance+rules+subscription 齐了才跳过
|
||
if (ENABLE_CREDIT_SYSTEM) {
|
||
if (get().balance && get().rules && get().subscription) return
|
||
} else {
|
||
if (get().subscription && get().membership) return
|
||
}
|
||
set({ loading: true, error: null })
|
||
try {
|
||
// 积分系统关闭时不拉取余额/规则/每日额度,但仍拉会员/订阅用于 VIP 标识展示
|
||
const balancePromise = ENABLE_CREDIT_SYSTEM
|
||
? getPointsBalance().catch(() => null)
|
||
: Promise.resolve(null)
|
||
const rulesPromise = ENABLE_CREDIT_SYSTEM
|
||
? getPointsRules().catch(() => null)
|
||
: Promise.resolve(null)
|
||
const dailyUsagePromise = ENABLE_CREDIT_SYSTEM
|
||
? getDailyUsage().catch(() => null)
|
||
: Promise.resolve(null)
|
||
const [balance, rules, subscription, dailyUsage, membership] = await Promise.all([
|
||
balancePromise,
|
||
rulesPromise,
|
||
getCurrentSubscription().catch(() => null),
|
||
dailyUsagePromise,
|
||
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,
|
||
})
|
||
},
|
||
}))
|