From d0b98449e2e5c8f121afcc15cdf5aefd56df402b Mon Sep 17 00:00:00 2001 From: Mosijiu Date: Wed, 24 Dec 2025 16:42:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=EF=BC=8C=E6=9F=A5=E7=9C=8B=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/account/page.tsx | 193 ++++++++++++++++++++++++++++++ app/api/orders/route.ts | 39 ++++++ app/api/payment/callback/route.ts | 147 ++++++++++++++++++++--- app/api/payment/status/route.ts | 10 ++ app/payment/success/page.tsx | 78 ++++++++++++ components/Header.tsx | 21 +++- 6 files changed, 468 insertions(+), 20 deletions(-) create mode 100644 app/account/page.tsx create mode 100644 app/api/orders/route.ts diff --git a/app/account/page.tsx b/app/account/page.tsx new file mode 100644 index 0000000..f361be4 --- /dev/null +++ b/app/account/page.tsx @@ -0,0 +1,193 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useSession } from 'next-auth/react' +import { Loader2, User, Mail, AlertCircle, PackageCheck, KeyRound, CreditCard } from 'lucide-react' +import Link from 'next/link' +import Header from '@/components/Header' + +type Order = { + id: number + order_no: string + product_type: string + product_name: string + amount: number + status: string + payment_method?: string + trade_no?: string + created_at: string + paid_at?: string + metadata?: any +} + +export default function AccountPage() { + const { data: session, status } = useSession() + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + if (status !== 'authenticated') return + const fetchOrders = async () => { + try { + const res = await fetch('/api/orders') + const data = await res.json() + if (res.ok && data.success) { + setOrders(data.orders || []) + } else { + setError(data.message || '加载订单失败') + } + } catch (e: any) { + setError(e?.message || '加载订单失败') + } finally { + setLoading(false) + } + } + fetchOrders() + }, [status]) + + if (status === 'loading') { + return ( +
+ +
+ ) + } + + if (status !== 'authenticated' || !session) { + return ( +
+ +

请先登录后查看个人中心

+ + 去登录 + +
+ ) + } + + return ( +
+
+
+
+

个人中心

+

查看账户信息、订单和激活码

+
+ +
+
+
+
+ +
+
+

{session.user?.name || '未命名用户'}

+

ID: {session.user?.id}

+
+
+
+ + {session.user?.email} +
+
+ +
+
+

订单列表

+ {loading && } +
+ + {error && ( +
+ + {error} +
+ )} + + {orders.length === 0 && !loading ? ( +

暂无订单

+ ) : ( +
+ {orders.map((o) => { + const license = o.metadata?.license + const licenseError = o.metadata?.licenseError + return ( +
+
+
{o.order_no}
+
+ {o.status} +
+
+
+ {o.product_name} · ¥{o.amount} · {o.payment_method || '—'} +
+ {o.trade_no && ( +
交易号:{o.trade_no}
+ )} + + {/* 激活码区域 */} + {o.status === 'paid' && ( +
+ {license ? ( +
+
+
+ + 激活码 +
+ + {license.planName || license.plan} + +
+
+ {license.activationCode} +
+ {license.isPermanent === false && license.expiresAt && ( +
+ 到期:{license.expiresAt} +
+ )} + {license.isPermanent && ( +
永久有效
+ )} +
+ ) : licenseError ? ( +
+ 激活码生成失败:{licenseError.message || '请联系客服'}{' '} + {licenseError.at ? `(${licenseError.at})` : ''} +
+ ) : ( +
+ 激活码生成中,请稍后再查看 +
+ )} +
+ )} +
+ ) + })} +
+ )} +
+
+
+
+ ) +} + diff --git a/app/api/orders/route.ts b/app/api/orders/route.ts new file mode 100644 index 0000000..c2b0ca6 --- /dev/null +++ b/app/api/orders/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth/next' +import { authOptions } from '@/lib/authOptions' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' + +function parseCallback(raw?: string | null): Record { + if (!raw) return {} + try { + return JSON.parse(raw) + } catch { + return { raw } + } +} + +export async function GET(request: NextRequest) { + try { + const session = await getServerSession(authOptions) + if (!session || !session.user?.id) { + return NextResponse.json({ message: '请先登录' }, { status: 401 }) + } + + const userId = parseInt(session.user.id) + const orders = await db.getUserOrders(userId) + + return NextResponse.json({ + success: true, + orders: orders.map((o) => ({ + ...o, + metadata: parseCallback(o.callback_data), + })), + }) + } catch (error) { + console.error('获取订单列表失败:', error) + return NextResponse.json({ message: '获取订单失败' }, { status: 500 }) + } +} + diff --git a/app/api/payment/callback/route.ts b/app/api/payment/callback/route.ts index 58404cf..0aa48c7 100644 --- a/app/api/payment/callback/route.ts +++ b/app/api/payment/callback/route.ts @@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server' import { db } from '@/lib/db' import crypto from 'crypto' +const LICENSE_API_URL = process.env.LICENSE_API_URL || 'http://45.249.208.118:9527/api/license/generate' +const LICENSE_API_KEY = process.env.LICENSE_API_KEY + // 验证嘟噜支付回调MD5签名 function verifySign(params: Record, key: string): boolean { const sign = params.sign @@ -42,6 +45,86 @@ function verifySign(params: Record, key: string): boolean { return calculatedSign === sign.toLowerCase() } +function parseCallbackData(raw?: string | null): Record { + if (!raw) return {} + try { + return JSON.parse(raw) + } catch (e) { + return { raw } + } +} + +function mergeCallbackData(raw: string | null | undefined, patch: Record) { + const base = parseCallbackData(raw) + return JSON.stringify({ ...base, ...patch }) +} + +// 映射本地商品类型到许可证类型(按文档:STANDARD / PRO) +const productPlanMap: Record = { + standard: 'STANDARD', + test: 'STANDARD', +} + +async function generateLicense(order: any) { + if (!LICENSE_API_KEY) { + console.warn('⚠️ LICENSE_API_KEY 未配置,跳过激活码生成') + return null + } + + const plan = productPlanMap[order.product_type] + if (!plan) { + console.log('ℹ️ 当前商品不需要生成激活码,product_type:', order.product_type) + return null + } + + const timestamp = Date.now() + const signStr = `${order.order_no}${timestamp}${LICENSE_API_KEY}` + const sign = crypto.createHash('md5').update(signStr, 'utf8').digest('hex') + + const user = await db.findUserById(order.user_id).catch(() => null) + + const payload: Record = { + orderNo: order.order_no, + plan, + customerEmail: user?.email, + customerName: user?.name, + contactInfo: user?.email, + timestamp, + sign, + } + + const res = await fetch(LICENSE_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': LICENSE_API_KEY, + }, + body: JSON.stringify(payload), + }) + + let json: any = null + try { + json = await res.json() + } catch (err) { + console.error('❌ 激活码接口返回解析失败:', err) + } + + if (!res.ok || !json?.success) { + console.error('❌ 激活码生成失败:', { + status: res.status, + statusText: res.statusText, + response: json, + }) + throw new Error(json?.error || res.statusText) + } + + return { + ...json.data, + plan, + requestedAt: new Date().toISOString(), + } +} + export async function POST(request: NextRequest) { try { const formData = await request.formData() @@ -103,20 +186,43 @@ export async function POST(request: NextRequest) { // 验证支付状态 // 易支付通常 trade_status = 'TRADE_SUCCESS' 表示支付成功 if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') { + // 已有元数据 + const existingMeta = parseCallbackData(order.callback_data) + + // 生成激活码(仅指定商品类型) + let licenseData = existingMeta.license + let licenseError = existingMeta.licenseError + if (!licenseData) { + try { + licenseData = await generateLicense(order) + } catch (err) { + console.error('❌ 激活码生成异常:', err) + licenseError = { + message: (err as any)?.message || '激活码生成失败', + at: new Date().toISOString(), + } + } + } + + const mergedMeta = mergeCallbackData(order.callback_data, { + payment: { + receivedAt: new Date().toISOString(), + tradeStatus, + params, + }, + ...(licenseData ? { license: licenseData } : {}), + ...(licenseError ? { licenseError } : {}), + }) + // 更新订单状态 - await db.updateOrderStatus( - orderNo, - 'paid', - tradeNo, - paymentMethod, - JSON.stringify(params) - ) + await db.updateOrderStatus(orderNo, 'paid', tradeNo, paymentMethod, mergedMeta) console.log('✅ 订单支付成功:', { orderNo, tradeNo, amount: order.amount, paymentMethod, + licenseGenerated: !!licenseData, }) // TODO: 这里可以添加其他业务逻辑,比如: @@ -199,14 +305,27 @@ export async function GET(request: NextRequest) { // 验证支付状态 if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') { + const existingMeta = parseCallbackData(order.callback_data) + let licenseData = existingMeta.license + if (!licenseData) { + try { + licenseData = await generateLicense(order) + } catch (err) { + console.error('❌ 激活码生成异常:', err) + } + } + + const mergedMeta = mergeCallbackData(order.callback_data, { + payment: { + receivedAt: new Date().toISOString(), + tradeStatus, + params, + }, + ...(licenseData ? { license: licenseData } : {}), + }) + // 更新订单状态 - await db.updateOrderStatus( - orderNo, - 'paid', - tradeNo, - paymentMethod, - JSON.stringify(params) - ) + await db.updateOrderStatus(orderNo, 'paid', tradeNo, paymentMethod, mergedMeta) console.log('✅ 订单支付成功:', { orderNo, diff --git a/app/api/payment/status/route.ts b/app/api/payment/status/route.ts index d796a08..f9369a8 100644 --- a/app/api/payment/status/route.ts +++ b/app/api/payment/status/route.ts @@ -5,6 +5,15 @@ import { db } from '@/lib/db' export const dynamic = 'force-dynamic' +function parseCallbackData(raw?: string | null): Record { + if (!raw) return {} + try { + return JSON.parse(raw) + } catch { + return { raw } + } +} + export async function GET(request: NextRequest) { try { // 验证登录状态 @@ -57,6 +66,7 @@ export async function GET(request: NextRequest) { trade_no: order.trade_no, created_at: order.created_at, paid_at: order.paid_at, + ...parseCallbackData(order.callback_data), }, }) } catch (error) { diff --git a/app/payment/success/page.tsx b/app/payment/success/page.tsx index 254ac4e..343ddac 100644 --- a/app/payment/success/page.tsx +++ b/app/payment/success/page.tsx @@ -13,6 +13,7 @@ function PaymentSuccessContent() { const orderNo = searchParams.get('order_no') const [orderStatus, setOrderStatus] = useState<'loading' | 'success' | 'failed' | 'pending'>('loading') const [orderInfo, setOrderInfo] = useState(null) + const [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle') useEffect(() => { if (!orderNo) { @@ -59,6 +60,22 @@ function PaymentSuccessContent() { return () => clearInterval(pollInterval) }, [orderNo]) + const handleCopy = async (text?: string) => { + if (!text) return + try { + await navigator.clipboard.writeText(text) + setCopyState('copied') + setTimeout(() => setCopyState('idle'), 1500) + } catch (e) { + console.error('复制失败:', e) + setCopyState('error') + setTimeout(() => setCopyState('idle'), 1500) + } + } + + const license = orderInfo?.license + const licenseError = orderInfo?.licenseError + return (
@@ -128,6 +145,67 @@ function PaymentSuccessContent() {
)} + {/* 激活码信息 */} + {license ? ( +
+
+
+

激活码已生成

+

请妥善保存,稍后可在此页面再次查看

+
+ +
+
+
+ 激活码: + + {license.activationCode} + +
+ {license.planName && ( +
+ 版本: + {license.planName} +
+ )} + {license.isPermanent === false && license.expiresAt && ( +
+ 到期时间: + {license.expiresAt} +
+ )} + {license.isPermanent && ( +
+ 有效期: + 永久有效 +
+ )} +
+
+ ) : licenseError ? ( +
+

激活码生成失败

+

+ {licenseError.message || '系统在生成激活码时出现错误,请联系客服处理。'} +

+

+ 时间:{licenseError.at} +

+
+ ) : ( +
+

激活码生成中

+

+ 支付已成功,系统正在生成激活码。请稍等片刻或稍后重新打开此页面查看。 +

+
+ )} +
{ {/* Login/User Info */} {status === 'authenticated' && session ? (
- +