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(
|
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)
|
||||||
|
|||||||
+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 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>
|
||||||
|
|||||||
@@ -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()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
Reference in New Issue
Block a user