修复兼容钱包总额展示

向个人中心和图片画板传递 owner 匹配的 legacy 总额

保持钱包分桶与账单明细为空避免伪造数据

补充个人中心和图片画板回归测试及契约文档
This commit is contained in:
2026-08-07 10:27:37 +08:00
parent ab66a50cfa
commit 58bf56ec56
9 changed files with 90 additions and 10 deletions
@@ -670,6 +670,45 @@ describe('ImageCanvasEditorView', () => {
expect(openAccountModal).toHaveBeenCalledTimes(1);
});
it('shows the owner-matched legacy wallet total without inventing breakdown rows', async () => {
const user = userEvent.setup();
usePlatformWalletStore.getState().setWalletOwner('user-1');
getPlatformProfileRechargeCenterMock.mockResolvedValue({
walletBalance: 37,
});
render(
<AuthUiContext.Provider
value={createAuthValue({
user: {
id: 'user-1',
publicUserCode: 'U001',
displayName: '测试用户',
avatarUrl: null,
phoneNumberMasked: '138****0000',
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
},
canAccessProtectedData: true,
})}
>
<ImageCanvasEditorView legacyWalletBalance={37} />
</AuthUiContext.Provider>,
);
const walletButton = await screen.findByRole('button', {
name: '泥点 37',
});
await user.hover(walletButton);
const details = await screen.findByRole('dialog', {
name: '泥点账户详情',
});
expect(within(details).getByText('充值中心响应缺少泥点余额')).toBeTruthy();
expect(within(details).queryByText('不限时泥点')).toBeNull();
expect(within(details).queryByText('每日免费泥点')).toBeNull();
});
it('opens the shared wallet breakdown and ledger from the canvas topbar', async () => {
const user = userEvent.setup();
usePlatformWalletStore.getState().setWalletOwner('user-1');
@@ -137,6 +137,7 @@ const CANVAS_STARTUP_TOOLS: CanvasStartupTool[] = [
];
type ImageCanvasEditorViewProps = {
legacyWalletBalance?: number | null;
onProjectAccessLost?: () => void;
};
@@ -319,6 +320,7 @@ const DEAD_INLINE_PLACEHOLDER_NOTICE =
'上次的完美像素处理未完成,画布占位已清理。请确认素材库是否已生成派生图。';
export function ImageCanvasEditorView({
legacyWalletBalance = null,
onProjectAccessLost,
}: ImageCanvasEditorViewProps = {}) {
const authUi = useAuthUi();
@@ -352,7 +354,9 @@ export function ImageCanvasEditorView({
const mudPointBalanceError = walletOwnerMatchesCurrentUser
? storedMudPointBalanceError
: '';
const walletBalance = mudPointBalance?.totalPoints ?? null;
const walletBalance =
mudPointBalance?.totalPoints ??
(walletOwnerMatchesCurrentUser ? legacyWalletBalance : null);
const editorRootRef = useRef<HTMLElement | null>(null);
const canvasViewportRef = useRef<HTMLDivElement | null>(null);
const assetListRef = useRef<HTMLDivElement | null>(null);
@@ -49,6 +49,7 @@ describe('PlatformActiveProfileView', () => {
dashboard={null}
isLoadingDashboard={false}
isLoadingWalletBalance={false}
legacyWalletBalance={null}
mudPointBalance={null}
user={null}
/>,
@@ -71,6 +72,7 @@ describe('PlatformActiveProfileView', () => {
}}
isLoadingDashboard={false}
isLoadingWalletBalance={false}
legacyWalletBalance={null}
mudPointBalance={{
totalPoints: 108,
permanentPoints: 88,
@@ -114,6 +116,7 @@ describe('PlatformActiveProfileView', () => {
dashboard={null}
isLoadingDashboard={false}
isLoadingWalletBalance={false}
legacyWalletBalance={null}
mudPointBalance={null}
user={authenticatedUser}
/>,
@@ -148,6 +151,7 @@ describe('PlatformActiveProfileView', () => {
dashboard={null}
isLoadingDashboard={false}
isLoadingWalletBalance={false}
legacyWalletBalance={null}
mudPointBalance={null}
user={authenticatedUser}
/>,
@@ -212,6 +216,7 @@ describe('PlatformActiveProfileView', () => {
dashboard={null}
isLoadingDashboard={false}
isLoadingWalletBalance={false}
legacyWalletBalance={null}
mudPointBalance={null}
user={authenticatedUser}
/>,
@@ -63,6 +63,7 @@ type PlatformActiveProfileViewProps = {
dashboard: ProfileDashboardSummary | null;
isLoadingDashboard: boolean;
isLoadingWalletBalance: boolean;
legacyWalletBalance: number | null;
mudPointBalance: ProfileMudPointBalance | null;
onLogin: () => void;
onOpenApiKeys: () => void;
@@ -245,11 +246,15 @@ function formatDashboardCount(value: number) {
function formatWalletBalance(
balance: ProfileMudPointBalance | null,
legacyBalance: number | null,
isLoading: boolean,
) {
if (balance) {
return formatDashboardCount(balance.totalPoints);
}
if (legacyBalance !== null) {
return formatDashboardCount(legacyBalance);
}
if (isLoading) {
return '读取中';
}
@@ -267,6 +272,7 @@ export function PlatformActiveProfileView({
dashboard,
isLoadingDashboard,
isLoadingWalletBalance,
legacyWalletBalance,
mudPointBalance,
onLogin,
onOpenApiKeys,
@@ -562,6 +568,7 @@ export function PlatformActiveProfileView({
label="泥点余额"
value={formatWalletBalance(
mudPointBalance,
legacyWalletBalance,
isLoadingWalletBalance,
)}
icon={Coins}
@@ -99,7 +99,16 @@ vi.mock('../project/ProjectGalleryView', () => ({
}));
vi.mock('../image-editor/ImageCanvasEditorView', () => ({
ImageCanvasEditorView: () => <main aria-label="图片画布编辑器" />,
ImageCanvasEditorView: ({
legacyWalletBalance,
}: {
legacyWalletBalance?: number | null;
}) => (
<main
aria-label="图片画布编辑器"
data-legacy-wallet-balance={legacyWalletBalance ?? ''}
/>
),
}));
vi.mock(
@@ -214,7 +223,7 @@ describe('PlatformEntryActiveFlowShell', () => {
expect(screen.queryByText('999')).toBeNull();
});
it('uses the owner-matched legacy recharge balance for an empty ledger', async () => {
it('passes the owner-matched legacy total to profile and editor while keeping the ledger empty', async () => {
authUiMock.value.user = {
id: 'user-1',
publicUserCode: '100001',
@@ -233,15 +242,29 @@ describe('PlatformEntryActiveFlowShell', () => {
profileCenterMock.isWalletLedgerOpen = true;
profileCenterMock.rechargeCenter = { walletBalance: 37 };
render(
const { rerender } = render(
<PlatformEntryFlowShellImpl
selectionStage="profile"
setSelectionStage={vi.fn()}
/>,
);
expect(
await screen.findByRole('button', { name: '泥点余额 37' }),
).toBeTruthy();
expect(await screen.findByText('37泥点')).toBeTruthy();
expect(screen.getByText('暂无账单记录')).toBeTruthy();
rerender(
<PlatformEntryFlowShellImpl
selectionStage="image-editor"
setSelectionStage={vi.fn()}
/>,
);
const imageEditor = await screen.findByRole('main', {
name: '图片画布编辑器',
});
expect(imageEditor.getAttribute('data-legacy-wallet-balance')).toBe('37');
});
it('clears the previous wallet synchronously when the authenticated account changes', async () => {
@@ -287,6 +287,9 @@ export function PlatformEntryFlowShellImpl({
requestLogin: () => authUi?.openLoginModal(),
currentUser: authUi?.user,
});
const legacyWalletBalance = walletOwnerMatchesCurrentUser
? (profileCenter.rechargeCenter?.walletBalance ?? null)
: null;
const openCreation = useCallback(() => {
if (!isDesktopLayout) {
@@ -362,6 +365,7 @@ export function PlatformEntryFlowShellImpl({
<div className="image-editor-stage-shell flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
<Suspense fallback={<LoadingPanel label="正在加载编辑器..." />}>
<ImageCanvasEditorView
legacyWalletBalance={legacyWalletBalance}
onProjectAccessLost={replaceWithProjectGallery}
/>
</Suspense>
@@ -370,9 +374,6 @@ export function PlatformEntryFlowShellImpl({
}
const isAuthenticated = Boolean(authUi?.user);
const legacyWalletBalance = walletOwnerMatchesCurrentUser
? (profileCenter.rechargeCenter?.walletBalance ?? null)
: null;
const balance = mudPointBalance?.totalPoints ?? legacyWalletBalance;
const isCreationStage =
!isProfileStage &&
@@ -543,6 +544,7 @@ export function PlatformEntryFlowShellImpl({
dashboard={dashboard}
isLoadingDashboard={isLoadingDashboard}
isLoadingWalletBalance={mudPointBalanceStatus === 'loading'}
legacyWalletBalance={legacyWalletBalance}
mudPointBalance={mudPointBalance}
user={authUi?.user}
onLogin={() => authUi?.openLoginModal()}