修改订单查询接口逻辑、前台页面增加刷新页面订单消失问题,调整后台支付账单页面数据问题

This commit is contained in:
2026-06-11 13:51:46 +08:00
parent cace26018a
commit 1b7b794164
4 changed files with 134 additions and 15 deletions
+24 -6
View File
@@ -448,6 +448,13 @@ async def get_payment_stats(
"""Return payment statistics for admin dashboard."""
from sqlalchemy import func
# Ensure by_status has all expected statuses with defaults
by_status = {
"pending": {"count": 0, "amount": 0.0},
"paid": {"count": 0, "amount": 0.0},
"cancelled": {"count": 0, "amount": 0.0},
}
# Status breakdown
status_result = await db.execute(
select(
@@ -456,12 +463,22 @@ async def get_payment_stats(
func.coalesce(func.sum(PaymentOrder.amount), 0).label("amount"),
).group_by(PaymentOrder.status)
)
by_status = {}
for row in status_result.all():
by_status[row.status] = {"count": row.count, "amount": round(float(row.amount), 2)}
if row.status in by_status:
by_status[row.status] = {
"count": row.count,
"amount": round(float(row.amount), 2)
}
else:
# Map any unexpected status to cancelled
by_status["cancelled"]["count"] += row.count
by_status["cancelled"]["amount"] += round(float(row.amount), 2)
# Today's stats
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
# Today's stats (CST time zone)
now_cst = datetime.now(CST)
today_start = now_cst.replace(hour=0, minute=0, second=0, microsecond=0)
today_end = today_start + timedelta(days=1)
today_result = await db.execute(
select(
func.count().label("paid_count"),
@@ -469,6 +486,7 @@ async def get_payment_stats(
).where(
PaymentOrder.status == "paid",
PaymentOrder.paid_at >= today_start,
PaymentOrder.paid_at < today_end,
)
)
today_row = today_result.one()
@@ -495,7 +513,7 @@ async def get_payment_stats(
"amount": round(o.amount, 2),
"credits": round(o.credits, 2),
"payment_method": o.payment_method,
"status": o.status,
"status": o.status if o.status in ("pending", "paid", "cancelled") else "cancelled",
"trade_no": o.trade_no,
"paid_at": _iso(o.paid_at),
"created_at": _iso(o.created_at),
@@ -547,7 +565,7 @@ async def get_admin_payment_orders(
"amount": round(o.amount, 2),
"credits": round(o.credits, 2),
"payment_method": o.payment_method,
"status": o.status,
"status": o.status if o.status in ("pending", "paid", "cancelled") else "cancelled",
"trade_no": o.trade_no,
"paid_at": _iso(o.paid_at),
"created_at": _iso(o.created_at),
+27 -1
View File
@@ -25,7 +25,10 @@ router = APIRouter(prefix="/payments", tags=["payments"])
@router.get("/methods")
async def get_payment_methods(db: AsyncSession = Depends(get_db)):
async def get_payment_methods(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Return which payment methods are enabled (from admin config)."""
from app.services.payment import _get_payment_configs
configs = await _get_payment_configs(db)
@@ -133,6 +136,29 @@ async def list_orders(
return orders
@router.get("/orders/{order_no}", response_model=PaymentOrderOut)
async def get_order(
order_no: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
from app.services.payment import _check_and_expire_order
result = await db.execute(
select(PaymentOrder)
.where(
PaymentOrder.order_no == order_no,
PaymentOrder.user_id == current_user.id,
)
.limit(1)
)
order = result.scalar_one_or_none()
if not order:
raise HTTPException(status_code=404, detail="订单不存在")
# Auto-expire if needed
await _check_and_expire_order(db, order)
return order
@router.post("/orders/{order_no}/cancel")
async def cancel_order(
order_no: str,
+4
View File
@@ -331,6 +331,10 @@ export async function getPaymentOrders(): Promise<any[]> {
return api.get('/payments/orders');
}
export async function getPaymentOrder(orderNo: string): Promise<any> {
return api.get(`/payments/orders/${orderNo}`);
}
export async function cancelPaymentOrder(orderNo: string): Promise<void> {
return api.post(`/payments/orders/${orderNo}/cancel`);
}
@@ -27,7 +27,7 @@ import {
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore';
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrders, cancelPaymentOrder, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
import NotificationPopup from '../NotificationPopup';
interface MenuConfig {
@@ -97,6 +97,9 @@ const AppLayout: React.FC = () => {
const countdownTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const currentOrderNoRef = useRef<string | null>(null);
const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false });
// LocalStorage keys
const PENDING_ORDER_KEY = 'pending_payment_order';
// 监听预览弹窗状态,关闭浮动按钮
useEffect(() => {
@@ -122,6 +125,57 @@ const AppLayout: React.FC = () => {
}).catch(() => {});
};
// 检查并恢复待处理的支付订单
useEffect(() => {
const checkPendingOrder = async () => {
const savedOrderStr = localStorage.getItem(PENDING_ORDER_KEY);
if (savedOrderStr) {
try {
const savedOrder = JSON.parse(savedOrderStr);
// 查询订单状态
const order = await getPaymentOrder(savedOrder.orderNo);
if (order.status === 'pending') {
// 订单仍然待支付,恢复弹窗
setCurrentPaymentInfo({
price: savedOrder.price,
credits: savedOrder.credits,
qrCode: savedOrder.qrCode,
method: savedOrder.method,
});
currentOrderNoRef.current = savedOrder.orderNo;
// 计算剩余时间
const now = Date.now();
const createdAt = new Date(savedOrder.createdAt).getTime();
const timeoutSeconds = savedOrder.timeoutSeconds || 180;
const elapsedSeconds = Math.floor((now - createdAt) / 1000);
const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds);
if (remainingSeconds > 0) {
setQrCodeModalOpen(true);
startPolling(savedOrder.orderNo, remainingSeconds);
} else {
// 已超时,清除
localStorage.removeItem(PENDING_ORDER_KEY);
}
} else if (order.status === 'paid') {
// 已支付
message.success('支付成功!积分已到账');
useAuthStore.getState().refreshUser();
localStorage.removeItem(PENDING_ORDER_KEY);
} else {
// 订单已取消或其他状态,清除
localStorage.removeItem(PENDING_ORDER_KEY);
}
} catch {
// 查询失败,清除
localStorage.removeItem(PENDING_ORDER_KEY);
}
}
};
checkPendingOrder();
}, []);
useEffect(() => {
getMenuConfigs().then(data => {
let items = data.filter((m: any) => m.is_active !== false && m.isActive !== false);
@@ -202,22 +256,23 @@ const AppLayout: React.FC = () => {
stopPolling();
setCountdown(timeoutSeconds);
// 订单状态轮询(每2秒查询一次,减少请求频率
// 订单状态轮询(每2秒查询一次,只查询当前订单
const pollingTimer = setInterval(async () => {
try {
const orders = await getPaymentOrders();
const order = orders.find((o: any) => o.orderNo === orderNo);
if (order && order.status === 'paid') {
const order = await getPaymentOrder(orderNo);
if (order.status === 'paid') {
stopPolling();
currentOrderNoRef.current = null;
localStorage.removeItem(PENDING_ORDER_KEY);
message.success('支付成功!积分已到账');
useAuthStore.getState().refreshUser();
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
setSelectedPlan(null);
} else if (order && order.status === 'cancelled') {
} else if (order.status === 'cancelled') {
stopPolling();
currentOrderNoRef.current = null;
localStorage.removeItem(PENDING_ORDER_KEY);
}
} catch {
// ignore polling errors
@@ -235,6 +290,7 @@ const AppLayout: React.FC = () => {
cancelPaymentOrder(currentOrderNoRef.current).catch(() => {});
currentOrderNoRef.current = null;
}
localStorage.removeItem(PENDING_ORDER_KEY);
message.warning('订单已超时,请重新充值');
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
@@ -639,15 +695,28 @@ const AppLayout: React.FC = () => {
const order = await createRechargeOrder(plan.id, paymentMethod);
if (order.paymentMethod === 'alipay' && order.qrUrl) {
// Alipay: show the real QR code URL from the backend
setCurrentPaymentInfo({
const paymentInfo = {
price: plan.price,
credits: totalCredits,
qrCode: order.qrUrl,
method: 'alipay',
});
};
setCurrentPaymentInfo(paymentInfo);
setRechargeModalOpen(false);
setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo;
// 保存到 localStorage
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo,
price: plan.price,
credits: totalCredits,
qrCode: order.qrUrl,
method: 'alipay',
createdAt: order.createdAt || new Date().toISOString(),
timeoutSeconds: 180,
}));
// Start polling for payment status
startPolling(order.orderNo);
} else {
@@ -684,6 +753,7 @@ const AppLayout: React.FC = () => {
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch {}
currentOrderNoRef.current = null;
}
localStorage.removeItem(PENDING_ORDER_KEY);
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
}}
@@ -809,6 +879,7 @@ const AppLayout: React.FC = () => {
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch {}
currentOrderNoRef.current = null;
}
localStorage.removeItem(PENDING_ORDER_KEY);
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
setSelectedPlan(null);