增加后台支付列表,修改前台有问题也能支付成功,增加支付日志

This commit is contained in:
2026-06-10 14:47:06 +08:00
parent 94b84f288d
commit 9974fd5a2e
14 changed files with 688 additions and 10 deletions
+2
View File
@@ -11,6 +11,7 @@ import AdminSettings from './pages/AdminSettings';
import AdminNotificationManager from './pages/AdminNotificationManager';
import AdminCreditRecords from './pages/AdminCreditRecords';
import AdminPaymentConfig from './pages/AdminPaymentConfig';
import AdminPaymentStats from './pages/AdminPaymentStats';
import AdminIndustries from './pages/AdminIndustries';
import AdminVideoEngines from './pages/AdminVideoEngines';
import AdminImageEngines from './pages/AdminImageEngines';
@@ -75,6 +76,7 @@ const App = () => {
<Route path="menu-configs" element={<AdminMenuConfig />} />
<Route path="recharge-packages" element={<AdminRechargePackages />} />
<Route path="payment" element={<AdminPaymentConfig />} />
<Route path="payment-stats" element={<AdminPaymentStats />} />
<Route path="settings" element={<AdminSettings />} />
<Route path="notifications" element={<AdminNotificationManager />} />
<Route path="operation-logs" element={<AdminOperationLogs />} />
+16
View File
@@ -211,6 +211,22 @@ export async function batchUpdatePaymentConfigs(configs: Record<string, string>)
await api.put('/admin/payment-configs/batch', configs);
}
export async function getPaymentStats(): Promise<{
by_status: Record<string, { count: number; amount: number }>;
today: { paid_count: number; paid_amount: number };
recent: any[];
}> {
return api.get('/admin/payment-stats');
}
export async function getAdminPaymentOrders(params?: { method?: string; status?: string }): Promise<{ items: any[] }> {
const qs = new URLSearchParams();
if (params?.method) qs.set('method', params.method);
if (params?.status) qs.set('status', params.status);
const suffix = qs.toString() ? `?${qs.toString()}` : '';
return api.get(`/admin/payment-orders${suffix}`);
}
export async function getAdminNotifications(): Promise<{ total: number; items: any[] }> {
return api.get('/admin/notifications');
}
@@ -11,6 +11,7 @@ const AdminPaymentConfig: React.FC = () => {
const [saving, setSaving] = useState(false);
const [wechatEnabled, setWechatEnabled] = useState(false);
const [alipayEnabled, setAlipayEnabled] = useState(false);
const [mockMode, setMockMode] = useState(false);
const [form] = Form.useForm();
const load = async () => {
@@ -31,6 +32,7 @@ const AdminPaymentConfig: React.FC = () => {
});
setWechatEnabled(map['payment_wechat_enabled'] === 'true');
setAlipayEnabled(map['payment_alipay_enabled'] === 'true');
setMockMode(map['payment_mock'] === 'true');
} catch {
message.error('加载支付配置失败');
}
@@ -43,6 +45,7 @@ const AdminPaymentConfig: React.FC = () => {
const values = await form.validateFields();
setSaving(true);
await batchUpdatePaymentConfigs({
payment_mock: String(mockMode),
payment_wechat_enabled: String(wechatEnabled),
payment_wechat_mch_id: values.wechat_mch_id || '',
payment_wechat_api_key: values.wechat_api_key || '',
@@ -66,6 +69,27 @@ const AdminPaymentConfig: React.FC = () => {
return (
<div style={{ maxWidth: 720 }}>
{/* Mock Mode Toggle */}
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16, background: mockMode ? '#fff7e6' : '#fafbff' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 44, height: 44, borderRadius: 10,
background: mockMode ? 'rgba(245,158,11,0.15)' : 'rgba(99,102,241,0.08)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 22, color: mockMode ? '#f59e0b' : '#6366f1',
}}><DollarOutlined /></div>
<div>
<Typography.Title level={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{mockMode ? '⚠️ 开启后所有充值会直接成功(仅用于测试)' : '关闭 - 使用真实支付渠道'}
</Typography.Text>
</div>
</div>
<Switch checked={mockMode} onChange={setMockMode} checkedChildren="已开启" unCheckedChildren="已关闭" />
</div>
</Card>
{/* WeChat Pay */}
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
@@ -0,0 +1,193 @@
import React, { useEffect, useState } from 'react';
import {
Card, Col, Row, Space, Table, Tag, Typography, Statistic, message,
} from 'antd';
import {
DollarOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined,
} from '@ant-design/icons';
import { getPaymentStats } from '../api';
import { formatDate } from '../utils/formatDate';
const AdminPaymentStats: React.FC = () => {
const [loading, setLoading] = useState(false);
const [stats, setStats] = useState<any>(null);
const load = async () => {
try {
setLoading(true);
const data = await getPaymentStats();
setStats(data);
} catch {
message.error('加载支付统计失败');
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, []);
const statusConfig: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
paid: { color: 'green', label: '已支付', icon: <CheckCircleOutlined /> },
pending: { color: 'gold', label: '待支付', icon: <ClockCircleOutlined /> },
cancelled: { color: 'default', label: '已取消', icon: <CloseCircleOutlined /> },
};
const methodConfig: Record<string, { color: string; label: string }> = {
alipay: { color: 'blue', label: '支付宝' },
wechat: { color: 'green', label: '微信' },
};
const columns = [
{ title: '订单号', dataIndex: 'order_no', key: 'order_no', width: 200 },
{
title: '支付方式', dataIndex: 'payment_method', key: 'payment_method', width: 100,
render: (m: string) => {
const c = methodConfig[m] || { color: 'default', label: m };
return <Tag color={c.color}>{c.label}</Tag>;
},
},
{
title: '金额', dataIndex: 'amount', key: 'amount', width: 100,
render: (a: number) => <Typography.Text strong style={{ color: '#10b981' }}>¥{a.toFixed(2)}</Typography.Text>,
},
{ title: '积分', dataIndex: 'credits', key: 'credits', width: 80 },
{
title: '状态', dataIndex: 'status', key: 'status', width: 100,
render: (s: string) => {
const c = statusConfig[s] || { color: 'default', label: s, icon: null };
return <Tag color={c.color} icon={c.icon}>{c.label}</Tag>;
},
},
{ title: '支付宝交易号', dataIndex: 'trade_no', key: 'trade_no', width: 200, render: (v: string) => v || '-' },
{
title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 160,
render: (d: string) => <span className="date-display" style={{ color: '#94a3b8' }}>{d ? formatDate(d) : '-'}</span>,
},
{
title: '支付时间', dataIndex: 'paid_at', key: 'paid_at', width: 160,
render: (d: string) => <span className="date-display" style={{ color: '#94a3b8' }}>{d ? formatDate(d) : '-'}</span>,
},
];
if (!stats) {
return <div style={{ padding: 24, color: '#94a3b8' }}></div>;
}
const paidInfo = stats.by_status?.paid || { count: 0, amount: 0 };
const pendingInfo = stats.by_status?.pending || { count: 0, amount: 0 };
const cancelledInfo = stats.by_status?.cancelled || { count: 0, amount: 0 };
const totalOrders = paidInfo.count + pendingInfo.count + cancelledInfo.count;
return (
<div>
{/* Summary cards */}
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={12} lg={6}>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<Statistic
title="今日收入"
value={stats.today.paid_amount}
precision={2}
prefix={<DollarOutlined style={{ color: '#10b981' }} />}
suffix="元"
valueStyle={{ color: '#10b981', fontWeight: 700 }}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{stats.today.paid_count}
</Typography.Text>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<Statistic
title="累计已支付"
value={paidInfo.amount}
precision={2}
prefix={<DollarOutlined style={{ color: '#6366f1' }} />}
suffix="元"
valueStyle={{ color: '#6366f1', fontWeight: 700 }}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{paidInfo.count}
</Typography.Text>
</Card>
</Col>
<Col xs={12} sm={12} lg={6}>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<Statistic
title="待支付"
value={pendingInfo.count}
prefix={<ClockCircleOutlined style={{ color: '#f59e0b' }} />}
suffix="笔"
valueStyle={{ color: '#f59e0b', fontWeight: 700 }}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
¥{pendingInfo.amount.toFixed(2)}
</Typography.Text>
</Card>
</Col>
<Col xs={12} sm={12} lg={6}>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<Statistic
title="已取消"
value={cancelledInfo.count}
prefix={<CloseCircleOutlined style={{ color: '#94a3b8' }} />}
suffix="笔"
valueStyle={{ color: '#94a3b8', fontWeight: 700 }}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
¥{cancelledInfo.amount.toFixed(2)}
</Typography.Text>
</Card>
</Col>
</Row>
{/* Status breakdown */}
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}
title={<Space><DollarOutlined /></Space>}>
<Row gutter={16}>
{['paid', 'pending', 'cancelled'].map(s => {
const info = stats.by_status?.[s] || { count: 0, amount: 0 };
const c = statusConfig[s];
const pct = totalOrders > 0 ? ((info.count / totalOrders) * 100).toFixed(1) : '0.0';
return (
<Col span={8} key={s}>
<div style={{
padding: 16, borderRadius: 10,
background: '#fafbff', border: '1px solid #f0f0f5',
}}>
<Space>
<Tag color={c.color} icon={c.icon}>{c.label}</Tag>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>{pct}%</Typography.Text>
</Space>
<div style={{ marginTop: 8, fontSize: 20, fontWeight: 700 }}>
{info.count} <span style={{ fontSize: 13, color: '#94a3b8', fontWeight: 400 }}></span>
</div>
<div style={{ fontSize: 13, color: '#64748b', marginTop: 4 }}>
¥{info.amount.toFixed(2)}
</div>
</div>
</Col>
);
})}
</Row>
</Card>
{/* Recent orders table */}
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}
title={<Space><DollarOutlined /> 50 </Space>}>
<Table
columns={columns}
dataSource={stats.recent}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10, size: 'small' }}
scroll={{ x: 1000 }}
size="middle"
/>
</Card>
</div>
);
};
export default AdminPaymentStats;
+20
View File
@@ -117,6 +117,26 @@ export interface AdminStats {
creditsConsumedToday: number;
}
export interface PaymentStats {
by_status: Record<string, { count: number; amount: number }>;
today: { paid_count: number; paid_amount: number };
recent: PaymentOrder[];
}
export interface PaymentOrder {
id: string;
order_no: string;
user_id: string;
user?: { username: string };
amount: number;
credits: number;
payment_method: string;
status: string;
trade_no?: string;
paid_at?: string;
created_at: string;
}
export interface ModelConfig {
id: string;
name: string;