diff --git a/src/app/api/orders/route.ts b/src/app/api/orders/route.ts index 6d7a02c..fc1f5b5 100644 --- a/src/app/api/orders/route.ts +++ b/src/app/api/orders/route.ts @@ -3,9 +3,10 @@ import { z } from 'zod'; import { createOrder, OrderError } from '@/lib/order/service'; import { getEnv } from '@/lib/config'; import { initPaymentProviders, paymentRegistry } from '@/lib/payment'; +import { getCurrentUserByToken } from '@/lib/sub2api/client'; const createOrderSchema = z.object({ - user_id: z.number().int().positive(), + token: z.string().min(1), amount: z.number().positive(), payment_type: z.string().min(1), src_host: z.string().max(253).optional(), @@ -24,7 +25,16 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: '参数错误', details: parsed.error.flatten().fieldErrors }, { status: 400 }); } - const { user_id, amount, payment_type, src_host, src_url, is_mobile } = parsed.data; + const { token, amount, payment_type, src_host, src_url, is_mobile } = parsed.data; + + // 通过 token 解析用户身份 + let userId: number; + try { + const user = await getCurrentUserByToken(token); + userId = user.id; + } catch { + return NextResponse.json({ error: '无效的 token,请重新登录', code: 'INVALID_TOKEN' }, { status: 401 }); + } // Validate amount range if (amount < env.MIN_RECHARGE_AMOUNT || amount > env.MAX_RECHARGE_AMOUNT) { @@ -43,7 +53,7 @@ export async function POST(request: NextRequest) { request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || '127.0.0.1'; const result = await createOrder({ - userId: user_id, + userId, amount, paymentType: payment_type, clientIp, diff --git a/src/app/pay/page.tsx b/src/app/pay/page.tsx index a16426a..8ac006d 100644 --- a/src/app/pay/page.tsx +++ b/src/app/pay/page.tsx @@ -35,7 +35,6 @@ interface AppConfig { function PayContent() { const searchParams = useSearchParams(); - const userId = Number(searchParams.get('user_id')); const token = (searchParams.get('token') || '').trim(); const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light'; const uiMode = searchParams.get('ui_mode') || 'standalone'; @@ -58,6 +57,7 @@ function PayContent() { const [ordersHasMore, setOrdersHasMore] = useState(false); const [ordersLoadingMore, setOrdersLoadingMore] = useState(false); const [activeMobileTab, setActiveMobileTab] = useState<'pay' | 'orders'>('pay'); + const [pendingCount, setPendingCount] = useState(0); const [config, setConfig] = useState({ enabledPaymentTypes: [], @@ -68,12 +68,13 @@ function PayContent() { const [userNotFound, setUserNotFound] = useState(false); const [helpImageOpen, setHelpImageOpen] = useState(false); - const effectiveUserId = resolvedUserId || userId; - const isEmbedded = uiMode === 'embedded' && isIframeContext; const hasToken = token.length > 0; + const isEmbedded = uiMode === 'embedded' && isIframeContext; const helpImageUrl = (config.helpImageUrl || '').trim(); const helpText = (config.helpText || '').trim(); const hasHelpContent = Boolean(helpImageUrl || helpText); + const MAX_PENDING = 3; + const pendingBlocked = pendingCount >= MAX_PENDING; useEffect(() => { if (typeof window === 'undefined') return; @@ -92,12 +93,49 @@ function PayContent() { }, [isMobile, step, tab]); const loadUserAndOrders = async () => { - if (!userId || Number.isNaN(userId) || userId <= 0) return; + if (!token) return; setUserNotFound(false); try { - // 始终获取服务端配置(不含隐私信息) - const cfgRes = await fetch(`/api/user?user_id=${userId}`); + // 通过 token 获取用户详情和订单 + const meRes = await fetch(`/api/orders/my?token=${encodeURIComponent(token)}`); + if (!meRes.ok) { + setUserNotFound(true); + return; + } + + const meData = await meRes.json(); + const meUser = meData.user || {}; + const meId = Number(meUser.id); + if (!Number.isInteger(meId) || meId <= 0) { + setUserNotFound(true); + return; + } + + setResolvedUserId(meId); + setPendingCount(meData.summary?.pending ?? 0); + + setUserInfo({ + id: meId, + username: + (typeof meUser.displayName === 'string' && meUser.displayName.trim()) || + (typeof meUser.username === 'string' && meUser.username.trim()) || + `用户 #${meId}`, + balance: typeof meUser.balance === 'number' ? meUser.balance : undefined, + }); + + if (Array.isArray(meData.orders)) { + setMyOrders(meData.orders); + setOrdersPage(1); + setOrdersHasMore((meData.total_pages ?? 1) > 1); + } else { + setMyOrders([]); + setOrdersPage(1); + setOrdersHasMore(false); + } + + // 获取服务端支付配置 + const cfgRes = await fetch(`/api/user?user_id=${meId}`); if (cfgRes.ok) { const cfgData = await cfgRes.json(); if (cfgData.config) { @@ -111,54 +149,11 @@ function PayContent() { helpText: cfgData.config.helpText ?? null, stripePublishableKey: cfgData.config.stripePublishableKey ?? null, }); - // 应用自定义 sublabel if (cfgData.config.sublabelOverrides) { applySublabelOverrides(cfgData.config.sublabelOverrides); } } - } else if (cfgRes.status === 404) { - setUserNotFound(true); - return; } - - // 有 token 时才尝试获取用户详情和订单 - if (token) { - const meRes = await fetch(`/api/orders/my?token=${encodeURIComponent(token)}`); - if (meRes.ok) { - const meData = await meRes.json(); - const meUser = meData.user || {}; - const meId = Number(meUser.id); - if (Number.isInteger(meId) && meId > 0) { - setResolvedUserId(meId); - } - - setUserInfo({ - id: Number.isInteger(meId) && meId > 0 ? meId : userId, - username: - (typeof meUser.displayName === 'string' && meUser.displayName.trim()) || - (typeof meUser.username === 'string' && meUser.username.trim()) || - `用户 #${userId}`, - balance: typeof meUser.balance === 'number' ? meUser.balance : undefined, - }); - - if (Array.isArray(meData.orders)) { - setMyOrders(meData.orders); - setOrdersPage(1); - setOrdersHasMore((meData.total_pages ?? 1) > 1); - } else { - setMyOrders([]); - setOrdersPage(1); - setOrdersHasMore(false); - } - return; - } - } - - // 无 token 或 token 失效:只显示用户 ID,不展示隐私信息(不显示余额) - setUserInfo({ id: userId, username: `用户 #${userId}` }); - setMyOrders([]); - setOrdersPage(1); - setOrdersHasMore(false); } catch { // ignore and keep page usable } @@ -189,7 +184,7 @@ function PayContent() { useEffect(() => { loadUserAndOrders(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userId, token]); + }, [token]); useEffect(() => { if (step !== 'result' || finalStatus !== 'COMPLETED') return; @@ -205,11 +200,11 @@ function PayContent() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [step, finalStatus]); - if (!effectiveUserId || Number.isNaN(effectiveUserId) || effectiveUserId <= 0) { + if (!hasToken) { return (
-

