feat: migrate payment provider to easy-pay, add order history and refund support

- Replace zpay with easy-pay payment provider (new lib/easy-pay/ module)
- Add order history page for users (pay/orders)
- Add GET /api/orders/my endpoint to list user's own orders
- Add GET /api/users/[id] endpoint for sub2api user lookup
- Add order status tracking module (lib/order/status.ts)
- Update config to support easy-pay credentials (merchant ID, key, gateway)
- Update PaymentForm and PaymentQRCode components for easy-pay flow
- Update pay page and admin page with new order management UI
- Update order service to support easy-pay, cancellation, and refund
This commit is contained in:
erio
2026-03-01 03:04:24 +08:00
commit d5719bf213
73 changed files with 10616 additions and 0 deletions

110
src/app/pay/result/page.tsx Normal file
View File

@@ -0,0 +1,110 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { useEffect, useState, Suspense } from 'react';
function ResultContent() {
const searchParams = useSearchParams();
const outTradeNo = searchParams.get('out_trade_no');
const tradeStatus = searchParams.get('trade_status');
const [status, setStatus] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!outTradeNo) {
setLoading(false);
return;
}
const checkOrder = async () => {
try {
const res = await fetch(`/api/orders/${outTradeNo}`);
if (res.ok) {
const data = await res.json();
setStatus(data.status);
}
} catch {
// ignore
} finally {
setLoading(false);
}
};
checkOrder();
// Poll a few times in case status hasn't updated yet
const timer = setInterval(checkOrder, 3000);
const timeout = setTimeout(() => clearInterval(timer), 30000);
return () => {
clearInterval(timer);
clearTimeout(timeout);
};
}, [outTradeNo]);
if (loading) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-gray-500">...</div>
</div>
);
}
const isSuccess = status === 'COMPLETED' || status === 'PAID' || status === 'RECHARGING';
const isPending = status === 'PENDING';
return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-md rounded-xl bg-white p-8 text-center shadow-lg">
{isSuccess ? (
<>
<div className="text-6xl text-green-500"></div>
<h1 className="mt-4 text-xl font-bold text-green-600">
{status === 'COMPLETED' ? '充值成功' : '充值处理中'}
</h1>
<p className="mt-2 text-gray-500">
{status === 'COMPLETED'
? '余额已成功到账!'
: '支付成功,余额正在充值中...'}
</p>
</>
) : isPending ? (
<>
<div className="text-6xl text-yellow-500"></div>
<h1 className="mt-4 text-xl font-bold text-yellow-600"></h1>
<p className="mt-2 text-gray-500"></p>
</>
) : (
<>
<div className="text-6xl text-red-500"></div>
<h1 className="mt-4 text-xl font-bold text-red-600">
{status === 'EXPIRED' ? '订单已超时' : status === 'CANCELLED' ? '订单已取消' : '支付异常'}
</h1>
<p className="mt-2 text-gray-500">
{status === 'EXPIRED'
? '订单已超时,请重新充值'
: status === 'CANCELLED'
? '订单已被取消'
: '请联系管理员处理'}
</p>
</>
)}
<p className="mt-4 text-xs text-gray-400">
: {outTradeNo || '未知'}
</p>
</div>
</div>
);
}
export default function PayResultPage() {
return (
<Suspense fallback={
<div className="flex min-h-screen items-center justify-center">
<div className="text-gray-500">...</div>
</div>
}>
<ResultContent />
</Suspense>
);
}