diff --git a/src/app/api/admin/orders/[id]/cancel/route.ts b/src/app/api/admin/orders/[id]/cancel/route.ts index 1279301..e200bfd 100644 --- a/src/app/api/admin/orders/[id]/cancel/route.ts +++ b/src/app/api/admin/orders/[id]/cancel/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth'; -import { adminCancelOrder, OrderError } from '@/lib/order/service'; +import { adminCancelOrder } from '@/lib/order/service'; +import { handleApiError } from '@/lib/utils/api'; export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { if (!(await verifyAdminToken(request))) return unauthorizedResponse(); @@ -13,10 +14,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } 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 }); + return handleApiError(error, '取消订单失败'); } } diff --git a/src/app/api/admin/orders/[id]/retry/route.ts b/src/app/api/admin/orders/[id]/retry/route.ts index e858892..c67ae94 100644 --- a/src/app/api/admin/orders/[id]/retry/route.ts +++ b/src/app/api/admin/orders/[id]/retry/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth'; -import { retryRecharge, OrderError } from '@/lib/order/service'; +import { retryRecharge } from '@/lib/order/service'; +import { handleApiError } from '@/lib/utils/api'; export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { if (!(await verifyAdminToken(request))) return unauthorizedResponse(); @@ -10,10 +11,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ 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 }); + return handleApiError(error, '重试充值失败'); } } diff --git a/src/app/api/admin/orders/[id]/route.ts b/src/app/api/admin/orders/[id]/route.ts index c498fbb..313bda2 100644 --- a/src/app/api/admin/orders/[id]/route.ts +++ b/src/app/api/admin/orders/[id]/route.ts @@ -23,6 +23,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ ...order, amount: Number(order.amount), + payAmount: order.payAmount ? Number(order.payAmount) : null, + feeRate: order.feeRate ? Number(order.feeRate) : null, refundAmount: order.refundAmount ? Number(order.refundAmount) : null, }); } diff --git a/src/app/api/admin/orders/route.ts b/src/app/api/admin/orders/route.ts index 5cd2f3b..0836e8b 100644 --- a/src/app/api/admin/orders/route.ts +++ b/src/app/api/admin/orders/route.ts @@ -16,11 +16,38 @@ export async function GET(request: NextRequest) { const where: Prisma.OrderWhereInput = {}; if (status && status in OrderStatus) where.status = status as OrderStatus; - if (userId) where.userId = Number(userId); + + // userId 校验:忽略无效值(NaN) + if (userId) { + const parsedUserId = Number(userId); + if (Number.isFinite(parsedUserId)) { + where.userId = parsedUserId; + } + } + + // 日期校验:忽略无效日期 if (dateFrom || dateTo) { - where.createdAt = {}; - if (dateFrom) where.createdAt.gte = new Date(dateFrom); - if (dateTo) where.createdAt.lte = new Date(dateTo); + const createdAt: Prisma.DateTimeFilter = {}; + let hasValidDate = false; + + if (dateFrom) { + const d = new Date(dateFrom); + if (!isNaN(d.getTime())) { + createdAt.gte = d; + hasValidDate = true; + } + } + if (dateTo) { + const d = new Date(dateTo); + if (!isNaN(d.getTime())) { + createdAt.lte = d; + hasValidDate = true; + } + } + + if (hasValidDate) { + where.createdAt = createdAt; + } } const [orders, total] = await Promise.all([ diff --git a/src/app/api/admin/refund/route.ts b/src/app/api/admin/refund/route.ts index f3846f1..414eb8e 100644 --- a/src/app/api/admin/refund/route.ts +++ b/src/app/api/admin/refund/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth'; -import { processRefund, OrderError } from '@/lib/order/service'; +import { processRefund } from '@/lib/order/service'; +import { handleApiError } from '@/lib/utils/api'; const refundSchema = z.object({ order_id: z.string().min(1), @@ -28,10 +29,6 @@ export async function POST(request: NextRequest) { 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 }); + return handleApiError(error, '退款失败'); } } diff --git a/src/app/api/alipay/notify/route.ts b/src/app/api/alipay/notify/route.ts index 5bfd700..6fd55d9 100644 --- a/src/app/api/alipay/notify/route.ts +++ b/src/app/api/alipay/notify/route.ts @@ -1,9 +1,9 @@ import { NextRequest } from 'next/server'; import { handlePaymentNotify } from '@/lib/order/service'; -import { AlipayProvider } from '@/lib/alipay/provider'; +import { paymentRegistry } from '@/lib/payment'; +import type { PaymentType } from '@/lib/payment'; import { getEnv } from '@/lib/config'; - -const alipayProvider = new AlipayProvider(); +import { extractHeaders } from '@/lib/utils/api'; export async function POST(request: NextRequest) { try { @@ -13,14 +13,15 @@ export async function POST(request: NextRequest) { return new Response('success', { headers: { 'Content-Type': 'text/plain' } }); } + const provider = paymentRegistry.getProvider('alipay_direct' as PaymentType); const rawBody = await request.text(); - const headers: Record = {}; - request.headers.forEach((value, key) => { - headers[key] = value; - }); + const headers = extractHeaders(request); - const notification = await alipayProvider.verifyNotification(rawBody, headers); - const success = await handlePaymentNotify(notification, alipayProvider.name); + const notification = await provider.verifyNotification(rawBody, headers); + if (!notification) { + return new Response('success', { headers: { 'Content-Type': 'text/plain' } }); + } + const success = await handlePaymentNotify(notification, provider.name); return new Response(success ? 'success' : 'fail', { headers: { 'Content-Type': 'text/plain' }, }); diff --git a/src/app/api/easy-pay/notify/route.ts b/src/app/api/easy-pay/notify/route.ts index 1cecada..7bdd14d 100644 --- a/src/app/api/easy-pay/notify/route.ts +++ b/src/app/api/easy-pay/notify/route.ts @@ -1,19 +1,21 @@ import { NextRequest } from 'next/server'; import { handlePaymentNotify } from '@/lib/order/service'; -import { EasyPayProvider } from '@/lib/easy-pay/provider'; - -const easyPayProvider = new EasyPayProvider(); +import { paymentRegistry } from '@/lib/payment'; +import type { PaymentType } from '@/lib/payment'; +import { extractHeaders } from '@/lib/utils/api'; export async function GET(request: NextRequest) { try { + // EasyPay 注册为 'alipay' 和 'wxpay' 类型,任一均可获取同一 provider 实例 + const provider = paymentRegistry.getProvider('alipay' as PaymentType); const rawBody = request.nextUrl.searchParams.toString(); - const headers: Record = {}; - request.headers.forEach((value, key) => { - headers[key] = value; - }); + const headers = extractHeaders(request); - const notification = await easyPayProvider.verifyNotification(rawBody, headers); - const success = await handlePaymentNotify(notification, easyPayProvider.name); + const notification = await provider.verifyNotification(rawBody, headers); + if (!notification) { + return new Response('success', { headers: { 'Content-Type': 'text/plain' } }); + } + const success = await handlePaymentNotify(notification, provider.name); return new Response(success ? 'success' : 'fail', { headers: { 'Content-Type': 'text/plain' }, }); diff --git a/src/app/api/orders/[id]/cancel/route.ts b/src/app/api/orders/[id]/cancel/route.ts index cfb8745..70a1075 100644 --- a/src/app/api/orders/[id]/cancel/route.ts +++ b/src/app/api/orders/[id]/cancel/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; -import { cancelOrder, OrderError } from '@/lib/order/service'; +import { cancelOrder } from '@/lib/order/service'; import { getCurrentUserByToken } from '@/lib/sub2api/client'; +import { handleApiError } from '@/lib/utils/api'; const cancelSchema = z.object({ token: z.string().min(1), @@ -31,10 +32,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } 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('Cancel order error:', error); - return NextResponse.json({ error: '取消订单失败' }, { status: 500 }); + return handleApiError(error, '取消订单失败'); } } diff --git a/src/app/api/orders/[id]/route.ts b/src/app/api/orders/[id]/route.ts index 67a7143..8b8564b 100644 --- a/src/app/api/orders/[id]/route.ts +++ b/src/app/api/orders/[id]/route.ts @@ -1,7 +1,16 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db'; -// 仅返回订单状态相关字段,不暴露任何用户隐私信息 +/** + * 订单状态轮询接口 — 仅返回 status / expiresAt 两个字段。 + * + * 安全考虑: + * - 订单 ID 使用 CUID(25 位随机字符),具有足够的不可预测性, + * 暴力猜测的成本远高于信息价值。 + * - 仅暴露 status 和 expiresAt,不涉及用户隐私或金额信息。 + * - 前端 PaymentQRCode 组件每 2 秒轮询此接口以更新支付状态, + * 添加认证会增加不必要的复杂度且影响轮询性能。 + */ export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const { id } = await params; diff --git a/src/app/api/orders/my/route.ts b/src/app/api/orders/my/route.ts index 0eadabb..87ad404 100644 --- a/src/app/api/orders/my/route.ts +++ b/src/app/api/orders/my/route.ts @@ -16,8 +16,16 @@ export async function GET(request: NextRequest) { const rawPageSize = Number(searchParams.get('page_size') || '20'); const pageSize = VALID_PAGE_SIZES.includes(rawPageSize) ? rawPageSize : 20; + // 单独处理认证,区分认证失败和其他错误 + let user; + try { + user = await getCurrentUserByToken(token); + } catch (error) { + console.error('Auth error in /api/orders/my:', error); + return NextResponse.json({ error: 'unauthorized' }, { status: 401 }); + } + try { - const user = await getCurrentUserByToken(token); const where = { userId: user.id }; const [orders, total, statusGroups] = await Promise.all([ @@ -76,6 +84,6 @@ export async function GET(request: NextRequest) { }); } catch (error) { console.error('Get my orders error:', error); - return NextResponse.json({ error: 'unauthorized' }, { status: 401 }); + return NextResponse.json({ error: '获取订单失败' }, { status: 500 }); } } diff --git a/src/app/api/orders/route.ts b/src/app/api/orders/route.ts index fc1f5b5..ad5f762 100644 --- a/src/app/api/orders/route.ts +++ b/src/app/api/orders/route.ts @@ -1,9 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; -import { createOrder, OrderError } from '@/lib/order/service'; +import { createOrder } from '@/lib/order/service'; import { getEnv } from '@/lib/config'; -import { initPaymentProviders, paymentRegistry } from '@/lib/payment'; +import { paymentRegistry } from '@/lib/payment'; import { getCurrentUserByToken } from '@/lib/sub2api/client'; +import { handleApiError } from '@/lib/utils/api'; const createOrderSchema = z.object({ token: z.string().min(1), @@ -17,7 +18,6 @@ const createOrderSchema = z.object({ export async function POST(request: NextRequest) { try { const env = getEnv(); - initPaymentProviders(); const body = await request.json(); const parsed = createOrderSchema.safeParse(body); @@ -66,10 +66,6 @@ export async function POST(request: NextRequest) { const { userName: _u, userBalance: _b, ...safeResult } = result; return NextResponse.json(safeResult); } catch (error) { - if (error instanceof OrderError) { - return NextResponse.json({ error: error.message, code: error.code }, { status: error.statusCode }); - } - console.error('Create order error:', error); - return NextResponse.json({ error: '创建订单失败,请稍后重试' }, { status: 500 }); + return handleApiError(error, '创建订单失败,请稍后重试'); } } diff --git a/src/app/api/stripe/webhook/route.ts b/src/app/api/stripe/webhook/route.ts index 462fa2c..7dec1e5 100644 --- a/src/app/api/stripe/webhook/route.ts +++ b/src/app/api/stripe/webhook/route.ts @@ -1,21 +1,18 @@ import { NextRequest, NextResponse } from 'next/server'; -import { initPaymentProviders, paymentRegistry } from '@/lib/payment'; +import { paymentRegistry } from '@/lib/payment'; import type { PaymentType } from '@/lib/payment'; import { handlePaymentNotify } from '@/lib/order/service'; +import { extractHeaders } from '@/lib/utils/api'; // Stripe needs raw body - ensure Next.js doesn't parse it export const dynamic = 'force-dynamic'; export async function POST(request: NextRequest): Promise { try { - initPaymentProviders(); const provider = paymentRegistry.getProvider('stripe' as PaymentType); const rawBody = Buffer.from(await request.arrayBuffer()); - const headers: Record = {}; - request.headers.forEach((value, key) => { - headers[key.toLowerCase()] = value; - }); + const headers = extractHeaders(request); const notification = await provider.verifyNotification(rawBody, headers); if (!notification) { diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index d25d56c..1c0536c 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getUser } from '@/lib/sub2api/client'; +import { getUser, getCurrentUserByToken } from '@/lib/sub2api/client'; import { getEnv } from '@/lib/config'; import { queryMethodLimits } from '@/lib/order/limits'; import { initPaymentProviders, paymentRegistry } from '@/lib/payment'; @@ -11,7 +11,24 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '无效的用户 ID' }, { status: 400 }); } + const token = request.nextUrl.searchParams.get('token')?.trim(); + if (!token) { + return NextResponse.json({ error: '缺少 token 参数' }, { status: 401 }); + } + try { + // 验证 token 并确保请求的 user_id 与 token 对应的用户匹配 + let tokenUser; + try { + tokenUser = await getCurrentUserByToken(token); + } catch { + return NextResponse.json({ error: '无效的 token' }, { status: 401 }); + } + + if (tokenUser.id !== userId) { + return NextResponse.json({ error: '无权访问该用户信息' }, { status: 403 }); + } + const env = getEnv(); initPaymentProviders(); const enabledTypes = paymentRegistry.getSupportedTypes(); diff --git a/src/app/api/wxpay/notify/route.ts b/src/app/api/wxpay/notify/route.ts index 01bc73f..880c749 100644 --- a/src/app/api/wxpay/notify/route.ts +++ b/src/app/api/wxpay/notify/route.ts @@ -1,9 +1,9 @@ import { NextRequest } from 'next/server'; import { handlePaymentNotify } from '@/lib/order/service'; -import { WxpayProvider } from '@/lib/wxpay'; +import { paymentRegistry } from '@/lib/payment'; +import type { PaymentType } from '@/lib/payment'; import { getEnv } from '@/lib/config'; - -const wxpayProvider = new WxpayProvider(); +import { extractHeaders } from '@/lib/utils/api'; export async function POST(request: NextRequest) { try { @@ -13,17 +13,15 @@ export async function POST(request: NextRequest) { return Response.json({ code: 'SUCCESS', message: '成功' }); } + const provider = paymentRegistry.getProvider('wxpay_direct' as PaymentType); const rawBody = await request.text(); - const headers: Record = {}; - request.headers.forEach((value, key) => { - headers[key] = value; - }); + const headers = extractHeaders(request); - const notification = await wxpayProvider.verifyNotification(rawBody, headers); + const notification = await provider.verifyNotification(rawBody, headers); if (!notification) { return Response.json({ code: 'SUCCESS', message: '成功' }); } - const success = await handlePaymentNotify(notification, wxpayProvider.name); + const success = await handlePaymentNotify(notification, provider.name); return Response.json( success ? { code: 'SUCCESS', message: '成功' } : { code: 'FAIL', message: '处理失败' }, { status: success ? 200 : 500 }, diff --git a/src/app/pay/page.tsx b/src/app/pay/page.tsx index 8ac006d..03933ac 100644 --- a/src/app/pay/page.tsx +++ b/src/app/pay/page.tsx @@ -135,7 +135,7 @@ function PayContent() { } // 获取服务端支付配置 - const cfgRes = await fetch(`/api/user?user_id=${meId}`); + const cfgRes = await fetch(`/api/user?user_id=${meId}&token=${encodeURIComponent(token)}`); if (cfgRes.ok) { const cfgData = await cfgRes.json(); if (cfgData.config) { diff --git a/src/lib/admin-auth.ts b/src/lib/admin-auth.ts index e713ed3..9a190de 100644 --- a/src/lib/admin-auth.ts +++ b/src/lib/admin-auth.ts @@ -30,7 +30,23 @@ async function isSub2ApiAdmin(token: string): Promise { } export async function verifyAdminToken(request: NextRequest): Promise { - const token = request.nextUrl.searchParams.get('token'); + // 优先从 Authorization: Bearer header 获取 + let token: string | null = null; + const authHeader = request.headers.get('authorization'); + if (authHeader?.startsWith('Bearer ')) { + token = authHeader.slice(7).trim(); + } + + // Fallback: query parameter(向后兼容,已弃用) + if (!token) { + token = request.nextUrl.searchParams.get('token'); + if (token) { + console.warn( + '[DEPRECATED] Admin token passed via query parameter. Use "Authorization: Bearer " header instead.', + ); + } + } + if (!token) return false; // 1. 本地 admin token diff --git a/src/lib/payment/index.ts b/src/lib/payment/index.ts index ab953b4..79a1d18 100644 --- a/src/lib/payment/index.ts +++ b/src/lib/payment/index.ts @@ -69,3 +69,6 @@ export function initPaymentProviders(): void { initialized = true; } + +// 注入 lazy init:Registry 方法会自动调用 initPaymentProviders() +paymentRegistry.setInitializer(initPaymentProviders); diff --git a/src/lib/payment/registry.ts b/src/lib/payment/registry.ts index e66a16f..53b0ad4 100644 --- a/src/lib/payment/registry.ts +++ b/src/lib/payment/registry.ts @@ -2,6 +2,18 @@ import type { PaymentProvider, PaymentType, MethodDefaultLimits } from './types' export class PaymentProviderRegistry { private providers = new Map(); + private _ensureInitialized: (() => void) | null = null; + + /** 设置 lazy init 回调,由 initPaymentProviders 注入 */ + setInitializer(fn: () => void): void { + this._ensureInitialized = fn; + } + + private autoInit(): void { + if (this._ensureInitialized) { + this._ensureInitialized(); + } + } register(provider: PaymentProvider): void { for (const type of provider.supportedTypes) { @@ -10,6 +22,7 @@ export class PaymentProviderRegistry { } getProvider(type: PaymentType): PaymentProvider { + this.autoInit(); const provider = this.providers.get(type); if (!provider) { throw new Error(`No payment provider registered for type: ${type}`); @@ -18,21 +31,25 @@ export class PaymentProviderRegistry { } hasProvider(type: PaymentType): boolean { + this.autoInit(); return this.providers.has(type); } getSupportedTypes(): PaymentType[] { + this.autoInit(); return Array.from(this.providers.keys()); } /** 获取指定渠道的提供商默认限额(未注册时返回 undefined) */ getDefaultLimit(type: string): MethodDefaultLimits | undefined { + this.autoInit(); const provider = this.providers.get(type as PaymentType); return provider?.defaultLimits?.[type]; } /** 获取指定渠道对应的提供商 key(如 'easypay'、'stripe') */ getProviderKey(type: string): string | undefined { + this.autoInit(); const provider = this.providers.get(type as PaymentType); return provider?.providerKey; } diff --git a/src/lib/utils/api.ts b/src/lib/utils/api.ts new file mode 100644 index 0000000..88aff57 --- /dev/null +++ b/src/lib/utils/api.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { OrderError } from '@/lib/order/service'; + +/** 统一处理 OrderError 和未知错误 */ +export function handleApiError(error: unknown, fallbackMessage: string): NextResponse { + if (error instanceof OrderError) { + return NextResponse.json({ error: error.message, code: error.code }, { status: error.statusCode }); + } + console.error(`${fallbackMessage}:`, error); + return NextResponse.json({ error: fallbackMessage }, { status: 500 }); +} + +/** 从 NextRequest 提取 headers 为普通对象 */ +export function extractHeaders(request: NextRequest): Record { + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + return headers; +}