恢复空账单的旧版余额兜底
在钱包 owner 匹配时保留充值中心 legacy walletBalance 共享泥点明细仍优先于兼容展示余额 覆盖仅返回旧版余额时空账单显示 37 泥点 同步更新账户余额兼容边界
This commit is contained in:
@@ -59,7 +59,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当
|
||||
|
||||
## 账户与充值
|
||||
|
||||
1. 主站、图片画板和 AI Game Creator 统一使用 `packages/shared` 的依赖注入式钱包 Zustand Store;主站与 Tauri 客户端只保留各自的 URL、认证和重试 transport adapter。主站顶部、图片画板顶部和“我的”统计必须消费同一份 `ProfileMudPointBalance` 快照,泥点总额固定取 `totalPoints`,不得混用 dashboard 的 `walletBalance`。切换或退出账号必须立即清空快照并拒绝旧账号在途响应;消费端在 owner 绑定 effect 生效前也必须按当前用户 ID 同步屏蔽 owner 不匹配的快照,旧账号请求不得阻塞新账号首次读取。普通充值中心 GET 必须在发起时捕获钱包 owner 生命周期与 invalidation 版本;回包只能结算不晚于该版本的刷新,过期快照不得覆盖余额或中止更新的终态刷新。生成、退款、充值、兑换码等余额可能变化事件只通知 Store 合并刷新。external generation 在 worker 领取后才预扣泥点,因此主站必须在任一项目的 active task 轮询期间持续推动钱包合并刷新,并在 `completed / failed` 任一终态再刷新以覆盖成功结算或失败退款;任务列表首次 bootstrap 的 active / terminal 任一读取瞬时失败时必须有界退避重试,成功取得全局 active ID 后再交给常规轮询,不能把首次空状态固化为停止钱包通知。公共泥点资产入口收起态展示“泥点图标 + 泥点总额 | 充值”;桌面端通过 hover / focus 展开,移动端通过点击展开。展开态只展示不限时泥点、每日免费泥点及后端返回的每日重置额度,并提供“使用详情”入口;余额都以后端充值中心 read model 为准,前端不得自行相减推算。
|
||||
1. 主站、图片画板和 AI Game Creator 统一使用 `packages/shared` 的依赖注入式钱包 Zustand Store;主站与 Tauri 客户端只保留各自的 URL、认证和重试 transport adapter。主站顶部、图片画板顶部和“我的”统计必须消费同一份 `ProfileMudPointBalance` 快照,泥点总额固定取 `totalPoints`,不得混用 dashboard 的 `walletBalance`;只有充值中心响应缺少 `mudPointBalance` 时,充值弹窗与空账单可以在 owner 匹配后使用同一响应的 legacy `walletBalance` 作为展示兜底,不得写回共享钱包状态。切换或退出账号必须立即清空快照并拒绝旧账号在途响应;消费端在 owner 绑定 effect 生效前也必须按当前用户 ID 同步屏蔽 owner 不匹配的快照,旧账号请求不得阻塞新账号首次读取。普通充值中心 GET 必须在发起时捕获钱包 owner 生命周期与 invalidation 版本;回包只能结算不晚于该版本的刷新,过期快照不得覆盖余额或中止更新的终态刷新。生成、退款、充值、兑换码等余额可能变化事件只通知 Store 合并刷新。external generation 在 worker 领取后才预扣泥点,因此主站必须在任一项目的 active task 轮询期间持续推动钱包合并刷新,并在 `completed / failed` 任一终态再刷新以覆盖成功结算或失败退款;任务列表首次 bootstrap 的 active / terminal 任一读取瞬时失败时必须有界退避重试,成功取得全局 active ID 后再交给常规轮询,不能把首次空状态固化为停止钱包通知。公共泥点资产入口收起态展示“泥点图标 + 泥点总额 | 充值”;桌面端通过 hover / focus 展开,移动端通过点击展开。展开态只展示不限时泥点、每日免费泥点及后端返回的每日重置额度,并提供“使用详情”入口;余额都以后端充值中心 read model 为准,前端不得自行相减推算。
|
||||
2. 账户充值弹窗标题统一为“购买更多泥点”,当前版本只展示泥点商品,不展示会员页签、会员商品、购买会员或升级会员入口。底层会员数据与周期刷新能力继续保留用于存量兼容和结算,会员周期限时泥点不在当前版本前台展示。
|
||||
3. 泥点默认商品固定为四档:`60 泥点 / ¥6`、`180 + 90 泥点 / ¥18`、`300 + 150 泥点 / ¥30`、`680 + 340 泥点 / ¥68`。`60` 档不加赠,后三档首次购买各加赠基础泥点的 `50%`;实际展示、下单校验和支付确认仍以后端返回的充值商品配置为准。
|
||||
4. 首充加赠资格按泥点商品档位独立计算。用户买过 `points_180` 后,只影响 `points_180` 的首充展示和结算,其它未购买档位仍保留各自首充加赠资格。
|
||||
|
||||
@@ -28,6 +28,11 @@ const profileClientMock = vi.hoisted(() => ({
|
||||
getPlatformProfileRechargeCenter: vi.fn(),
|
||||
}));
|
||||
|
||||
const profileCenterMock = vi.hoisted(() => ({
|
||||
isWalletLedgerOpen: false,
|
||||
rechargeCenter: null as { walletBalance: number } | null,
|
||||
}));
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => authUiMock.value,
|
||||
}));
|
||||
@@ -102,11 +107,11 @@ vi.mock('./usePlatformProfileCenterController', () => ({
|
||||
isLoadingRechargeCenter: false,
|
||||
isLoadingWalletLedger: false,
|
||||
isRechargeOpen: false,
|
||||
isWalletLedgerOpen: false,
|
||||
isWalletLedgerOpen: profileCenterMock.isWalletLedgerOpen,
|
||||
loadRechargeCenter: vi.fn(),
|
||||
nativeWechatPayment: null,
|
||||
openWalletLedgerPanel: vi.fn(),
|
||||
rechargeCenter: null,
|
||||
rechargeCenter: profileCenterMock.rechargeCenter,
|
||||
rechargeError: null,
|
||||
rechargePaymentResult: null,
|
||||
setIsRechargeOpen: vi.fn(),
|
||||
@@ -140,6 +145,8 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
usePlatformWalletStore.getState().resetWalletBalance();
|
||||
profileClientMock.getPlatformProfileDashboard.mockReset();
|
||||
profileClientMock.getPlatformProfileRechargeCenter.mockReset();
|
||||
profileCenterMock.isWalletLedgerOpen = false;
|
||||
profileCenterMock.rechargeCenter = null;
|
||||
authUiMock.value.user = null;
|
||||
authUiMock.value.canAccessProtectedData = false;
|
||||
authUiMock.value.openLoginModal.mockReset();
|
||||
@@ -199,6 +206,36 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
expect(screen.queryByText('999')).toBeNull();
|
||||
});
|
||||
|
||||
it('uses the owner-matched legacy recharge balance for an empty ledger', async () => {
|
||||
authUiMock.value.user = {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
displayName: '测试用户',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
};
|
||||
authUiMock.value.canAccessProtectedData = true;
|
||||
profileClientMock.getPlatformProfileDashboard.mockResolvedValue(null);
|
||||
profileClientMock.getPlatformProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 37,
|
||||
});
|
||||
profileCenterMock.isWalletLedgerOpen = true;
|
||||
profileCenterMock.rechargeCenter = { walletBalance: 37 };
|
||||
|
||||
render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="profile"
|
||||
setSelectionStage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('37泥点')).toBeTruthy();
|
||||
expect(screen.getByText('暂无账单记录')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('clears the previous wallet synchronously when the authenticated account changes', async () => {
|
||||
authUiMock.value.user = {
|
||||
id: 'user-1',
|
||||
|
||||
@@ -370,7 +370,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
|
||||
const isAuthenticated = Boolean(authUi?.user);
|
||||
const balance = mudPointBalance?.totalPoints ?? null;
|
||||
const legacyWalletBalance = walletOwnerMatchesCurrentUser
|
||||
? (profileCenter.rechargeCenter?.walletBalance ?? null)
|
||||
: null;
|
||||
const balance = mudPointBalance?.totalPoints ?? legacyWalletBalance;
|
||||
const isCreationStage =
|
||||
!isProfileStage &&
|
||||
(selectionStage === 'platform' || selectionStage === 'creation-home');
|
||||
|
||||
Reference in New Issue
Block a user