合并最新 master 到作品管理分支
Project CI / Backend tests (pull_request) Failing after 39s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m44s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 3m43s
Project CI / Repository checks (pull_request) Failing after 40s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m52s
Project CI / Frontend tests (pull_request) Successful in 3m17s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m12s
Project CI / Native shell tests (pull_request) Successful in 6m41s
Project CI / Backend tests (pull_request) Failing after 39s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m44s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 3m43s
Project CI / Repository checks (pull_request) Failing after 40s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m52s
Project CI / Frontend tests (pull_request) Successful in 3m17s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m12s
Project CI / Native shell tests (pull_request) Successful in 6m41s
This commit is contained in:
@@ -52,6 +52,7 @@ import type {
|
||||
AdminLoginResponse,
|
||||
AdminMeResponse,
|
||||
AdminOverviewResponse,
|
||||
AdminPaymentOrderListResponse,
|
||||
AdminProjectSnapshotChannelsResponse,
|
||||
AdminProjectSnapshotListQuery,
|
||||
AdminProjectSnapshotListResponse,
|
||||
@@ -804,6 +805,12 @@ export function listAdminRechargeOrders(
|
||||
);
|
||||
}
|
||||
|
||||
export function listAdminPaymentOrders(token: string) {
|
||||
return request<AdminPaymentOrderListResponse>('/admin/api/payment/orders', {
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
export function getAdminUserDetail(token: string, query: AdminUserDetailQuery) {
|
||||
return request<AdminUserDetailResponse>(
|
||||
`/admin/api/profile/users/detail${buildAdminUserDetailQuery(query)}`,
|
||||
|
||||
@@ -1054,6 +1054,39 @@ export interface AdminWechatPaymentCheckPayload {
|
||||
knownRefundsRefreshed: number;
|
||||
}
|
||||
|
||||
export interface AdminPaymentOrderEntry {
|
||||
orderId: string;
|
||||
appId: string;
|
||||
ownerUserId: string;
|
||||
merchantOrderId: string;
|
||||
title: string;
|
||||
amountCents: number;
|
||||
currency: string;
|
||||
provider: string;
|
||||
providerTradeNo: string | null;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
paidAt: string | null;
|
||||
}
|
||||
|
||||
export interface AdminPaymentOrderListResponse {
|
||||
entries: AdminPaymentOrderEntry[];
|
||||
webhooks: AdminPaymentWebhookEntry[];
|
||||
}
|
||||
|
||||
export interface AdminPaymentWebhookEntry {
|
||||
deliveryId: string;
|
||||
orderId: string;
|
||||
appId: string;
|
||||
callbackUrl: string;
|
||||
status: string;
|
||||
attemptCount: number;
|
||||
availableAt: string;
|
||||
lastErrorMessage: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AdminRechargeRefundPreviewResponse {
|
||||
order: AdminRechargeOrderEntryPayload;
|
||||
paymentCheck: AdminWechatPaymentCheckPayload;
|
||||
|
||||
@@ -36,6 +36,7 @@ import { AdminGrayReleaseConfigPage } from '../pages/AdminGrayReleaseConfigPage'
|
||||
import { AdminInviteCodePage } from '../pages/AdminInviteCodePage';
|
||||
import { AdminLoginPage } from '../pages/AdminLoginPage';
|
||||
import { AdminOverviewPage } from '../pages/AdminOverviewPage';
|
||||
import { AdminPaymentOrderPage } from '../pages/AdminPaymentOrderPage';
|
||||
import { AdminProfileWalletConfigPage } from '../pages/AdminProfileWalletConfigPage';
|
||||
import { AdminProjectSnapshotsPage } from '../pages/AdminProjectSnapshotsPage';
|
||||
import { AdminRechargeOrderPage } from '../pages/AdminRechargeOrderPage';
|
||||
@@ -297,6 +298,12 @@ export function AdminApp() {
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'payment-orders' ? (
|
||||
<AdminPaymentOrderPage
|
||||
token={token}
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'editor-generation-pricing' ? (
|
||||
<AdminEditorGenerationPricingPage
|
||||
token={token}
|
||||
|
||||
@@ -51,6 +51,7 @@ const routeIcons = {
|
||||
tasks: ListChecks,
|
||||
'recharge-products': BadgeDollarSign,
|
||||
'recharge-orders': ReceiptText,
|
||||
'payment-orders': ReceiptText,
|
||||
'editor-generation-pricing': Coins,
|
||||
'editor-showcase': Star,
|
||||
'game-distribution': Gamepad2,
|
||||
|
||||
@@ -14,6 +14,7 @@ export type AdminRouteId =
|
||||
| 'tasks'
|
||||
| 'recharge-products'
|
||||
| 'recharge-orders'
|
||||
| 'payment-orders'
|
||||
| 'editor-generation-pricing'
|
||||
| 'editor-showcase'
|
||||
| 'game-distribution'
|
||||
@@ -53,6 +54,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
|
||||
{ id: 'tasks', label: '任务配置', hash: '#tasks' },
|
||||
{ id: 'recharge-products', label: '充值商品', hash: '#recharge-products' },
|
||||
{ id: 'recharge-orders', label: '充值管理', hash: '#recharge-orders' },
|
||||
{ id: 'payment-orders', label: '支付订单', hash: '#payment-orders' },
|
||||
{
|
||||
id: 'editor-generation-pricing',
|
||||
label: '模型定价',
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
AdminEmptyState,
|
||||
AdminListPanel,
|
||||
AdminPage,
|
||||
AdminPageHeading,
|
||||
AdminStatusPill,
|
||||
} from '@genarrative/shared/components';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
formatAdminApiError,
|
||||
listAdminPaymentOrders,
|
||||
} from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminPaymentOrderEntry,
|
||||
AdminPaymentWebhookEntry,
|
||||
} from '../api/adminApiTypes';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
type AdminPaymentOrderPageProps = {
|
||||
token: string;
|
||||
onUnauthorized: (message?: string) => void;
|
||||
};
|
||||
|
||||
function statusTone(status: string): 'ok' | 'pending' | 'error' {
|
||||
if (status === 'paid') return 'ok';
|
||||
if (status === 'closed' || status === 'expired') return 'error';
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
export function AdminPaymentOrderPage({
|
||||
token,
|
||||
onUnauthorized,
|
||||
}: AdminPaymentOrderPageProps) {
|
||||
const [entries, setEntries] = useState<AdminPaymentOrderEntry[]>([]);
|
||||
const [webhooks, setWebhooks] = useState<AdminPaymentWebhookEntry[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const load = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
void listAdminPaymentOrders(token)
|
||||
.then((response) => {
|
||||
setEntries(response.entries ?? []);
|
||||
setWebhooks(response.webhooks ?? []);
|
||||
})
|
||||
.catch((nextError: unknown) => {
|
||||
handlePageError(nextError, onUnauthorized, (message) =>
|
||||
setError(message || formatAdminApiError(nextError)),
|
||||
);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [onUnauthorized, token]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<AdminPage wide>
|
||||
<AdminPageHeading
|
||||
title="支付订单"
|
||||
description="平台统一收款的外部产品订单与到账状态。"
|
||||
actions={
|
||||
<button
|
||||
className="admin-button admin-button--secondary"
|
||||
type="button"
|
||||
onClick={load}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<AdminListPanel
|
||||
title="外部回调投递"
|
||||
count={`${webhooks.length} 条`}
|
||||
busy={isLoading}
|
||||
emptyText={<AdminEmptyState>暂无外部回调记录。</AdminEmptyState>}
|
||||
columns={[
|
||||
{
|
||||
key: 'delivery',
|
||||
label: '投递',
|
||||
render: (entry) => (
|
||||
<>
|
||||
<strong>{entry.deliveryId}</strong>
|
||||
<small className="admin-muted-block">{entry.orderId}</small>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ key: 'app', label: '应用', render: (entry) => entry.appId },
|
||||
{
|
||||
key: 'callback',
|
||||
label: '回调地址',
|
||||
render: (entry) => entry.callbackUrl,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
render: (entry) => (
|
||||
<AdminStatusPill
|
||||
tone={
|
||||
entry.status === 'delivered'
|
||||
? 'ok'
|
||||
: entry.status === 'failed'
|
||||
? 'error'
|
||||
: 'pending'
|
||||
}
|
||||
>
|
||||
{entry.status}
|
||||
</AdminStatusPill>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'attempts',
|
||||
label: '尝试次数',
|
||||
render: (entry) => entry.attemptCount,
|
||||
},
|
||||
{
|
||||
key: 'error',
|
||||
label: '最近错误',
|
||||
render: (entry) => entry.lastErrorMessage ?? '-',
|
||||
},
|
||||
{
|
||||
key: 'updated',
|
||||
label: '更新时间',
|
||||
render: (entry) => entry.updatedAt,
|
||||
},
|
||||
]}
|
||||
rows={webhooks}
|
||||
rowKey={(entry) => entry.deliveryId}
|
||||
/>
|
||||
{error ? (
|
||||
<div className="admin-alert admin-alert--error">{error}</div>
|
||||
) : null}
|
||||
<AdminListPanel
|
||||
title="订单记录"
|
||||
count={`${entries.length} 条`}
|
||||
busy={isLoading}
|
||||
emptyText={
|
||||
<AdminEmptyState>
|
||||
暂无支付订单,外部产品创建订单后会显示在这里。
|
||||
</AdminEmptyState>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
key: 'order',
|
||||
label: '订单',
|
||||
render: (entry) => (
|
||||
<>
|
||||
<strong>{entry.merchantOrderId}</strong>
|
||||
<small className="admin-muted-block">{entry.orderId}</small>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'app',
|
||||
label: '应用',
|
||||
render: (entry) => (
|
||||
<>
|
||||
<strong>{entry.title}</strong>
|
||||
<small className="admin-muted-block">{entry.appId}</small>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
label: '金额',
|
||||
render: (entry) =>
|
||||
`${entry.currency} ${(entry.amountCents / 100).toFixed(2)}`,
|
||||
},
|
||||
{ key: 'provider', label: '渠道', render: (entry) => entry.provider },
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
render: (entry) => (
|
||||
<AdminStatusPill tone={statusTone(entry.status)}>
|
||||
{entry.status}
|
||||
</AdminStatusPill>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'created',
|
||||
label: '创建时间',
|
||||
render: (entry) => entry.createdAt,
|
||||
},
|
||||
{
|
||||
key: 'paid',
|
||||
label: '支付时间',
|
||||
render: (entry) => entry.paidAt ?? '-',
|
||||
},
|
||||
]}
|
||||
rows={entries}
|
||||
rowKey={(entry) => entry.orderId}
|
||||
/>
|
||||
</AdminPage>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user