合并 origin/master(52c83cc2a,248 个提交)到 feat/game-purchase
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m28s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m26s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m56s
Project CI / Backend tests (pull_request) Successful in 9m4s
Project CI / Repository checks (pull_request) Successful in 7m13s
Project CI / Native shell tests (pull_request) Successful in 10m16s
Project CI / Frontend tests (pull_request) Successful in 2m26s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m28s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m26s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m56s
Project CI / Backend tests (pull_request) Successful in 9m4s
Project CI / Repository checks (pull_request) Successful in 7m13s
Project CI / Native shell tests (pull_request) Successful in 10m16s
Project CI / Frontend tests (pull_request) Successful in 2m26s
- 同步 master 侧 248 个提交:会员与泥点计费(PR #588 / #603)、创作者主页与关注粉丝(PR #638)、运行页刷新真重载(PR #640)、AGC 模板 templateVersion 递增(PR #643)等;确认 master 未触碰发行网关与 `api-server/src/modules/game_distribution.rs`。 - 冲突 1(server-rs/crates/module-runtime/src/domain.rs:1157-1162)`RuntimeProfileWalletLedgerSourceType` 两侧各自在末尾追加变体:保留 master 的 `MembershipUpgradeGrant` 为索引 16(线上已落库口径),本分支 `GamePurchase` 顺延为索引 17,并保留其「每账号每游戏一次扣费」注释。 - 冲突 2(server-rs/crates/module-runtime/src/domain.rs:1184-1188)`as_str()` 同时保留 `membership_upgrade_grant` 与 `game_purchase` 两个分支。 - 冲突 3(server-rs/crates/spacetime-client/src/active/mapper/runtime_profile.rs:95-101)bindings→领域枚举映射同时保留 `MembershipUpgradeGrant` 与 `GamePurchase` 两支,按新索引顺序排列。 - 冲突 4(server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs:43-47)生成枚举按模块源码顺序合并为 …`MembershipUpgradeGrant`、`GamePurchase`;随后 `npm run spacetime:generate` 复核该文件与生成结果逐字节一致。 - 冲突 5(server-rs/crates/api-server/src/admin.rs:4849-4853)sats 索引映射改为 `16 => membership_upgrade_grant` 与 `17 => game_purchase` 两条并存。 - 冲突 6(server-rs/crates/api-server/src/admin.rs:6368-6461)索引断言并入 master 的 `[16] => membership_upgrade_grant`,并把 master 的「17 尚未映射」占位断言替换为 `[17] => game_purchase`;本分支的后台消耗统计两个用例原样保留。 - 冲突 7(server-rs/crates/api-server/src/runtime_profile.rs:219-226)来源文案格式化同时保留 `MembershipUpgradeGrant` 与 `GamePurchase` 两支,注释随 `GamePurchase` 保留。 - 冲突 8(docs/project-memory/shared-memory/decision-log.md:3-24)两条决策记录并列保留:本分支 2026-10-05「播放会话前缀在三处入口清空 Cookie」在前,master 2026-10-03「每日免费发放额为 0 时前端隐藏该池」在后。 - 保住 master 侧:发行响应 ETag/304/gzip(`release_asset_response_with_cache` 五参形态与 `release_asset_etag` / `if_none_match_matches` / `accepts_gzip_encoding` / `gzip_release_asset` / `release_asset_not_modified_response`)、共享加载面 `PlatformGameLoadingSurface` 及其使用方、三池钱包与会员档位目录改造。 - 保住本分支侧:付费作品 404 守卫(`price_mud_points > 0` 且位于 `load_package` 之前)、购买与播放会话网关路由、nginx×3 / Pingora / Vite 的播放会话 Cookie 清空与 `nginx-route-parity.matrix.json` 登记、viewer 感知公开详情、后台审核价格、`PlatformGamePricingField` 共享定价组件与两端接入、AGC 项目版本只读 + 平台派发下一版本号、schema 与迁移白名单、钱包来源 `game_purchase`。 - 验证:`git merge-base --is-ancestor origin/master HEAD` 返回 0;`check:encoding`、`check:doc-index`、`check:spacetime-schema`、`check:server-rs-ddd`、`check:rustfmt`、`check:npm-workspaces`、`check:game-distribution-dto-parity`、`check:game-distribution-price-limit-parity`、`check:pingora-route-parity`、`check:nginx-spa-routes`、根/admin-web/AGC typecheck、`git diff --check` 全绿。 - 测试:`cargo test -p module-game-distribution` 30 passed,`cargo test -p api-server game_distribution` 61 passed,`cargo test -p spacetime-module game_distribution` 7 passed;定向 vitest 28 个文件 252 个用例全通过。 - `npm run spacetime:generate` 除 11 个 procedure 文件的纯空白格式差异外无任何语义改动(`git diff -w` 为空),已保留 master 的生成产物形态,提交内无 schema/绑定语义变化。
This commit is contained in:
@@ -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(() => ({
|
||||
@@ -262,12 +268,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',
|
||||
@@ -281,7 +285,7 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByLabelText('泥点 207')).toBeTruthy();
|
||||
expect(await screen.findByLabelText(/可用总额 207/)).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '下载客户端' })).toBeTruthy();
|
||||
|
||||
rerender(
|
||||
@@ -311,7 +315,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;
|
||||
|
||||
@@ -355,10 +366,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',
|
||||
@@ -370,7 +380,7 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
setSelectionStage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await screen.findByLabelText('泥点 66');
|
||||
await screen.findByLabelText(/可用总额 66/);
|
||||
|
||||
let resolveNextOwner!: (value: unknown) => void;
|
||||
profileClientMock.getPlatformProfileRechargeCenter.mockImplementation(
|
||||
@@ -387,7 +397,7 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('泥点 66')).toBeNull();
|
||||
expect(screen.queryByLabelText(/可用总额 66/)).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(usePlatformWalletStore.getState()).toMatchObject({
|
||||
ownerUserId: 'user-2',
|
||||
@@ -396,16 +406,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 () => {
|
||||
@@ -423,10 +432,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',
|
||||
@@ -438,7 +446,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,
|
||||
@@ -404,7 +405,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
});
|
||||
const legacyWalletBalance = walletOwnerMatchesCurrentUser
|
||||
? (storedLegacyWalletBalance ??
|
||||
profileCenter.rechargeCenter?.walletBalance ??
|
||||
resolveProfileMudPointDisplayTotal(
|
||||
profileCenter.rechargeCenter?.mudPointBalance,
|
||||
) ??
|
||||
null)
|
||||
: null;
|
||||
|
||||
@@ -1006,9 +1009,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: '资产操作消耗',
|
||||
|
||||
+11
-10
@@ -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 = {},
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user