无效的用户 ID

+

缺少认证信息

请从 Sub2API 平台正确访问充值页面

@@ -229,7 +224,6 @@ function PayContent() { const buildScopedUrl = (path: string, forceOrdersTab = false) => { const params = new URLSearchParams(); - if (effectiveUserId) params.set('user_id', String(effectiveUserId)); if (token) params.set('token', token); params.set('theme', theme); params.set('ui_mode', uiMode); @@ -242,6 +236,11 @@ function PayContent() { const ordersUrl = isMobile ? mobileOrdersUrl : pcOrdersUrl; const handleSubmit = async (amount: number, paymentType: string) => { + if (pendingBlocked) { + setError(`您有 ${pendingCount} 个待支付订单,请先完成或取消后再试(最多 ${MAX_PENDING} 个)`); + return; + } + setLoading(true); setError(''); @@ -250,7 +249,7 @@ function PayContent() { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - user_id: effectiveUserId, + token, amount, payment_type: paymentType, is_mobile: isMobile, @@ -263,6 +262,7 @@ function PayContent() { if (!res.ok) { const codeMessages: Record = { + INVALID_TOKEN: '认证已失效,请重新从平台进入充值页面', USER_INACTIVE: '账户已被禁用,无法充值,请联系管理员', TOO_MANY_PENDING: '您有过多待支付订单,请先完成或取消现有订单后再试', USER_NOT_FOUND: '用户不存在,请检查链接是否正确', @@ -402,7 +402,7 @@ function PayContent() { {isMobile ? ( activeMobileTab === 'pay' ? ( ) : (
@@ -452,9 +456,6 @@ function PayContent() {
  • 订单完成后会自动到账
  • 如需历史记录请查看「我的订单」
  • {config.maxDailyAmount > 0 &&
  • 每日最大充值 ¥{config.maxDailyAmount.toFixed(2)}
  • } - {!hasToken && ( -
  • 当前链接无 token,订单查询受限
  • - )}
    diff --git a/src/components/PaymentForm.tsx b/src/components/PaymentForm.tsx index 3e543f1..e6e650b 100644 --- a/src/components/PaymentForm.tsx +++ b/src/components/PaymentForm.tsx @@ -23,6 +23,8 @@ interface PaymentFormProps { onSubmit: (amount: number, paymentType: string) => Promise; loading?: boolean; dark?: boolean; + pendingBlocked?: boolean; + pendingCount?: number; } const QUICK_AMOUNTS = [10, 20, 50, 100, 200, 500, 1000, 2000]; @@ -43,6 +45,8 @@ export default function PaymentForm({ onSubmit, loading, dark = false, + pendingBlocked = false, + pendingCount = 0, }: PaymentFormProps) { const [amount, setAmount] = useState(''); const [paymentType, setPaymentType] = useState(enabledPaymentTypes[0] || 'alipay'); @@ -321,12 +325,26 @@ export default function PaymentForm({ )} + {/* Pending order limit warning */} + {pendingBlocked && ( +
    + 您有 {pendingCount} 个待支付订单,请先完成或取消后再充值 +
    + )} + {/* Submit */} );