Revert "extract recharge logic"

This reverts commit 8c899b8a
This commit is contained in:
2026-07-20 10:29:11 +08:00
parent 36ae38c9a6
commit cc4a1f8297
8 changed files with 405 additions and 891 deletions
+161 -21
View File
@@ -35,7 +35,10 @@ import {
import {
PlatformMudPointWalletEntry,
} from '../../../packages/shared/src/components/PlatformMudPointWalletEntry';
import { PlatformProfileRechargeModal } from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
import {
PlatformProfileRechargeModal,
type PlatformProfileRechargeNativePaymentState,
} from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
import { PlatformProfileWalletLedgerModal } from '../../../packages/shared/src/components/PlatformProfileWalletLedgerModal';
import type {
AuthEntryResponse,
@@ -72,6 +75,8 @@ import {
selectGameCreationAppReadyTasks,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import type {
ProfileRechargeCenterResponse,
ProfileRechargeProduct,
ProfileWalletLedgerResponse,
} from '../../../packages/shared/src/contracts/runtime';
import {
@@ -79,8 +84,12 @@ import {
API_RESPONSE_ENVELOPE_VERSION,
unwrapApiResponse,
} from '../../../packages/shared/src/http';
import { useRechargeController } from './hooks/useRechargeController';
import { getClientProfileWalletLedger } from './services/clientApi';
import {
confirmClientWechatProfileRechargeOrder,
createClientProfileRechargeOrder,
getClientProfileRechargeCenter,
getClientProfileWalletLedger,
} from './services/clientApi';
import { useWalletStore } from './stores/useWalletStore';
import HomeView, {
type HomeAgentMode,
@@ -97,6 +106,10 @@ const seedManifest = createGameCreationAppManifest(
);
const defaultProjectPath = '/tmp/genarrative-ai-game-draft';
type RechargeContent = Omit<
ProfileRechargeCenterResponse,
'walletBalance' | 'mudPointBalance'
>;
const RECENT_WORKSPACES_STORAGE_KEY =
'genarrative-ai-game-creator.recent-workspaces.v1';
const AGENT_RUN_HISTORY_MAX_COUNT = 100;
@@ -5364,23 +5377,16 @@ export function WorkspaceLauncher({
const [walletLedgerError, setWalletLedgerError] = useState<string | null>(
null,
);
const {
buy: buyRechargeProduct,
close: closeRecharge,
closeNativePayment: closeNativeRechargePayment,
confirmNativePayment: confirmNativeRechargePayment,
content: rechargeContent,
error: rechargeError,
isLoading: rechargeLoading,
isOpen: rechargeOpen,
load: loadRechargeCenter,
nativePayment: nativeRechargePayment,
open: openRecharge,
submittingProductId: submittingRechargeProductId,
} = useRechargeController({
applyWalletBalanceSnapshot,
onWalletBalanceMayHaveChanged,
});
const [rechargeOpen, setRechargeOpen] = useState(false);
const [rechargeContent, setRechargeContent] =
useState<RechargeContent | null>(null);
const [rechargeLoading, setRechargeLoading] = useState(false);
const [rechargeError, setRechargeError] = useState<string | null>(null);
const [submittingRechargeProductId, setSubmittingRechargeProductId] =
useState<string | null>(null);
const [nativeRechargePayment, setNativeRechargePayment] =
useState<PlatformProfileRechargeNativePaymentState | null>(null);
const rechargeLifecycleRef = useRef(0);
const rechargeModalCenter =
rechargeContent && mudPointBalance
? {
@@ -6207,6 +6213,140 @@ export function WorkspaceLauncher({
void loadWalletLedger();
}
function applyRechargeContent(center: ProfileRechargeCenterResponse) {
const { walletBalance, mudPointBalance, ...content } = center;
void walletBalance;
if (mudPointBalance) {
applyWalletBalanceSnapshot(mudPointBalance);
}
setRechargeContent(content);
}
async function loadRechargeCenter() {
const rechargeLifecycle = rechargeLifecycleRef.current;
setRechargeLoading(true);
setRechargeError(null);
try {
const center = await getClientProfileRechargeCenter();
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
return;
}
applyRechargeContent(center);
} catch (error) {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
setRechargeError(
error instanceof Error ? error.message : '读取泥点购买信息失败',
);
}
} finally {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
setRechargeLoading(false);
}
}
}
function openRecharge() {
rechargeLifecycleRef.current += 1;
setRechargeOpen(true);
setRechargeError(null);
void loadRechargeCenter();
}
function closeRecharge() {
rechargeLifecycleRef.current += 1;
setRechargeOpen(false);
setNativeRechargePayment(null);
setSubmittingRechargeProductId(null);
}
async function buyRechargeProduct(product: ProfileRechargeProduct) {
if (submittingRechargeProductId) {
return;
}
const rechargeLifecycle = rechargeLifecycleRef.current;
setSubmittingRechargeProductId(product.productId);
setRechargeError(null);
try {
const response = await createClientProfileRechargeOrder(
product.productId,
);
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
return;
}
applyRechargeContent(response.center);
const nativePayment = response.wechatNativePayment;
const codeUrl = nativePayment?.codeUrl?.trim();
const expiresAt = nativePayment?.expiresAt?.trim();
if (!nativePayment || !codeUrl || !expiresAt) {
throw new Error('微信 Native 支付链接生成失败');
}
setNativeRechargePayment({
orderId: response.order.orderId,
productTitle: response.order.productTitle,
amountCents: response.order.amountCents,
codeUrl,
expiresAt,
isConfirming: false,
});
} catch (error) {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
setRechargeError(error instanceof Error ? error.message : '充值失败');
}
} finally {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
setSubmittingRechargeProductId(null);
}
}
}
async function confirmNativeRechargePayment() {
if (!nativeRechargePayment || nativeRechargePayment.isConfirming) {
return;
}
const rechargeLifecycle = rechargeLifecycleRef.current;
const orderId = nativeRechargePayment.orderId;
setNativeRechargePayment((current) =>
current?.orderId === orderId
? { ...current, isConfirming: true, confirmMessage: undefined }
: current,
);
try {
const response = await confirmClientWechatProfileRechargeOrder(orderId);
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
return;
}
applyRechargeContent(response.center);
if (response.order.status === 'paid') {
setNativeRechargePayment(null);
void onWalletBalanceMayHaveChanged();
return;
}
const confirmMessage =
response.order.status === 'pending' ||
(response.order.status === 'expired' &&
!response.order.expirationCheckedAt)
? '暂未确认到账,请确认付款完成后再点一次。'
: '订单未支付成功,请关闭二维码后重新下单。';
setNativeRechargePayment((current) =>
current?.orderId === orderId
? { ...current, isConfirming: false, confirmMessage }
: current,
);
} catch {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
setNativeRechargePayment((current) =>
current?.orderId === orderId
? {
...current,
isConfirming: false,
confirmMessage: '暂时没能确认到账状态,请稍后再试。',
}
: current,
);
}
}
}
function showLauncherNotice(title: string) {
setLauncherNotice({
title,
@@ -9306,7 +9446,7 @@ export function WorkspaceLauncher({
onRetry={() => void loadRechargeCenter()}
onBuy={(product) => void buyRechargeProduct(product)}
onConfirmNativePayment={() => void confirmNativeRechargePayment()}
onCloseNativePayment={closeNativeRechargePayment}
onCloseNativePayment={() => setNativeRechargePayment(null)}
/>
) : null}
{walletLedgerOpen ? (
@@ -1,155 +0,0 @@
import { useCallback, useRef, useState } from 'react';
import type {
ProfileMudPointBalance,
ProfileRechargeCenterResponse,
ProfileRechargeProduct,
} from '../../../../packages/shared/src';
import { useWechatNativeRechargeController } from '../../../../packages/shared/src/components/PlatformProfileRechargeModal/useWechatNativeRechargeController';
import {
confirmClientWechatProfileRechargeOrder,
createClientProfileRechargeOrder,
getClientProfileRechargeCenter,
watchClientWechatProfileRechargeOrder,
} from '../services/clientApi';
export type RechargeContent = Omit<
ProfileRechargeCenterResponse,
'walletBalance' | 'mudPointBalance'
>;
type UseRechargeControllerArgs = {
applyWalletBalanceSnapshot: (balance: ProfileMudPointBalance) => void;
onWalletBalanceMayHaveChanged: () => Promise<void>;
};
export function useRechargeController({
applyWalletBalanceSnapshot,
onWalletBalanceMayHaveChanged,
}: UseRechargeControllerArgs) {
const [isOpen, setIsOpen] = useState(false);
const [content, setContent] = useState<RechargeContent | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [submittingProductId, setSubmittingProductId] = useState<string | null>(
null,
);
const lifecycleRef = useRef(0);
const applyCenter = useCallback(
(center: ProfileRechargeCenterResponse) => {
const { walletBalance, mudPointBalance, ...nextContent } = center;
void walletBalance;
setContent(nextContent);
if (mudPointBalance) {
applyWalletBalanceSnapshot(mudPointBalance);
}
},
[applyWalletBalanceSnapshot],
);
const {
beginNativePayment,
closeNativePayment,
confirmNativePayment,
nativePayment,
paymentResult,
resetNativePayment,
setPaymentResult,
} = useWechatNativeRechargeController({
confirmOrder: confirmClientWechatProfileRechargeOrder,
watchOrder: watchClientWechatProfileRechargeOrder,
onCenterReceived: applyCenter,
onPaid: onWalletBalanceMayHaveChanged,
});
const load = useCallback(async () => {
const lifecycle = lifecycleRef.current;
setIsLoading(true);
setError(null);
try {
const center = await getClientProfileRechargeCenter();
if (lifecycleRef.current === lifecycle) {
applyCenter(center);
}
} catch (loadError) {
if (lifecycleRef.current === lifecycle) {
setError(
loadError instanceof Error
? loadError.message
: '读取泥点购买信息失败',
);
}
} finally {
if (lifecycleRef.current === lifecycle) {
setIsLoading(false);
}
}
}, [applyCenter]);
const open = useCallback(() => {
lifecycleRef.current += 1;
resetNativePayment();
setIsOpen(true);
setError(null);
void load();
}, [load, resetNativePayment]);
const close = useCallback(() => {
lifecycleRef.current += 1;
setIsOpen(false);
setSubmittingProductId(null);
resetNativePayment();
}, [resetNativePayment]);
const buy = useCallback(
async (product: ProfileRechargeProduct) => {
if (submittingProductId) {
return;
}
const lifecycle = lifecycleRef.current;
setSubmittingProductId(product.productId);
setError(null);
setPaymentResult(null);
try {
const response = await createClientProfileRechargeOrder(
product.productId,
);
if (lifecycleRef.current !== lifecycle) {
return;
}
applyCenter(response.center);
beginNativePayment(response);
} catch (buyError) {
if (lifecycleRef.current === lifecycle) {
setError(buyError instanceof Error ? buyError.message : '充值失败');
}
} finally {
if (lifecycleRef.current === lifecycle) {
setSubmittingProductId(null);
}
}
},
[applyCenter, beginNativePayment, setPaymentResult, submittingProductId],
);
const paymentResultError =
paymentResult && paymentResult.kind !== 'success'
? paymentResult.message
: null;
return {
buy,
close,
closeNativePayment,
confirmNativePayment,
content,
error: error ?? paymentResultError,
isLoading,
isOpen,
load,
nativePayment,
open,
submittingProductId,
};
}
@@ -97,22 +97,6 @@ export async function requestClientApi<T>(
init: RequestInit,
fallbackMessage: string,
options: { skipAuth?: boolean } = {},
) {
const response = await requestClientApiResponse(
url,
init,
fallbackMessage,
options,
);
const text = await response.text();
return text ? unwrapApiResponse<T>(JSON.parse(text) as T) : (null as T);
}
async function requestClientApiResponse(
url: string,
init: RequestInit,
fallbackMessage: string,
options: { skipAuth?: boolean } = {},
) {
const headers = new Headers(init.headers);
headers.set(API_RESPONSE_ENVELOPE_HEADER, API_RESPONSE_ENVELOPE_VERSION);
@@ -141,7 +125,8 @@ async function requestClientApiResponse(
{ status: response.status },
);
}
return response;
const text = await response.text();
return text ? unwrapApiResponse<T>(JSON.parse(text) as T) : (null as T);
}
export function getClientProfileDashboard() {
@@ -183,51 +168,6 @@ export function confirmClientWechatProfileRechargeOrder(orderId: string) {
);
}
export async function watchClientWechatProfileRechargeOrder(
orderId: string,
options: { signal: AbortSignal },
) {
const response = await requestClientApiResponse(
`/api/profile/recharge/orders/${encodeURIComponent(orderId)}/wechat/events`,
{
method: 'GET',
headers: { Accept: 'text/event-stream' },
signal: options.signal,
},
'订阅充值订单状态失败',
);
const blocks = (await response.text()).split(/\r?\n\r?\n/u);
let latest: ConfirmWechatProfileRechargeOrderResponse | null = null;
for (const block of blocks) {
let eventName = 'message';
const dataLines: string[] = [];
for (const line of block.split(/\r?\n/u)) {
if (line.startsWith('event:')) {
eventName = line.slice('event:'.length).trim();
} else if (line.startsWith('data:')) {
dataLines.push(line.slice('data:'.length).trimStart());
}
}
if (dataLines.length === 0) {
continue;
}
const parsed = JSON.parse(dataLines.join('\n')) as Record<string, unknown>;
if (eventName === 'order' && parsed.order && parsed.center) {
latest = parsed as ConfirmWechatProfileRechargeOrderResponse;
} else if (eventName === 'error') {
throw new Error(
typeof parsed.message === 'string' && parsed.message.trim()
? parsed.message.trim()
: '订阅充值订单状态失败',
);
}
}
if (!latest) {
throw new Error('充值订单状态流返回不完整');
}
return latest;
}
export function getClientProfileWalletLedger() {
return requestClientApi<ProfileWalletLedgerResponse>(
'/api/profile/wallet-ledger',
@@ -1,77 +0,0 @@
/* @vitest-environment jsdom */
import { afterEach, expect, it, vi } from 'vitest';
import { watchClientWechatProfileRechargeOrder } from '../src/services/clientApi';
const storedValues = new Map<string, string>();
Object.defineProperty(window, 'localStorage', {
configurable: true,
value: {
clear: () => storedValues.clear(),
getItem: (key: string) => storedValues.get(key) ?? null,
removeItem: (key: string) => storedValues.delete(key),
setItem: (key: string, value: string) => storedValues.set(key, value),
},
});
afterEach(() => {
vi.restoreAllMocks();
window.localStorage.clear();
});
it('reads the latest authoritative order snapshot from the recharge SSE stream', async () => {
window.localStorage.setItem(
'genarrative.auth.access-token.v1',
'access-token',
);
const order = {
orderId: 'order-native-1',
productId: 'points_60',
productTitle: '60泥点',
kind: 'points',
amountCents: 600,
status: 'paid',
paymentChannel: 'wechat_native',
paidAt: '2026-07-17T00:01:00Z',
providerTransactionId: 'transaction-1',
createdAt: '2026-07-17T00:00:00Z',
pointsDelta: 60,
membershipExpiresAt: null,
};
const center = { walletBalance: 180 };
const fetchSpy = vi
.spyOn(globalThis, 'fetch')
.mockResolvedValue(
new Response(
[
'event: order',
`data: ${JSON.stringify({ order, center })}`,
'',
'event: done',
`data: ${JSON.stringify({ orderId: order.orderId, status: 'paid' })}`,
'',
].join('\n'),
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
),
);
const abortController = new AbortController();
await expect(
watchClientWechatProfileRechargeOrder(order.orderId, {
signal: abortController.signal,
}),
).resolves.toEqual({ order, center });
expect(fetchSpy).toHaveBeenCalledWith(
`/api/profile/recharge/orders/${order.orderId}/wechat/events`,
expect.objectContaining({
method: 'GET',
signal: abortController.signal,
headers: expect.any(Headers),
}),
);
const request = fetchSpy.mock.calls[0]?.[1];
expect(new Headers(request?.headers).get('Authorization')).toBe(
'Bearer access-token',
);
});