This commit is contained in:
2026-08-10 16:42:47 +08:00
parent d1e8eb7316
commit 86c65f9ec8
7 changed files with 697 additions and 641 deletions
+21 -2
View File
@@ -294,17 +294,36 @@ async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)):
async def list_orders( async def list_orders(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100), page_size: int = Query(20, ge=1, le=100),
status_filter: str | None = Query(None, description="按状态筛选: pending/paid/refunded/failed/cancelled"),
start_date: str | None = Query(None, description="创建时间起始,格式 YYYY-MM-DD"),
end_date: str | None = Query(None, description="创建时间结束,格式 YYYY-MM-DD"),
invoice_mode: bool = Query(False, description="开票模式:仅返回已支付订单"),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
from app.services.payment import _check_and_expire_order from app.services.payment import _check_and_expire_order
from datetime import datetime, timezone, timedelta
count_query = select(func.count(PaymentOrder.id)).where(PaymentOrder.user_id == current_user.id) # 构建筛选条件
conditions = [PaymentOrder.user_id == current_user.id]
if status_filter:
conditions.append(PaymentOrder.status == status_filter)
if invoice_mode:
conditions.append(PaymentOrder.status == "paid")
if start_date:
start_dt = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
conditions.append(PaymentOrder.created_at >= start_dt)
if end_date:
end_dt = (datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)).replace(tzinfo=timezone.utc)
conditions.append(PaymentOrder.created_at < end_dt)
# 统计总数
count_query = select(func.count(PaymentOrder.id)).where(*conditions)
total = (await db.execute(count_query)).scalar() or 0 total = (await db.execute(count_query)).scalar() or 0
result = await db.execute( result = await db.execute(
select(PaymentOrder) select(PaymentOrder)
.where(PaymentOrder.user_id == current_user.id) .where(*conditions)
.order_by(PaymentOrder.created_at.desc()) .order_by(PaymentOrder.created_at.desc())
.offset((page - 1) * page_size) .offset((page - 1) * page_size)
.limit(page_size) .limit(page_size)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -27,7 +27,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>民众智创</title> <title>民众智创</title>
<script type="module" crossorigin src="/assets/index-D5aCA_p5.js"></script> <script type="module" crossorigin src="/assets/index-BlvEhxKB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DtYH0-uL.css"> <link rel="stylesheet" crossorigin href="/assets/index-DtYH0-uL.css">
</head> </head>
<body> <body>
+9 -1
View File
@@ -445,10 +445,18 @@ export async function createRechargeOrder(planId: string, method: string = 'wech
return api.post('/payments/recharge', { plan: planId, method }); return api.post('/payments/recharge', { plan: planId, method });
} }
export async function getPaymentOrders(page = 1, pageSize = 20): Promise<{ items: any[]; total: number }> { export async function getPaymentOrders(
page = 1,
pageSize = 20,
options?: { statusFilter?: string; startDate?: string; endDate?: string; invoiceMode?: boolean }
): Promise<{ items: any[]; total: number }> {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('page', String(page)); params.set('page', String(page));
params.set('page_size', String(pageSize)); params.set('page_size', String(pageSize));
if (options?.statusFilter) params.set('status_filter', options.statusFilter);
if (options?.startDate) params.set('start_date', options.startDate);
if (options?.endDate) params.set('end_date', options.endDate);
if (options?.invoiceMode) params.set('invoice_mode', 'true');
return api.get(`/payments/orders?${params.toString()}`); return api.get(`/payments/orders?${params.toString()}`);
} }
+11 -20
View File
@@ -99,36 +99,27 @@ const InvoicePage: React.FC = () => {
} }
}; };
// 加载订单数据(显示所有已支付订单,已开票的标记 _occupied // 加载订单数据(开票模式:后端只返回已支付订单
const loadOrderData = async (page: number = orderPage, occupiedIds?: Set<string>) => { const loadOrderData = async (page: number = orderPage, occupiedIds?: Set<string>) => {
setOrderLoading(true); setOrderLoading(true);
try { try {
const data = await getPaymentOrders(page, orderPageSize); const data = await getPaymentOrders(page, orderPageSize, {
// 只显示已支付订单 invoiceMode: true,
let items = (data.items || []).filter((o: any) => (o.status || o.payment_status) === 'paid'); startDate: orderDateFilter[0] || undefined,
// 日期过滤 endDate: orderDateFilter[1] || undefined,
if (orderDateFilter[0]) { });
items = items.filter((o: any) => { const items = data.items || [];
const created = o.created_at || o.createdAt;
return created && created >= orderDateFilter[0];
});
}
if (orderDateFilter[1]) {
items = items.filter((o: any) => {
const created = o.created_at || o.createdAt;
return created && created <= orderDateFilter[1] + 'T23:59:59';
});
}
// 标记已开票订单 // 标记已开票订单
const occIds = occupiedIds || occupiedOrderIds; const occIds = occupiedIds || occupiedOrderIds;
items = items.map((o: any) => ({ const markedItems = items.map((o: any) => ({
...o, ...o,
_occupied: occIds.has(o.id || o.order_no || o.orderNo), _occupied: occIds.has(o.id || o.order_no || o.orderNo),
})); }));
setOrderData(items); setOrderData(markedItems);
setOrderTotal(items.length); setOrderTotal(data.total || 0);
} catch { } catch {
setOrderData([]); setOrderData([]);
setOrderTotal(0);
} }
setOrderLoading(false); setOrderLoading(false);
}; };
+42 -4
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Table, Tag, Empty, Spin, Typography, Pagination } from 'antd'; import { Table, Tag, Empty, Spin, Typography, Pagination, Select, DatePicker, Space } from 'antd';
import { FileTextOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons'; import { FileTextOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons';
import { getPaymentOrders } from '../api'; import { getPaymentOrders } from '../api';
@@ -9,15 +9,21 @@ const OrderRecordsPage: React.FC = () => {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize] = useState(10); const [pageSize] = useState(10);
const [statusFilter, setStatusFilter] = useState<string>('');
const [dateRange, setDateRange] = useState<any>([null, null]);
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [page]); }, [page, statusFilter]);
const loadData = async () => { const loadData = async () => {
setLoading(true); setLoading(true);
try { try {
const data = await getPaymentOrders(page, pageSize); const data = await getPaymentOrders(page, pageSize, {
statusFilter: statusFilter || undefined,
startDate: dateRange[0]?.format?.('YYYY-MM-DD'),
endDate: dateRange[1]?.format?.('YYYY-MM-DD'),
});
setOrders(data.items || []); setOrders(data.items || []);
setTotal(data.total || 0); setTotal(data.total || 0);
} catch { } catch {
@@ -30,6 +36,12 @@ const OrderRecordsPage: React.FC = () => {
setPage(newPage); setPage(newPage);
}; };
const handleReset = () => {
setStatusFilter('');
setDateRange([null, null]);
setPage(1);
};
const columns = [ const columns = [
{ {
title: '订单号', title: '订单号',
@@ -139,7 +151,33 @@ const OrderRecordsPage: React.FC = () => {
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<FileTextOutlined style={{ color: '#0ea5e9', fontSize: 16 }} /> <FileTextOutlined style={{ color: '#0ea5e9', fontSize: 16 }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text> <Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
<span style={{ color: '#94a3b8', fontSize: 13 }}> {orders.length} </span> <span style={{ color: '#94a3b8', fontSize: 13 }}> {total} </span>
</div>
{/* 搜索栏 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
<Select
placeholder="支付状态"
allowClear
style={{ width: 130 }}
value={statusFilter || undefined}
onChange={(v) => { setStatusFilter(v || ''); setPage(1); }}
options={[
{ value: 'pending', label: '待支付' },
{ value: 'paid', label: '已支付' },
{ value: 'refunded', label: '已退款' },
{ value: 'failed', label: '支付失败' },
{ value: 'cancelled', label: '已取消' },
]}
/>
<DatePicker.RangePicker
value={dateRange}
onChange={(dates) => { setDateRange(dates); setPage(1); }}
allowClear
placeholder={['开始日期', '结束日期']}
/>
{(statusFilter || dateRange[0] || dateRange[1]) && (
<Typography.Link onClick={handleReset}></Typography.Link>
)}
</div> </div>
<div style={{ borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }}> <div style={{ borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }}>
<Spin spinning={loading}> <Spin spinning={loading}>