修改订单查询接口逻辑、前台页面增加刷新页面订单消失问题,调整后台支付账单页面数据问题
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user