From 3b9d645042ab65fcf882f45e997d34ec0ec51ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 2 Oct 2026 19:00:23 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84(=E9=92=B1=E5=8C=85):=20?= =?UTF-8?q?=E5=85=B1=E4=BA=AB=E7=BB=84=E4=BB=B6=E4=B8=8E=E9=92=B1=E5=8C=85?= =?UTF-8?q?=20store=20=E6=94=B9=E7=94=A8=E4=B8=89=E6=B1=A0=E4=BD=99?= =?UTF-8?q?=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - createProfileWalletStore 的余额状态改为三池视图,去掉「响应只有 walletBalance 时兜底」的旧分支,缺少 mudPointBalance 一律算读取失败 - 泥点明细面板并列展示永久泥点 / 月度泥点 / 每日免费泥点,月度行带北京时间刷新时刻;「不限时泥点」标签改为「永久泥点」 - 充值弹层当前余额改用三池合计,不再读 walletBalance - 三池与首充赠送相关的 store、明细面板、充值弹层测试同步更新 Co-authored-by: Junie --- .../index.test.tsx | 17 ++- .../PlatformMudPointWalletEntry/index.tsx | 27 +++- .../index.test.tsx | 127 ++++++++---------- .../PlatformProfileRechargeModal/index.tsx | 6 +- .../stores/createProfileWalletStore.test.ts | 25 ++-- .../src/stores/createProfileWalletStore.ts | 26 ++-- 6 files changed, 118 insertions(+), 110 deletions(-) diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry/index.test.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry/index.test.tsx index 6925cf4a3..982ddaeb6 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry/index.test.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry/index.test.tsx @@ -8,10 +8,9 @@ import { formatMudPointCount } from '../../utils/format'; import { PlatformMudPointWalletEntry } from './index.tsx'; const breakdown = { - totalPoints: 207, permanentPoints: 100, - limitedPoints: 80, - limitedExpiresAt: '2026-07-06T16:00:00Z', + monthlyPoints: 80, + monthlyResetsAt: '2026-07-06T16:00:00Z', dailyFreePoints: 27, dailyFreeResetPoints: 20, dailyFreeResetsAt: '2026-07-12T16:00:00Z', @@ -22,7 +21,7 @@ test('formats mud point counts consistently', () => { expect(formatMudPointCount(12_345, true)).toBe('1.2万'); }); -test('shows only permanent and daily free points in the shared wallet panel', async () => { +test('shows permanent, monthly and daily free points in the shared wallet panel', async () => { const user = userEvent.setup(); const onRequestDetails = vi.fn(); const onRecharge = vi.fn(); @@ -43,11 +42,15 @@ test('shows only permanent and daily free points in the shared wallet panel', as const details = screen.getByRole('dialog', { name: '泥点账户详情' }); expect(details.className).toContain('rounded-[1.25rem]'); - expect(within(details).getByText('不限时泥点')).toBeTruthy(); - expect(within(details).getByText('按量充值、兑换码获得')).toBeTruthy(); + expect(within(details).getByText('永久泥点')).toBeTruthy(); + expect(within(details).getByText('月度泥点')).toBeTruthy(); + expect(within(details).getByText('不过期,创作照常扣点')).toBeTruthy(); expect(within(details).getByText('100')).toBeTruthy(); + expect(within(details).getByText('80')).toBeTruthy(); + expect( + within(details).getByText('2026-07-07 00:00 清零并发放新额度'), + ).toBeTruthy(); expect(within(details).queryByText('限时泥点')).toBeNull(); - expect(within(details).queryByText('2026-07-07 到期')).toBeNull(); expect(within(details).getByText('每日免费泥点')).toBeTruthy(); expect(within(details).getByText('27')).toBeTruthy(); expect(within(details).getByText('每天重置为 20 泥点')).toBeTruthy(); diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx index 69519586f..82b0d51e7 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx @@ -11,10 +11,15 @@ import { import type { ProfileMudPointBalance } from '../../contracts/runtime'; import MUD_POINT_ICON_SRC from '../../icons/topbar-wallet.png'; import { formatMudPointCount } from '../../utils/format'; +import { + formatMudPointResetAt, + type ProfileMudPointBalanceView, + resolveProfileMudPointBalanceView, +} from '../../utils/mudPoints'; export type PlatformMudPointWalletEntryProps = { balance: number | null; - breakdown?: ProfileMudPointBalance | null; + breakdown?: ProfileMudPointBalance | ProfileMudPointBalanceView | null; isLoading?: boolean; error?: string | null; variant?: 'desktop' | 'mobile' | 'editor'; @@ -55,7 +60,7 @@ function MudPointBalanceRow({ export function PlatformMudPointWalletEntry({ balance, - breakdown, + breakdown: rawBreakdown, isLoading = false, error = null, variant = 'desktop', @@ -71,6 +76,9 @@ export function PlatformMudPointWalletEntry({ const closeTimerRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const isCompact = variant === 'mobile'; + const breakdown = rawBreakdown + ? resolveProfileMudPointBalanceView(rawBreakdown) + : null; const displayedBalance = balance ?? breakdown?.totalPoints ?? null; const balanceLabel = displayedBalance === null @@ -79,6 +87,7 @@ export function PlatformMudPointWalletEntry({ const exactBalanceLabel = displayedBalance === null ? '--' : formatMudPointCount(displayedBalance); const detailsRequestKey = `balance:${balance ?? breakdown?.totalPoints ?? 'unknown'}`; + const monthlyResetLabel = formatMudPointResetAt(breakdown?.monthlyResetsAt); const cancelPendingClose = useCallback(() => { if (closeTimerRef.current !== null) { @@ -272,11 +281,19 @@ export function PlatformMudPointWalletEntry({ {breakdown ? ( <> + - {/*TODO 限时泥点 is not enabled by now */} buildPointProduct({ - productId: 'points_180', - title: '180泥点', - priceCents: 1800, - pointsAmount: 180, - bonusPoints: 90, - badgeLabel: '首充加赠', - description: '首充加赠90泥点', + productId: `points_${pointsAmount}`, + title: `${pointsAmount}泥点`, + priceCents: pointsAmount * 10, + pointsAmount, + description: `${pointsAmount}泥点`, }), - buildPointProduct({ - productId: 'points_300', - title: '300泥点', - priceCents: 3000, - pointsAmount: 300, - bonusPoints: 150, - badgeLabel: '首充加赠', - description: '首充加赠150泥点', - }), - buildPointProduct({ - productId: 'points_680', - title: '680泥点', - priceCents: 6800, - pointsAmount: 680, - bonusPoints: 340, - badgeLabel: '首充加赠', - description: '首充加赠340泥点', - }), - ]; + ); } -function buildMembershipProduct(): ProfileRechargeProduct { +function buildMembershipPlan() { return { - ...buildPointProduct(), - productId: 'member_month', - title: '会员月卡', - priceCents: 2800, - kind: 'membership', - pointsAmount: 0, - durationDays: 30, - description: '会员月卡', - tier: 'month', - membershipPeriodDays: 30, + plan: 'starter' as const, + title: 'Starter', + rank: 1, + monthPriceCents: 3_900, + yearPriceCents: 39_000, + periodPoints: 400, + modelAccess: 'basic' as const, + concurrentJobLimit: 1, + sortOrder: 10, + enabled: true, }; } @@ -100,18 +89,17 @@ vi.mock('qrcode', () => ({ })); describe('PlatformProfileRechargeModal', () => { - test('renders the four point products and forwards the selected buy action', async () => { + test('renders the six point products and forwards the selected buy action', async () => { const user = userEvent.setup(); const onBuy = vi.fn(); render( { expect( within(dialog).getByRole('button', { name: '60泥点 ¥6 购买' }), ).toBeTruthy(); + expect( + within(dialog).getByRole('button', { name: '180泥点 ¥18 购买' }), + ).toBeTruthy(); expect( within(dialog).getByRole('button', { - name: '180泥点 +90泥点 ¥18 购买', + name: '300泥点 ¥30 购买', }), ).toBeTruthy(); expect( within(dialog).getByRole('button', { - name: '300泥点 +150泥点 ¥30 购买', + name: '680泥点 ¥68 购买', }), ).toBeTruthy(); + expect(within(dialog).queryAllByText('首充加赠')).toHaveLength(0); + expect( + within(dialog).getByRole('button', { name: '1,280泥点 ¥128 购买' }), + ).toBeTruthy(); expect( - within(dialog).getByRole('button', { - name: '680泥点 +340泥点 ¥68 购买', - }), + within(dialog).getByRole('button', { name: '3,280泥点 ¥328 购买' }), ).toBeTruthy(); - expect(within(dialog).getAllByText('首充加赠')).toHaveLength(3); expect(within(dialog).queryByRole('tablist')).toBeNull(); - expect(within(dialog).queryByText('会员月卡')).toBeNull(); + expect(within(dialog).queryByText('Starter')).toBeNull(); await user.click( - within(dialog).getByRole('button', { - name: '180泥点 +90泥点 ¥18 购买', - }), + within(dialog).getByRole('button', { name: '180泥点 ¥18 购买' }), ); expect(onBuy).toHaveBeenCalledWith( expect.objectContaining({ productId: 'points_180' }), @@ -165,11 +155,10 @@ describe('PlatformProfileRechargeModal', () => { render( { ); expect(screen.getByText('暂无可购买套餐')).toBeTruthy(); - expect(screen.queryByText('会员月卡')).toBeNull(); + expect(screen.queryByText('Starter')).toBeNull(); expect(screen.queryByRole('tablist')).toBeNull(); }); @@ -198,11 +187,10 @@ describe('PlatformProfileRechargeModal', () => { render( { render( diff --git a/packages/shared/src/stores/createProfileWalletStore.test.ts b/packages/shared/src/stores/createProfileWalletStore.test.ts index c216c73dd..609132cdc 100644 --- a/packages/shared/src/stores/createProfileWalletStore.test.ts +++ b/packages/shared/src/stores/createProfileWalletStore.test.ts @@ -8,10 +8,9 @@ import { createProfileWalletStore } from './createProfileWalletStore'; function balance(totalPoints: number): ProfileMudPointBalance { return { - totalPoints, permanentPoints: totalPoints, - limitedPoints: 0, - limitedExpiresAt: null, + monthlyPoints: 0, + monthlyResetsAt: null, dailyFreePoints: 0, dailyFreeResetPoints: 20, dailyFreeResetsAt: '2026-08-04T00:00:00+08:00', @@ -254,31 +253,29 @@ describe('createProfileWalletStore', () => { expect(store.getState().mudPointBalance?.totalPoints).toBe(100); }); - test('retains a legacy total without inventing a balance breakdown', async () => { + test('treats a center response without a balance breakdown as an error', async () => { const store = createProfileWalletStore({ - getRechargeCenter: vi.fn().mockResolvedValue({ - walletBalance: 37, - } as ProfileRechargeCenterResponse), + getRechargeCenter: vi + .fn() + .mockResolvedValue({} as ProfileRechargeCenterResponse), }); store.getState().setWalletOwner('user-a'); await store.getState().onWalletBalanceMayHaveChanged(); expect(store.getState()).toMatchObject({ - legacyWalletBalance: 37, + legacyWalletBalance: null, mudPointBalance: null, mudPointBalanceStatus: 'error', mudPointBalanceError: '泥点明细读取失败', }); }); - test('clears a stale balance breakdown when a newer refresh only has a legacy total', async () => { + test('keeps the last known good breakdown when a newer refresh fails', async () => { const getRechargeCenter = vi .fn() .mockResolvedValueOnce(center(90)) - .mockResolvedValueOnce({ - walletBalance: 37, - } as ProfileRechargeCenterResponse); + .mockRejectedValueOnce(new Error('network down')); const store = createProfileWalletStore({ getRechargeCenter }); store.getState().setWalletOwner('user-a'); @@ -286,11 +283,11 @@ describe('createProfileWalletStore', () => { await store.getState().onWalletBalanceMayHaveChanged(); expect(store.getState()).toMatchObject({ - legacyWalletBalance: 37, - mudPointBalance: null, + legacyWalletBalance: 90, mudPointBalanceStatus: 'error', mudPointBalanceError: '泥点明细读取失败', }); + expect(store.getState().mudPointBalance?.permanentPoints).toBe(90); }); test('applies an owner-scoped legacy mutation snapshot atomically', () => { diff --git a/packages/shared/src/stores/createProfileWalletStore.ts b/packages/shared/src/stores/createProfileWalletStore.ts index 4cace6e85..2dd46c251 100644 --- a/packages/shared/src/stores/createProfileWalletStore.ts +++ b/packages/shared/src/stores/createProfileWalletStore.ts @@ -4,6 +4,10 @@ import type { ProfileMudPointBalance, ProfileRechargeCenterResponse, } from '../contracts/runtime'; +import { + type ProfileMudPointBalanceView, + resolveProfileMudPointBalanceView, +} from '../utils/mudPoints'; export type MudPointBalanceStatus = 'idle' | 'loading' | 'ready' | 'error'; @@ -22,7 +26,7 @@ export type ProfileWalletBalanceSnapshot = Readonly<{ export type ProfileWalletStore = { ownerUserId: string | null; - mudPointBalance: ProfileMudPointBalance | null; + mudPointBalance: ProfileMudPointBalanceView | null; legacyWalletBalance: number | null; mudPointBalanceStatus: MudPointBalanceStatus; mudPointBalanceError: string; @@ -32,7 +36,7 @@ export type ProfileWalletStore = { ) => ProfileWalletBalanceSnapshot | null; applyWalletBalanceSnapshot: ( snapshot: ProfileWalletBalanceSnapshot, - balance: ProfileMudPointBalance, + balance: ProfileMudPointBalance | ProfileMudPointBalanceView, ) => boolean; applyLegacyWalletBalanceSnapshot: ( snapshot: ProfileWalletBalanceSnapshot, @@ -119,9 +123,10 @@ export function createProfileWalletStore( snapshot.operationSequence, ); settledRefreshVersion = snapshot.invalidationVersion; + const mudPointBalance = resolveProfileMudPointBalanceView(balance); set({ - mudPointBalance: balance, - legacyWalletBalance: balance.totalPoints, + mudPointBalance, + legacyWalletBalance: mudPointBalance.totalPoints, mudPointBalanceStatus: 'ready', mudPointBalanceError: '', }); @@ -181,19 +186,16 @@ export function createProfileWalletStore( continue; } if (!center.mudPointBalance) { - if (Number.isFinite(center.walletBalance)) { - set({ - mudPointBalance: null, - legacyWalletBalance: center.walletBalance, - }); - } throw new Error('充值中心响应缺少泥点余额'); } settledRefreshVersion = requestedVersion; + const mudPointBalance = resolveProfileMudPointBalanceView( + center.mudPointBalance, + ); set({ - mudPointBalance: center.mudPointBalance, - legacyWalletBalance: center.mudPointBalance.totalPoints, + mudPointBalance, + legacyWalletBalance: mudPointBalance.totalPoints, mudPointBalanceStatus: 'ready', mudPointBalanceError: '', });