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

View File

@@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from 'next/server';
import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth';
import { adminCancelOrder, OrderError } from '@/lib/order/service';
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
if (!verifyAdminToken(request)) return unauthorizedResponse();
try {
const { id } = await params;
await adminCancelOrder(id);
return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof OrderError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode },
);
}
console.error('Admin cancel order error:', error);
return NextResponse.json({ error: '取消订单失败' }, { status: 500 });
}
}

View File

@@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from 'next/server';
import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth';
import { retryRecharge, OrderError } from '@/lib/order/service';
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
if (!verifyAdminToken(request)) return unauthorizedResponse();
try {
const { id } = await params;
await retryRecharge(id);
return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof OrderError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode },
);
}
console.error('Retry recharge error:', error);
return NextResponse.json({ error: '重试充值失败' }, { status: 500 });
}
}

View File

@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
if (!verifyAdminToken(request)) return unauthorizedResponse();
const { id } = await params;
const order = await prisma.order.findUnique({
where: { id },
include: {
auditLogs: {
orderBy: { createdAt: 'desc' },
},
},
});
if (!order) {
return NextResponse.json({ error: '订单不存在' }, { status: 404 });
}
return NextResponse.json({
...order,
amount: Number(order.amount),
refundAmount: order.refundAmount ? Number(order.refundAmount) : null,
});
}

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth';
import { Prisma } from '@prisma/client';
export async function GET(request: NextRequest) {
if (!verifyAdminToken(request)) return unauthorizedResponse();
const searchParams = request.nextUrl.searchParams;
const page = Math.max(1, Number(searchParams.get('page') || '1'));
const pageSize = Math.min(100, Math.max(1, Number(searchParams.get('page_size') || '20')));
const status = searchParams.get('status');
const userId = searchParams.get('user_id');
const dateFrom = searchParams.get('date_from');
const dateTo = searchParams.get('date_to');
const where: Prisma.OrderWhereInput = {};
if (status) where.status = status as any;
if (userId) where.userId = Number(userId);
if (dateFrom || dateTo) {
where.createdAt = {};
if (dateFrom) where.createdAt.gte = new Date(dateFrom);
if (dateTo) where.createdAt.lte = new Date(dateTo);
}
const [orders, total] = await Promise.all([
prisma.order.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
select: {
id: true,
userId: true,
userName: true,
userEmail: true,
amount: true,
status: true,
paymentType: true,
createdAt: true,
paidAt: true,
completedAt: true,
failedReason: true,
expiresAt: true,
},
}),
prisma.order.count({ where }),
]);
return NextResponse.json({
orders: orders.map(o => ({
...o,
amount: Number(o.amount),
})),
total,
page,
page_size: pageSize,
total_pages: Math.ceil(total / pageSize),
});
}

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth';
import { processRefund, OrderError } from '@/lib/order/service';
const refundSchema = z.object({
order_id: z.string().min(1),
reason: z.string().optional(),
force: z.boolean().optional().default(false),
});
export async function POST(request: NextRequest) {
if (!verifyAdminToken(request)) return unauthorizedResponse();
try {
const body = await request.json();
const parsed = refundSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: '参数错误', details: parsed.error.flatten().fieldErrors },
{ status: 400 },
);
}
const result = await processRefund({
orderId: parsed.data.order_id,
reason: parsed.data.reason,
force: parsed.data.force,
});
return NextResponse.json(result);
} catch (error) {
if (error instanceof OrderError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode },
);
}
console.error('Refund error:', error);
return NextResponse.json({ error: '退款失败' }, { status: 500 });
}
}