1
This commit is contained in:
@@ -294,17 +294,36 @@ async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
async def list_orders(
|
||||
page: int = Query(1, ge=1),
|
||||
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),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
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
|
||||
|
||||
result = await db.execute(
|
||||
select(PaymentOrder)
|
||||
.where(PaymentOrder.user_id == current_user.id)
|
||||
.where(*conditions)
|
||||
.order_by(PaymentOrder.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
|
||||
+613
File diff suppressed because one or more lines are too long
-613
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -27,7 +27,7 @@
|
||||
<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" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -445,10 +445,18 @@ export async function createRechargeOrder(planId: string, method: string = 'wech
|
||||
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();
|
||||
params.set('page', String(page));
|
||||
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()}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -99,36 +99,27 @@ const InvoicePage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 加载订单数据(显示所有已支付订单,已开票的标记 _occupied)
|
||||
// 加载订单数据(开票模式:后端只返回已支付订单)
|
||||
const loadOrderData = async (page: number = orderPage, occupiedIds?: Set<string>) => {
|
||||
setOrderLoading(true);
|
||||
try {
|
||||
const data = await getPaymentOrders(page, orderPageSize);
|
||||
// 只显示已支付订单
|
||||
let items = (data.items || []).filter((o: any) => (o.status || o.payment_status) === 'paid');
|
||||
// 日期过滤
|
||||
if (orderDateFilter[0]) {
|
||||
items = items.filter((o: any) => {
|
||||
const created = o.created_at || o.createdAt;
|
||||
return created && created >= orderDateFilter[0];
|
||||
const data = await getPaymentOrders(page, orderPageSize, {
|
||||
invoiceMode: true,
|
||||
startDate: orderDateFilter[0] || undefined,
|
||||
endDate: orderDateFilter[1] || undefined,
|
||||
});
|
||||
}
|
||||
if (orderDateFilter[1]) {
|
||||
items = items.filter((o: any) => {
|
||||
const created = o.created_at || o.createdAt;
|
||||
return created && created <= orderDateFilter[1] + 'T23:59:59';
|
||||
});
|
||||
}
|
||||
const items = data.items || [];
|
||||
// 标记已开票订单
|
||||
const occIds = occupiedIds || occupiedOrderIds;
|
||||
items = items.map((o: any) => ({
|
||||
const markedItems = items.map((o: any) => ({
|
||||
...o,
|
||||
_occupied: occIds.has(o.id || o.order_no || o.orderNo),
|
||||
}));
|
||||
setOrderData(items);
|
||||
setOrderTotal(items.length);
|
||||
setOrderData(markedItems);
|
||||
setOrderTotal(data.total || 0);
|
||||
} catch {
|
||||
setOrderData([]);
|
||||
setOrderTotal(0);
|
||||
}
|
||||
setOrderLoading(false);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { getPaymentOrders } from '../api';
|
||||
|
||||
@@ -9,15 +9,21 @@ const OrderRecordsPage: React.FC = () => {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(10);
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [dateRange, setDateRange] = useState<any>([null, null]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [page]);
|
||||
}, [page, statusFilter]);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
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 || []);
|
||||
setTotal(data.total || 0);
|
||||
} catch {
|
||||
@@ -30,6 +36,12 @@ const OrderRecordsPage: React.FC = () => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setStatusFilter('');
|
||||
setDateRange([null, null]);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '订单号',
|
||||
@@ -139,7 +151,33 @@ const OrderRecordsPage: React.FC = () => {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<FileTextOutlined style={{ color: '#0ea5e9', fontSize: 16 }} />
|
||||
<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 style={{ borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }}>
|
||||
<Spin spinning={loading}>
|
||||
|
||||
Reference in New Issue
Block a user