Merge remote-tracking branch 'origin/master' into feat/game-fork
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m25s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m31s
Project CI / Frontend tests (pull_request) Successful in 2m38s
Project CI / Native shell tests (pull_request) Failing after 2m52s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m29s
Project CI / Backend tests (pull_request) Successful in 7m44s
Project CI / Repository checks (pull_request) Successful in 5m37s

This commit is contained in:
2026-10-06 12:24:10 +08:00
203 changed files with 15892 additions and 13200 deletions
@@ -1,4 +1,4 @@
/* @vitest-environment jsdom */
/* @vitest-environment jsdom */
import { canvasViewportToWorldTransform } from '@genarrative/image-canvas-react';
import {
@@ -339,19 +339,18 @@ describe('ImageCanvasEditorView', () => {
updatedAt: null,
});
getPlatformProfileRechargeCenterMock.mockResolvedValue({
walletBalance: 1234,
mudPointBalance: {
totalPoints: 1234,
permanentPoints: 1000,
limitedPoints: 214,
limitedExpiresAt: '2026-07-31T16:00:00Z',
monthlyPoints: 214,
monthlyResetsAt: '2026-07-31T16:00:00Z',
dailyFreePoints: 20,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-07-12T16:00:00Z',
},
membership: {
status: 'normal',
tier: 'normal',
plan: 'normal',
startedAt: null,
expiresAt: null,
updatedAt: null,
@@ -359,11 +358,12 @@ describe('ImageCanvasEditorView', () => {
cycleResetsAt: null,
cycleGrantedPoints: 0,
cycleRemainingPoints: 0,
cyclePeriodDays: 30,
cycleIndex: 1,
cycleCount: 1,
cycleKind: 'monthly',
},
pointProducts: [],
membershipProducts: [],
benefits: [],
membershipPlans: [],
latestOrder: null,
hasPointsRecharged: false,
});
@@ -635,10 +635,9 @@ describe('ImageCanvasEditorView', () => {
usePlatformWalletStore
.getState()
.applyWalletBalanceSnapshot(walletSnapshot!, {
totalPoints: 1234,
permanentPoints: 1000,
limitedPoints: 214,
limitedExpiresAt: '2026-07-31T16:00:00Z',
monthlyPoints: 214,
monthlyResetsAt: '2026-07-31T16:00:00Z',
dailyFreePoints: 20,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-07-12T16:00:00Z',
@@ -663,7 +662,7 @@ describe('ImageCanvasEditorView', () => {
</AuthUiContext.Provider>,
);
expect(await screen.findByLabelText('泥点 1,234')).toBeTruthy();
expect(await screen.findByLabelText(/可用总额 1,234/)).toBeTruthy();
expect(getPlatformProfileDashboardMock).not.toHaveBeenCalled();
});
@@ -703,17 +702,17 @@ describe('ImageCanvasEditorView', () => {
expect(openAccountModal).toHaveBeenCalledTimes(1);
});
it('shows the owner-matched legacy wallet total without inventing breakdown rows', async () => {
it('shows the breakdown read failure without inventing rows', async () => {
const user = userEvent.setup();
usePlatformWalletStore.getState().setWalletOwner('user-1');
const walletSnapshot = usePlatformWalletStore
.getState()
.captureWalletBalanceSnapshot('user-1');
usePlatformWalletStore
.getState()
.applyLegacyWalletBalanceSnapshot(walletSnapshot!, 37);
getPlatformProfileRechargeCenterMock.mockResolvedValue({
walletBalance: 37,
getPlatformProfileRechargeCenterMock.mockRejectedValue(
new Error('network down'),
);
// 真实链路里 AuthGate 挂载 usePlatformWalletLifecycle 时会触发首次余额读取;本用例独立渲染
// ImageCanvasEditorView,需手动补上这一步。否则 store 停在 idle,入口把它当成 loading 而
// 跳过 hover 触发的明细请求,弹层会永远停在「余额读取中」。
await act(async () => {
await usePlatformWalletStore.getState().onWalletBalanceMayHaveChanged();
});
render(
<AuthUiContext.Provider
@@ -736,16 +735,18 @@ describe('ImageCanvasEditorView', () => {
);
const walletButton = await screen.findByRole('button', {
name: '泥点 37',
name: /可用总额 --/,
});
await user.hover(walletButton);
const details = await screen.findByRole('dialog', {
name: '泥点账户详情',
name: '会员与泥点账户详情',
});
expect(within(details).getByText('泥点明细读取失败')).toBeTruthy();
// 详情弹层打开即渲染 loading 态,读取失败的 rejection 在下一个微任务才提交;
// 必须等待错误文案出现,否则同步 getByText 会稳定命中的是「余额读取中」。
expect(await within(details).findByText('泥点明细读取失败')).toBeTruthy();
expect(within(details).queryByText('充值中心响应缺少泥点余额')).toBeNull();
expect(within(details).queryByText('不限时泥点')).toBeNull();
expect(within(details).queryByText('永久泥点')).toBeNull();
expect(within(details).queryByText('每日免费泥点')).toBeNull();
});
@@ -771,7 +772,7 @@ describe('ImageCanvasEditorView', () => {
);
expect(
(await screen.findByRole('button', { name: '泥点 --' })).getAttribute(
(await screen.findByRole('button', { name: /可用总额 --/ })).getAttribute(
'aria-busy',
),
).toBe('true');
@@ -786,10 +787,9 @@ describe('ImageCanvasEditorView', () => {
usePlatformWalletStore
.getState()
.applyWalletBalanceSnapshot(walletSnapshot!, {
totalPoints: 1234,
permanentPoints: 1000,
limitedPoints: 0,
limitedExpiresAt: null,
monthlyPoints: 0,
monthlyResetsAt: null,
dailyFreePoints: 234,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-07-12T16:00:00Z',
@@ -815,17 +815,16 @@ describe('ImageCanvasEditorView', () => {
);
const walletButton = await screen.findByRole('button', {
name: '泥点 1,234',
name: /可用总额 1,234/,
});
await user.hover(walletButton);
const details = await screen.findByRole('dialog', {
name: '泥点账户详情',
name: '会员与泥点账户详情',
});
expect(within(details).getByText('不限时泥点')).toBeTruthy();
expect(within(details).queryByText('限时泥点')).toBeNull();
expect(within(details).getByText('每日免费泥点')).toBeTruthy();
expect(within(details).getByText('永久泥点')).toBeTruthy();
expect(within(details).getByText('月度泥点')).toBeTruthy();
await user.click(within(details).getByRole('button', { name: '使用详情' }));
expect(
@@ -919,11 +918,11 @@ describe('ImageCanvasEditorView', () => {
const viewport = screen.getByLabelText('画布工作区');
fireEvent.click(
await screen.findByRole('button', {
name: '充值',
name: '会员与泥点',
}),
);
expect(
await screen.findByRole('dialog', { name: '购买更多泥点' }),
await screen.findByRole('dialog', { name: '会员与泥点' }),
).toBeTruthy();
fireEvent.keyDown(window, { key: 'Delete', code: 'Delete' });
@@ -949,7 +948,7 @@ describe('ImageCanvasEditorView', () => {
).toBe('true');
expect(viewport.getAttribute('aria-disabled')).toBe('true');
fireEvent.click(screen.getByRole('button', { name: '关闭购买更多泥点' }));
fireEvent.click(screen.getByRole('button', { name: '关闭会员与泥点' }));
const resumedToolbar = await screen.findByRole('toolbar', {
name: '快速编辑框选工具',
@@ -561,6 +561,7 @@ export function ImageCanvasEditorView({
buyRechargeProduct,
closeNativeWechatPayment,
confirmNativeWechatPayment,
isLoadingMembershipQuote,
isLoadingRechargeCenter,
isLoadingWalletLedger,
isRechargeOpen,
@@ -569,12 +570,15 @@ export function ImageCanvasEditorView({
isWalletLedgerOpen,
loadRechargeCenter,
loadWalletLedger,
membershipQuote,
membershipQuoteError,
nativeWechatPayment,
openRechargeOrRewardCodeModal,
openWalletLedgerPanel,
rechargeCenter,
rechargeError,
rechargePaymentResult,
requestMembershipQuote,
rewardCodeError,
rewardCodeInput,
rewardCodeSuccess,
@@ -584,6 +588,7 @@ export function ImageCanvasEditorView({
setRechargePaymentResult,
setRewardCodeInput,
submittingRechargeProductId,
submitMembershipCheckout,
submitRewardCode,
walletLedger,
walletLedgerError,
@@ -2522,7 +2527,9 @@ export function ImageCanvasEditorView({
walletBreakdown: mudPointBalance,
isWalletBalanceLoading,
isWalletDetailsLoading: isLoadingRechargeCenter,
walletDetailsError: rechargeError || mudPointBalanceError || null,
// 明细弹层展示的正是钱包 store 的 breakdown;其读取失败文案优先于充值弹层的原始报错,
// 避免把「读详情」的原始错误串(如 network down)泄漏到顶栏明细里。
walletDetailsError: mudPointBalanceError || rechargeError || null,
currentUser: authUi?.user,
assetExportStatus,
isExportingAssets,
@@ -2746,9 +2753,14 @@ export function ImageCanvasEditorView({
error={rechargeError}
submittingProductId={submittingRechargeProductId}
nativePayment={nativeWechatPayment}
membershipQuote={membershipQuote}
isLoadingMembershipQuote={isLoadingMembershipQuote}
membershipQuoteError={membershipQuoteError}
onClose={() => setIsRechargeOpen(false)}
onRetry={loadRechargeCenter}
onBuy={buyRechargeProduct}
onRequestMembershipQuote={requestMembershipQuote}
onSubmitMembershipCheckout={submitMembershipCheckout}
onConfirmNativePayment={confirmNativeWechatPayment}
onCloseNativePayment={closeNativeWechatPayment}
/>
@@ -17,8 +17,8 @@ import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
const walletBreakdown = {
totalPoints: 1_234,
permanentPoints: 1_000,
limitedPoints: 214,
limitedExpiresAt: '2026-07-31T16:00:00Z',
monthlyPoints: 214,
monthlyResetsAt: '2026-07-31T16:00:00Z',
dailyFreePoints: 20,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-07-12T16:00:00Z',
@@ -105,27 +105,24 @@ describe('ImageCanvasTopbarView', () => {
const user = userEvent.setup();
const props = renderTopbar({
walletBalance: 12_345,
walletBreakdown: {
...walletBreakdown,
totalPoints: 12_345,
},
});
const walletChip = screen.getByRole('button', {
name: '泥点 12,345',
name: /可用总额 12,345/,
});
expect(walletChip.textContent).toBe('泥点 1.2万');
expect(walletChip.textContent).toContain('可用总额 1.2万');
await user.hover(walletChip);
const details = screen.getByRole('dialog', { name: '泥点账户详情' });
expect(within(details).getByText('不限时泥点')).toBeTruthy();
expect(within(details).queryByText('限时泥点')).toBeNull();
expect(within(details).getByText('每日免费泥点')).toBeTruthy();
const details = screen.getByRole('dialog', {
name: '会员与泥点账户详情',
});
expect(within(details).getByText('永久泥点')).toBeTruthy();
expect(within(details).getByText('月度泥点')).toBeTruthy();
await user.click(within(details).getByRole('button', { name: '使用详情' }));
expect(props.onOpenWalletLedger).toHaveBeenCalledTimes(1);
await user.click(screen.getByRole('button', { name: '充值' }));
await user.click(screen.getByRole('button', { name: '会员与泥点' }));
expect(props.onRecharge).toHaveBeenCalledTimes(1);
expect(props.onRequestWalletDetails).toHaveBeenCalledTimes(1);
});
@@ -77,8 +77,8 @@ describe('PlatformActiveProfileView', () => {
mudPointBalance={{
totalPoints: 108,
permanentPoints: 88,
limitedPoints: 0,
limitedExpiresAt: null,
monthlyPoints: 0,
monthlyResetsAt: null,
dailyFreePoints: 20,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-08-04T00:00:00+08:00',
@@ -16,7 +16,7 @@ import { useCallback, useRef, useState } from 'react';
import type {
AuthUser,
ProfileDashboardSummary,
ProfileMudPointBalance,
ProfileMudPointBalanceView,
} from '@/packages/shared/src';
import profileClockImage from '../../../media/profile/_Image (1).png';
@@ -67,7 +67,7 @@ type PlatformActiveProfileViewProps = {
isLoadingDashboard: boolean;
isLoadingWalletBalance: boolean;
legacyWalletBalance: number | null;
mudPointBalance: ProfileMudPointBalance | null;
mudPointBalance: ProfileMudPointBalanceView | null;
onLogin: () => void;
onOpenApiKeys: () => void;
onOpenPaymentService?: () => void;
@@ -250,7 +250,7 @@ function formatDashboardCount(value: number) {
}
function formatWalletBalance(
balance: ProfileMudPointBalance | null,
balance: ProfileMudPointBalanceView | null,
legacyBalance: number | null,
isLoading: boolean,
) {
@@ -44,7 +44,13 @@ const profileClientMock = vi.hoisted(() => ({
const profileCenterMock = vi.hoisted(() => ({
isWalletLedgerOpen: false,
rechargeCenter: null as { walletBalance: number } | null,
rechargeCenter: null as {
mudPointBalance?: {
dailyFreePoints: number;
monthlyPoints: number;
permanentPoints: number;
};
} | null,
}));
const gameDistributionMock = vi.hoisted(() => ({
@@ -266,12 +272,10 @@ describe('PlatformEntryActiveFlowShell', () => {
updatedAt: null,
});
profileClientMock.getPlatformProfileRechargeCenter.mockResolvedValue({
walletBalance: 888,
mudPointBalance: {
totalPoints: 207,
permanentPoints: 180,
limitedPoints: 7,
limitedExpiresAt: '2026-08-31T16:00:00Z',
monthlyPoints: 7,
monthlyResetsAt: '2026-08-31T16:00:00Z',
dailyFreePoints: 20,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-08-04T00:00:00+08:00',
@@ -285,7 +289,7 @@ describe('PlatformEntryActiveFlowShell', () => {
/>,
);
expect(await screen.findByLabelText('泥点 207')).toBeTruthy();
expect(await screen.findByLabelText(/可用总额 207/)).toBeTruthy();
expect(screen.getByRole('button', { name: '下载客户端' })).toBeTruthy();
rerender(
@@ -315,7 +319,14 @@ describe('PlatformEntryActiveFlowShell', () => {
authUiMock.value.canAccessProtectedData = true;
profileClientMock.getPlatformProfileDashboard.mockResolvedValue(null);
profileClientMock.getPlatformProfileRechargeCenter.mockResolvedValue({
walletBalance: 37,
mudPointBalance: {
permanentPoints: 37,
monthlyPoints: 0,
monthlyResetsAt: null,
dailyFreePoints: 0,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-08-04T00:00:00+08:00',
},
});
profileCenterMock.isWalletLedgerOpen = true;
@@ -359,10 +370,9 @@ describe('PlatformEntryActiveFlowShell', () => {
profileClientMock.getPlatformProfileDashboard.mockResolvedValue(null);
profileClientMock.getPlatformProfileRechargeCenter.mockResolvedValue({
mudPointBalance: {
totalPoints: 66,
permanentPoints: 66,
limitedPoints: 0,
limitedExpiresAt: null,
monthlyPoints: 0,
monthlyResetsAt: null,
dailyFreePoints: 0,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-08-04T00:00:00+08:00',
@@ -374,7 +384,7 @@ describe('PlatformEntryActiveFlowShell', () => {
setSelectionStage={vi.fn()}
/>,
);
await screen.findByLabelText('泥点 66');
await screen.findByLabelText(/可用总额 66/);
let resolveNextOwner!: (value: unknown) => void;
profileClientMock.getPlatformProfileRechargeCenter.mockImplementation(
@@ -391,7 +401,7 @@ describe('PlatformEntryActiveFlowShell', () => {
/>,
);
expect(screen.queryByLabelText('泥点 66')).toBeNull();
expect(screen.queryByLabelText(/可用总额 66/)).toBeNull();
await waitFor(() => {
expect(usePlatformWalletStore.getState()).toMatchObject({
ownerUserId: 'user-2',
@@ -400,16 +410,15 @@ describe('PlatformEntryActiveFlowShell', () => {
});
resolveNextOwner({
mudPointBalance: {
totalPoints: 67,
permanentPoints: 67,
limitedPoints: 0,
limitedExpiresAt: null,
monthlyPoints: 0,
monthlyResetsAt: null,
dailyFreePoints: 0,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-08-04T00:00:00+08:00',
},
});
await screen.findByLabelText('泥点 67');
await screen.findByLabelText(/可用总额 67/);
});
it('coalesces visibility and focus refreshes when the page returns to the foreground', async () => {
@@ -427,10 +436,9 @@ describe('PlatformEntryActiveFlowShell', () => {
profileClientMock.getPlatformProfileDashboard.mockResolvedValue(null);
profileClientMock.getPlatformProfileRechargeCenter.mockResolvedValue({
mudPointBalance: {
totalPoints: 10,
permanentPoints: 10,
limitedPoints: 0,
limitedExpiresAt: null,
monthlyPoints: 0,
monthlyResetsAt: null,
dailyFreePoints: 0,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-08-04T00:00:00+08:00',
@@ -442,7 +450,7 @@ describe('PlatformEntryActiveFlowShell', () => {
setSelectionStage={vi.fn()}
/>,
);
await screen.findByLabelText('泥点 10');
await screen.findByLabelText(/可用总额 10/);
await act(async () => {
document.dispatchEvent(new Event('visibilitychange'));
@@ -23,6 +23,7 @@ import { PlatformProfileRechargeModal } from '../../../packages/shared/src/compo
import { PlatformProfileReferralModal } from '../../../packages/shared/src/components/PlatformProfileReferralModal';
import { PlatformProfileWalletLedgerModal } from '../../../packages/shared/src/components/PlatformProfileWalletLedgerModal';
import type { ProfileDashboardSummary } from '../../../packages/shared/src/contracts/runtime';
import { resolveProfileMudPointDisplayTotal } from '../../../packages/shared/src/utils/mudPoints';
import {
hasAppHistoryBackEntry,
pushAppHistoryPath,
@@ -424,7 +425,9 @@ export function PlatformEntryFlowShellImpl({
});
const legacyWalletBalance = walletOwnerMatchesCurrentUser
? (storedLegacyWalletBalance ??
profileCenter.rechargeCenter?.walletBalance ??
resolveProfileMudPointDisplayTotal(
profileCenter.rechargeCenter?.mudPointBalance,
) ??
null)
: null;
@@ -1146,9 +1149,14 @@ export function PlatformEntryFlowShellImpl({
error={profileCenter.rechargeError}
submittingProductId={profileCenter.submittingRechargeProductId}
nativePayment={profileCenter.nativeWechatPayment}
membershipQuote={profileCenter.membershipQuote}
isLoadingMembershipQuote={profileCenter.isLoadingMembershipQuote}
membershipQuoteError={profileCenter.membershipQuoteError}
onClose={() => profileCenter.setIsRechargeOpen(false)}
onRetry={profileCenter.loadRechargeCenter}
onBuy={profileCenter.buyRechargeProduct}
onRequestMembershipQuote={profileCenter.requestMembershipQuote}
onSubmitMembershipCheckout={profileCenter.submitMembershipCheckout}
onConfirmNativePayment={profileCenter.confirmNativeWechatPayment}
onCloseNativePayment={profileCenter.closeNativeWechatPayment}
/>
@@ -11,6 +11,7 @@ const PLATFORM_WALLET_LEDGER_SOURCE_LABELS = {
snapshot_sync: '账户同步',
membership_period_grant: '会员周期发放',
membership_period_reset: '会员周期重置',
membership_upgrade_grant: '会员升级补点',
daily_free_grant: '每日免费发放',
daily_free_reset: '每日免费重置',
asset_operation_consume: '资产操作消耗',
@@ -32,10 +32,9 @@ function deferred<T>() {
function mudPointBalance(totalPoints: number) {
return {
totalPoints,
permanentPoints: totalPoints,
limitedPoints: 0,
limitedExpiresAt: null,
monthlyPoints: 0,
monthlyResetsAt: null,
dailyFreePoints: 0,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-08-07T00:00:00+08:00',
@@ -52,7 +51,6 @@ function nativeRechargeResponse(totalPoints = 80, orderId = 'order-a') {
expirationCheckedAt: null,
},
center: {
walletBalance: totalPoints,
mudPointBalance: mudPointBalance(totalPoints),
},
wechatNativePayment: {
@@ -76,14 +74,13 @@ describe('usePlatformProfileCenterController recharge fallback', () => {
);
});
test('keeps the response legacy balance when no shared snapshot exists', async () => {
test('keeps the response three-pool balance when no shared snapshot exists', async () => {
usePlatformWalletStore.getState().setWalletOwner('user-a');
profileClientMocks.getPlatformProfileRechargeCenter.mockResolvedValue({
walletBalance: 37,
mudPointBalance: mudPointBalance(37),
membership: null,
pointProducts: [],
membershipProducts: [],
benefits: [],
membershipPlans: [],
latestOrder: null,
hasPointsRecharged: false,
});
@@ -92,7 +89,9 @@ describe('usePlatformProfileCenterController recharge fallback', () => {
act(() => result.current.loadRechargeCenter());
await waitFor(() => {
expect(result.current.rechargeCenter?.walletBalance).toBe(37);
expect(
result.current.rechargeCenter?.mudPointBalance?.permanentPoints,
).toBe(37);
});
});
@@ -198,7 +197,9 @@ describe('usePlatformProfileCenterController recharge fallback', () => {
expect(result.current.submittingRechargeProductId).toBeNull();
expect(result.current.nativeWechatPayment?.orderId).toBe('order-b');
expect(result.current.rechargePaymentResult).toBeNull();
expect(result.current.rechargeCenter?.walletBalance).toBe(90);
expect(
result.current.rechargeCenter?.mudPointBalance?.permanentPoints,
).toBe(90);
expect(usePlatformWalletStore.getState()).toMatchObject({
ownerUserId: 'user-b',
mudPointBalance: mudPointBalance(90),
@@ -19,19 +19,17 @@ vi.mock('../../services/platform-entry/platformProfileClient', async () => {
});
const rechargeCenter: ProfileRechargeCenterResponse = {
walletBalance: 100,
mudPointBalance: {
totalPoints: 100,
permanentPoints: 100,
limitedPoints: 0,
limitedExpiresAt: null,
monthlyPoints: 0,
monthlyResetsAt: null,
dailyFreePoints: 0,
dailyFreeResetPoints: 0,
dailyFreeResetsAt: null,
},
membership: {
status: 'normal',
tier: 'normal',
plan: 'normal',
startedAt: null,
expiresAt: null,
updatedAt: null,
@@ -39,11 +37,12 @@ const rechargeCenter: ProfileRechargeCenterResponse = {
cycleResetsAt: null,
cycleGrantedPoints: 0,
cycleRemainingPoints: 0,
cyclePeriodDays: 30,
cycleIndex: 1,
cycleCount: 1,
cycleKind: 'monthly',
},
pointProducts: [],
membershipProducts: [],
benefits: [],
membershipPlans: [],
latestOrder: null,
hasPointsRecharged: false,
};
@@ -1,8 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { PlatformProfileRechargeNativePaymentState } from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
import { type PlatformProfileRechargeNativePaymentState } from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
import { resolveMembershipProductId } from '../../../packages/shared/src/components/PlatformProfileRechargeModal/membershipProduct';
import {
type ConfirmWechatProfileRechargeOrderResponse,
type ProfileMembershipCycleKind,
type ProfileMembershipPlan,
type ProfileMembershipUpgradeQuote,
type ProfileRechargeCenterResponse,
type ProfileRechargeOrder,
type ProfileRechargeProduct,
@@ -29,6 +33,7 @@ import { requestWechatJsapiPayment } from '../../services/payment/wechatJsapiPay
import {
confirmWechatPlatformProfileRechargeOrder,
createPlatformProfileRechargeOrder,
getPlatformProfileMembershipUpgradeQuote,
getPlatformProfileRechargeCenter,
getPlatformProfileReferralInviteCenter,
getPlatformProfileWalletLedger,
@@ -392,6 +397,13 @@ export function usePlatformProfileCenterController({
useState<NativeWechatPaymentState | null>(null);
const [submittingRechargeProductId, setSubmittingRechargeProductId] =
useState<string | null>(null);
const [membershipQuote, setMembershipQuote] =
useState<ProfileMembershipUpgradeQuote | null>(null);
const [isLoadingMembershipQuote, setIsLoadingMembershipQuote] =
useState(false);
const [membershipQuoteError, setMembershipQuoteError] = useState<
string | null
>(null);
const [isWalletLedgerOpen, setIsWalletLedgerOpen] = useState(false);
const [walletLedger, setWalletLedger] =
useState<ProfileWalletLedgerResponse | null>(null);
@@ -431,6 +443,7 @@ export function usePlatformProfileCenterController({
const confirmingWechatRechargeOrderRef =
useRef<WechatRechargeOrderLifecycle | null>(null);
const rechargeSubmissionRef = useRef<symbol | null>(null);
const membershipQuoteRequestRef = useRef<symbol | null>(null);
const accountWriteAbortControllerRef = useRef(new AbortController());
const rechargeCenterReadRevisionRef = useRef(0);
const accountLifecycleRevisionRef = useRef(0);
@@ -555,6 +568,10 @@ export function usePlatformProfileCenterController({
setWechatRechargeOrderConfirmationState(null);
setNativeWechatPayment(null);
setSubmittingRechargeProductId(null);
setMembershipQuote(null);
setIsLoadingMembershipQuote(false);
setMembershipQuoteError(null);
membershipQuoteRequestRef.current = null;
setIsWalletLedgerOpen(false);
setWalletLedger(null);
setWalletLedgerError(null);
@@ -1028,7 +1045,7 @@ export function usePlatformProfileCenterController({
}, [isRechargeOrderCurrent, nativeWechatPayment]);
const buyRechargeProduct = useCallback(
(product: ProfileRechargeProduct) => {
(product: Pick<ProfileRechargeProduct, 'productId' | 'kind'>) => {
const account = captureAccountLifecycle();
if (
rechargeSubmissionRef.current ||
@@ -1333,6 +1350,63 @@ export function usePlatformProfileCenterController({
],
);
const requestMembershipQuote = useCallback(
(plan: ProfileMembershipPlan) => {
const account = captureAccountLifecycle();
const requestSignal = accountWriteAbortControllerRef.current.signal;
if (
!account.ownerUserId ||
!isAccountLifecycleCurrent(account) ||
requestSignal.aborted
) {
return;
}
const requestToken = Symbol('profile-membership-quote');
membershipQuoteRequestRef.current = requestToken;
const isLatestQuoteRequest = () =>
isAccountLifecycleCurrent(account) &&
membershipQuoteRequestRef.current === requestToken;
setMembershipQuote(null);
setMembershipQuoteError(null);
setIsLoadingMembershipQuote(true);
void getPlatformProfileMembershipUpgradeQuote(plan, {
signal: requestSignal,
})
.then((quote) => {
if (!isLatestQuoteRequest()) {
return;
}
setMembershipQuote(quote);
})
.catch((error: unknown) => {
if (!isLatestQuoteRequest()) {
return;
}
setMembershipQuoteError(
error instanceof Error ? error.message : '读取升级报价失败',
);
})
.finally(() => {
if (isLatestQuoteRequest()) {
setIsLoadingMembershipQuote(false);
}
});
},
[captureAccountLifecycle, isAccountLifecycleCurrent],
);
const submitMembershipCheckout = useCallback(
(plan: ProfileMembershipPlan, cycleKind: ProfileMembershipCycleKind) => {
// 会员商品没有商品配置行:标题与价格由后端按档位与周期现算,
// 这里只需要把 productId 交给同一条下单链路。
buyRechargeProduct({
productId: resolveMembershipProductId(plan, cycleKind),
kind: 'membership',
});
},
[buyRechargeProduct],
);
const confirmNativeWechatPayment = useCallback(() => {
if (!nativeWechatPayment || nativeWechatPayment.isConfirming) {
return;
@@ -1864,8 +1938,6 @@ export function usePlatformProfileCenterController({
}
return {
...rechargeCenter,
walletBalance:
mudPointBalance?.totalPoints ?? rechargeCenter.walletBalance,
mudPointBalance: mudPointBalance ?? rechargeCenter.mudPointBalance,
};
}, [mudPointBalance, rechargeCenter, walletOwnerMatchesCurrentUser]);
@@ -1874,6 +1946,11 @@ export function usePlatformProfileCenterController({
closeNativeWechatPayment,
closeProfilePopupPanel,
confirmNativeWechatPayment,
isLoadingMembershipQuote,
membershipQuote,
membershipQuoteError,
requestMembershipQuote,
submitMembershipCheckout,
copyInviteCode,
inviteCodeCopyState: inviteCodeCopyState as CopyFeedbackState,
inviteLinkCopyState: inviteLinkCopyState as CopyFeedbackState,
@@ -11,6 +11,8 @@ import type {
ExternalApiKeyListResponse,
ExternalApiKeyMutationResponse,
ProfileDashboardSummary,
ProfileMembershipPlan,
ProfileMembershipUpgradeQuote,
ProfileRechargeCenterResponse,
ProfileRechargeOrder,
ProfileReferralInviteCenterResponse,
@@ -231,6 +233,22 @@ export function createPlatformProfileRechargeOrder(
);
}
export function getPlatformProfileMembershipUpgradeQuote(
targetPlan: ProfileMembershipPlan,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ProfileMembershipUpgradeQuote>(
'/membership/upgrade-quote',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetPlan }),
},
'读取升级报价失败',
options,
);
}
export function confirmWechatPlatformProfileRechargeOrder(
orderId: string,
options: PlatformProfileRequestOptions = {},
+2 -4
View File
@@ -22,12 +22,10 @@ beforeEach(() => {
vi.clearAllMocks();
usePlatformWalletStore.getState().resetWalletBalance();
profileClientMocks.getPlatformProfileRechargeCenter.mockResolvedValue({
walletBalance: 42,
mudPointBalance: {
totalPoints: 42,
permanentPoints: 42,
limitedPoints: 0,
limitedExpiresAt: null,
monthlyPoints: 0,
monthlyResetsAt: null,
dailyFreePoints: 0,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-08-08T00:00:00+08:00',