微信登录&支付接入

This commit is contained in:
Mosijiu 2025-12-21 19:28:31 +08:00
parent 13ce098db6
commit 8acfeec678
11 changed files with 1910 additions and 1318 deletions

179
README_PAYMENT.md Normal file
View File

@ -0,0 +1,179 @@
# 💳 支付功能集成说明
## 📋 功能概述
已实现完整的易支付集成功能,包括:
- ✅ 订单创建和管理
- ✅ 易支付接口对接
- ✅ 支付回调处理
- ✅ 支付结果页面
- ✅ 订单状态查询
## 🗄️ 数据库配置
### 1. 创建订单表
执行以下 SQL 脚本创建订单表:
```bash
# 如果使用 Docker PostgreSQL
docker exec -i flnxi-postgres psql -U flnxi_user -d flnxi_db < scripts/create_orders_table.sql
# 如果使用系统 PostgreSQL
psql -U flnxi_user -d flnxi_db -f scripts/create_orders_table.sql
```
或者直接在数据库中执行 `scripts/create_orders_table.sql` 文件内容。
### 2. 表结构说明
| 字段 | 类型 | 说明 |
|------|------|------|
| id | SERIAL | 主键,自增 |
| order_no | VARCHAR(64) | 订单号(唯一) |
| user_id | INTEGER | 用户ID(外键) |
| product_type | VARCHAR(32) | 产品类型:standard/deployment/custom |
| product_name | VARCHAR(255) | 产品名称 |
| amount | DECIMAL(10,2) | 订单金额 |
| status | VARCHAR(32) | 订单状态:pending/paid/failed/cancelled |
| payment_method | VARCHAR(32) | 支付方式 |
| trade_no | VARCHAR(128) | 第三方交易号 |
| callback_data | TEXT | 回调数据(JSON) |
| created_at | TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | 更新时间 |
| paid_at | TIMESTAMP | 支付时间 |
## ⚙️ 环境变量配置
在 `.env.local` 文件中添加以下配置:
```env
# 易支付配置
EPAY_PID=your_merchant_id # 易支付商户ID
EPAY_KEY=your_merchant_key # 易支付商户密钥
EPAY_API_URL=https://your-epay-domain.com/submit.php # 易支付接口地址
# NextAuth配置(如果还没有)
NEXTAUTH_URL=https://your-domain.com # 网站域名
NEXTAUTH_SECRET=your_secret_key # NextAuth密钥
```
## 🔄 支付流程
### 1. 用户点击购买
- 用户在产品页面点击"立即购买"
- 系统检查登录状态,未登录则跳转登录页
### 2. 创建订单
- 前端调用 `/api/payment/create`
- 后端创建订单记录
- 生成易支付支付参数和签名
- 返回支付表单参数
### 3. 跳转支付
- 前端自动提交表单到易支付
- 用户完成支付
### 4. 支付回调
- 易支付调用 `/api/payment/callback`
- 验证签名
- 更新订单状态
- 返回 SUCCESS
### 5. 支付结果
- 用户跳转到 `/payment/success`
- 页面自动查询订单状态
- 显示支付结果
## 📁 文件结构
```
app/
├── api/
│ └── payment/
│ ├── create/
│ │ └── route.ts # 创建支付订单接口
│ ├── callback/
│ │ └── route.ts # 支付回调接口
│ └── status/
│ └── route.ts # 查询订单状态接口
├── payment/
│ └── success/
│ └── page.tsx # 支付结果页面
lib/
└── db.ts # 数据库操作(已扩展订单相关函数)
components/
└── Pricing.tsx # 定价组件(已集成支付功能)
scripts/
└── create_orders_table.sql # 订单表创建脚本
```
## 🧪 测试步骤
### 1. 配置环境变量
确保 `.env.local` 中配置了易支付相关信息。
### 2. 创建订单表
执行数据库迁移脚本。
### 3. 测试支付流程
1. 登录账户
2. 访问首页,滚动到定价部分
3. 点击"立即购买"(标准版或远程部署服务)
4. 应该会跳转到易支付页面
5. 完成支付后,会跳转回支付结果页面
### 4. 测试回调
- 易支付会调用 `/api/payment/callback`
- 检查订单状态是否更新为 `paid`
## 🔒 安全说明
1. **签名验证**:所有回调都会验证签名,防止伪造请求
2. **订单验证**:回调时会验证订单是否存在
3. **用户验证**:查询订单时会验证订单是否属于当前用户
4. **幂等性**:已支付的订单不会重复处理
## 📝 产品配置
在 `app/api/payment/create/route.ts` 中可以配置产品信息:
```typescript
const PRODUCTS = {
standard: {
name: 'FLNXI标准版',
price: 599,
},
deployment: {
name: '远程部署服务',
price: 299,
},
custom: {
name: '定制版咨询',
price: 0, // 需要沟通报价
},
}
```
## 🐛 常见问题
### 1. 回调签名验证失败
- 检查 `EPAY_KEY` 是否正确
- 确认易支付平台使用的签名算法(当前使用MD5)
### 2. 订单状态未更新
- 检查回调接口是否可访问
- 查看服务器日志确认回调是否收到
- 确认数据库连接正常
### 3. 支付后未跳转
- 检查 `NEXTAUTH_URL` 配置是否正确
- 确认易支付配置的回调地址正确
## 📞 技术支持
如有问题,请检查:
1. 服务器日志:`/api/payment/callback` 的调用记录
2. 数据库:订单表的数据是否正确
3. 环境变量:易支付配置是否正确

View File

@ -0,0 +1,229 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import crypto from 'crypto'
// 验证易支付回调签名
function verifySign(params: Record<string, any>, key: string): boolean {
const sign = params.sign
if (!sign) return false
// 移除sign和sign_type参数
const filtered: Record<string, any> = {}
Object.keys(params)
.sort()
.forEach((k) => {
if (k !== 'sign' && k !== 'sign_type' && params[k] !== '' && params[k] !== null && params[k] !== undefined) {
filtered[k] = params[k]
}
})
// 拼接字符串
const signStr = Object.keys(filtered)
.map((k) => `${k}=${filtered[k]}`)
.join('&')
const signStrWithKey = `${signStr}&key=${key}`
// MD5加密并转大写
const calculatedSign = crypto.createHash('md5').update(signStrWithKey).digest('hex').toUpperCase()
return calculatedSign === sign.toUpperCase()
}
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const params: Record<string, any> = {}
// 将FormData转换为对象
formData.forEach((value, key) => {
params[key] = value.toString()
})
// 也支持JSON格式
if (Object.keys(params).length === 0) {
try {
const json = await request.json()
Object.assign(params, json)
} catch {
// 忽略JSON解析错误
}
}
console.log('📥 收到支付回调:', params)
const epayKey = process.env.EPAY_KEY
if (!epayKey) {
console.error('❌ 易支付密钥未配置')
return NextResponse.text('FAIL', { status: 500 })
}
// 验证签名
if (!verifySign(params, epayKey)) {
console.error('❌ 签名验证失败:', params)
return NextResponse.text('FAIL', { status: 400 })
}
// 获取订单信息
const orderNo = params.out_trade_no
const tradeNo = params.trade_no
const tradeStatus = params.trade_status
const paymentMethod = params.type || params.payment_method
if (!orderNo) {
console.error('❌ 订单号缺失')
return NextResponse.text('FAIL', { status: 400 })
}
// 查找订单
const order = await db.findOrderByOrderNo(orderNo)
if (!order) {
console.error('❌ 订单不存在:', orderNo)
return NextResponse.text('FAIL', { status: 404 })
}
// 检查订单状态
if (order.status === 'paid') {
console.log('✅ 订单已支付,跳过处理:', orderNo)
return NextResponse.text('SUCCESS')
}
// 验证支付状态
// 易支付通常 trade_status = 'TRADE_SUCCESS' 表示支付成功
if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') {
// 更新订单状态
await db.updateOrderStatus(
orderNo,
'paid',
tradeNo,
paymentMethod,
JSON.stringify(params)
)
console.log('✅ 订单支付成功:', {
orderNo,
tradeNo,
amount: order.amount,
paymentMethod,
})
// TODO: 这里可以添加其他业务逻辑,比如:
// - 发送邮件通知
// - 更新用户权限
// - 记录日志等
return NextResponse.text('SUCCESS')
} else {
// 支付失败
await db.updateOrderStatus(
orderNo,
'failed',
tradeNo,
paymentMethod,
JSON.stringify(params)
)
console.log('❌ 订单支付失败:', {
orderNo,
tradeStatus,
params,
})
return NextResponse.text('SUCCESS') // 即使失败也要返回SUCCESS,避免重复通知
}
} catch (error) {
console.error('处理支付回调错误:', error)
return NextResponse.text('FAIL', { status: 500 })
}
}
// 也支持GET请求(某些支付平台可能使用GET)
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const params: Record<string, any> = {}
searchParams.forEach((value, key) => {
params[key] = value
})
console.log('📥 收到支付回调(GET):', params)
const epayKey = process.env.EPAY_KEY
if (!epayKey) {
console.error('❌ 易支付密钥未配置')
return NextResponse.text('FAIL', { status: 500 })
}
// 验证签名
if (!verifySign(params, epayKey)) {
console.error('❌ 签名验证失败:', params)
return NextResponse.text('FAIL', { status: 400 })
}
// 获取订单信息
const orderNo = params.out_trade_no
const tradeNo = params.trade_no
const tradeStatus = params.trade_status
const paymentMethod = params.type || params.payment_method
if (!orderNo) {
console.error('❌ 订单号缺失')
return NextResponse.text('FAIL', { status: 400 })
}
// 查找订单
const order = await db.findOrderByOrderNo(orderNo)
if (!order) {
console.error('❌ 订单不存在:', orderNo)
return NextResponse.text('FAIL', { status: 404 })
}
// 检查订单状态
if (order.status === 'paid') {
console.log('✅ 订单已支付,跳过处理:', orderNo)
return NextResponse.text('SUCCESS')
}
// 验证支付状态
if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') {
// 更新订单状态
await db.updateOrderStatus(
orderNo,
'paid',
tradeNo,
paymentMethod,
JSON.stringify(params)
)
console.log('✅ 订单支付成功:', {
orderNo,
tradeNo,
amount: order.amount,
paymentMethod,
})
return NextResponse.text('SUCCESS')
} else {
// 支付失败
await db.updateOrderStatus(
orderNo,
'failed',
tradeNo,
paymentMethod,
JSON.stringify(params)
)
console.log('❌ 订单支付失败:', {
orderNo,
tradeStatus,
params,
})
return NextResponse.text('SUCCESS')
}
} catch (error) {
console.error('处理支付回调错误:', error)
return NextResponse.text('FAIL', { status: 500 })
}
}

View File

@ -0,0 +1,153 @@
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
import { db } from '@/lib/db'
import crypto from 'crypto'
// 产品配置
const PRODUCTS = {
standard: {
name: 'FLNXI标准版',
price: 599,
},
deployment: {
name: '远程部署服务',
price: 299,
},
custom: {
name: '定制版咨询',
price: 0, // 需要沟通报价
},
}
// 生成订单号
function generateOrderNo(): string {
const timestamp = Date.now()
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0')
return `FLNXI${timestamp}${random}`
}
// 生成易支付签名
function generateSign(params: Record<string, any>, key: string): string {
// 移除空值和sign参数
const filtered: Record<string, any> = {}
Object.keys(params)
.sort()
.forEach((k) => {
if (params[k] !== '' && params[k] !== null && params[k] !== undefined && k !== 'sign') {
filtered[k] = params[k]
}
})
// 拼接字符串
const signStr = Object.keys(filtered)
.map((k) => `${k}=${filtered[k]}`)
.join('&')
const signStrWithKey = `${signStr}&key=${key}`
// MD5加密并转大写
return crypto.createHash('md5').update(signStrWithKey).digest('hex').toUpperCase()
}
export async function POST(request: NextRequest) {
try {
// 验证登录状态
const session = await getServerSession(authOptions)
if (!session || !session.user?.id) {
return NextResponse.json(
{ message: '请先登录' },
{ status: 401 }
)
}
const body = await request.json()
const { productType, paymentMethod = 'alipay' } = body
// 验证产品类型
if (!productType || !PRODUCTS[productType as keyof typeof PRODUCTS]) {
return NextResponse.json(
{ message: '无效的产品类型' },
{ status: 400 }
)
}
const product = PRODUCTS[productType as keyof typeof PRODUCTS]
// 定制版需要特殊处理
if (productType === 'custom') {
return NextResponse.json(
{ message: '定制版请联系客服咨询报价', redirect: '#contact' },
{ status: 200 }
)
}
// 检查易支付配置
const epayPid = process.env.EPAY_PID
const epayKey = process.env.EPAY_KEY
const epayApiUrl = process.env.EPAY_API_URL || 'https://your-epay-domain.com/submit.php'
if (!epayPid || !epayKey || !epayApiUrl) {
return NextResponse.json(
{ message: '支付系统未配置,请联系管理员' },
{ status: 500 }
)
}
// 生成订单号
const orderNo = generateOrderNo()
const userId = parseInt(session.user.id)
// 创建订单
const order = await db.createOrder(
orderNo,
userId,
productType,
product.name,
product.price
)
// 获取回调地址
const baseUrl = process.env.NEXTAUTH_URL || request.headers.get('origin') || 'http://localhost:23000'
const notifyUrl = `${baseUrl}/api/payment/callback`
const returnUrl = `${baseUrl}/payment/success?order_no=${orderNo}`
// 构建易支付参数
const epayParams = {
pid: epayPid,
type: paymentMethod, // alipay, wxpay, qqpay等
out_trade_no: orderNo,
notify_url: notifyUrl,
return_url: returnUrl,
name: product.name,
money: product.price.toFixed(2),
}
// 生成签名
const sign = generateSign(epayParams, epayKey)
epayParams.sign = sign
epayParams.sign_type = 'MD5'
console.log('✅ 创建支付订单:', {
orderNo,
userId,
product: product.name,
amount: product.price,
})
// 返回支付参数,前端提交表单到易支付
return NextResponse.json({
success: true,
orderNo,
orderId: order.id,
paymentUrl: epayApiUrl,
paymentParams: epayParams,
})
} catch (error) {
console.error('创建支付订单错误:', error)
return NextResponse.json(
{ message: '创建订单失败,请稍后重试' },
{ status: 500 }
)
}
}

View File

@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
import { db } from '@/lib/db'
export async function GET(request: NextRequest) {
try {
// 验证登录状态
const session = await getServerSession(authOptions)
if (!session || !session.user?.id) {
return NextResponse.json(
{ message: '请先登录' },
{ status: 401 }
)
}
const searchParams = request.nextUrl.searchParams
const orderNo = searchParams.get('order_no')
if (!orderNo) {
return NextResponse.json(
{ message: '订单号不能为空' },
{ status: 400 }
)
}
// 查找订单
const order = await db.findOrderByOrderNo(orderNo)
if (!order) {
return NextResponse.json(
{ message: '订单不存在' },
{ status: 404 }
)
}
// 验证订单是否属于当前用户
const userId = parseInt(session.user.id)
if (order.user_id !== userId) {
return NextResponse.json(
{ message: '无权访问此订单' },
{ status: 403 }
)
}
return NextResponse.json({
success: true,
order: {
id: order.id,
order_no: order.order_no,
product_type: order.product_type,
product_name: order.product_name,
amount: order.amount,
status: order.status,
payment_method: order.payment_method,
trade_no: order.trade_no,
created_at: order.created_at,
paid_at: order.paid_at,
},
})
} catch (error) {
console.error('查询订单状态错误:', error)
return NextResponse.json(
{ message: '查询失败,请稍后重试' },
{ status: 500 }
)
}
}

View File

@ -2,7 +2,7 @@
import { signIn } from 'next-auth/react'
import { useState } from 'react'
import { Mail, Lock, ArrowLeft, AlertCircle } from 'lucide-react'
import { Mail, Lock, ArrowLeft, AlertCircle, QrCode } from 'lucide-react'
import Link from 'next/link'
import Image from 'next/image'
@ -10,6 +10,7 @@ export default function SignInPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [wechatLoading, setWeChatLoading] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
@ -37,6 +38,19 @@ export default function SignInPage() {
}
}
const handleWeChatLogin = async () => {
setError('')
setWeChatLoading(true)
try {
await signIn('wechat', {
callbackUrl: '/',
})
} catch (err) {
setError('唤起微信登录失败,请稍后重试')
setWeChatLoading(false)
}
}
return (
<div className="min-h-screen bg-gradient-to-b from-apple-gray-50 to-white flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full">
@ -154,6 +168,27 @@ export default function SignInPage() {
</button>
</form>
{/* 第三方登录 */}
<div className="mt-6 space-y-3">
<div className="flex items-center space-x-3">
<div className="flex-1 h-px bg-apple-gray-100" />
<span className="text-xs text-apple-gray-400">或使用第三方登录</span>
<div className="flex-1 h-px bg-apple-gray-100" />
</div>
<button
type="button"
disabled={wechatLoading}
onClick={handleWeChatLogin}
className="w-full py-3 flex items-center justify-center space-x-3 border-2 border-apple-gray-200 rounded-xl hover:border-apple-blue hover:text-apple-blue transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<QrCode className="w-5 h-5" />
<span>{wechatLoading ? '正在拉起微信...' : '微信扫码登录'}</span>
</button>
<p className="text-xs text-apple-gray-500 text-center">
请确保已在微信开放平台配置回调域名:`/api/auth/callback/wechat`
</p>
</div>
{/* 底部说明 */}
<div className="mt-6 pt-6 border-t border-apple-gray-100 text-center text-sm text-apple-gray-600">
<p>

View File

@ -0,0 +1,191 @@
'use client'
import { useEffect, useState } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import { CheckCircle, XCircle, Loader2 } from 'lucide-react'
import Link from 'next/link'
import Image from 'next/image'
export default function PaymentSuccessPage() {
const searchParams = useSearchParams()
const router = useRouter()
const orderNo = searchParams.get('order_no')
const [orderStatus, setOrderStatus] = useState<'loading' | 'success' | 'failed' | 'pending'>('loading')
const [orderInfo, setOrderInfo] = useState<any>(null)
useEffect(() => {
if (!orderNo) {
setOrderStatus('failed')
return
}
// 查询订单状态
const checkOrderStatus = async () => {
try {
const response = await fetch(`/api/payment/status?order_no=${orderNo}`)
const data = await response.json()
if (data.success) {
setOrderInfo(data.order)
setOrderStatus(data.order.status === 'paid' ? 'success' : data.order.status === 'failed' ? 'failed' : 'pending')
} else {
setOrderStatus('failed')
}
} catch (error) {
console.error('查询订单状态错误:', error)
setOrderStatus('failed')
}
}
checkOrderStatus()
// 如果订单还在pending状态,每3秒轮询一次(最多轮询10次)
let pollCount = 0
const pollInterval = setInterval(() => {
if (pollCount >= 10) {
clearInterval(pollInterval)
return
}
pollCount++
checkOrderStatus()
}, 3000)
return () => clearInterval(pollInterval)
}, [orderNo])
return (
<div className="min-h-screen bg-gradient-to-b from-apple-gray-50 to-white flex items-center justify-center px-4 py-12">
<div className="max-w-md w-full">
{/* Logo */}
<div className="text-center mb-8">
<div className="flex justify-center mb-4">
<div className="relative w-16 h-16">
<Image
src="/images/logo.png"
alt="FLNXI Logo"
fill
className="object-contain"
priority
/>
</div>
</div>
</div>
{/* 支付结果卡片 */}
<div className="bg-white rounded-3xl apple-shadow-lg p-8">
{orderStatus === 'loading' && (
<div className="text-center py-8">
<Loader2 className="w-16 h-16 text-apple-blue animate-spin mx-auto mb-4" />
<h2 className="text-2xl font-bold text-apple-gray-900 mb-2">
正在确认支付结果...
</h2>
<p className="text-apple-gray-600">
请稍候,我们正在验证您的支付信息
</p>
</div>
)}
{orderStatus === 'success' && (
<div className="text-center py-8">
<div className="w-16 h-16 bg-gradient-to-r from-green-500 to-emerald-600 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-8 h-8 text-white" />
</div>
<h2 className="text-3xl font-bold text-apple-gray-900 mb-2">
支付成功!
</h2>
<p className="text-apple-gray-600 mb-6">
感谢您的购买,订单已确认
</p>
{orderInfo && (
<div className="bg-apple-gray-50 rounded-xl p-4 mb-6 text-left">
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-apple-gray-600">订单号:</span>
<span className="font-mono font-semibold">{orderInfo.order_no}</span>
</div>
<div className="flex justify-between">
<span className="text-apple-gray-600">商品名称:</span>
<span className="font-semibold">{orderInfo.product_name}</span>
</div>
<div className="flex justify-between">
<span className="text-apple-gray-600">支付金额:</span>
<span className="font-bold text-green-600">¥{orderInfo.amount}</span>
</div>
{orderInfo.trade_no && (
<div className="flex justify-between">
<span className="text-apple-gray-600">交易号:</span>
<span className="font-mono text-xs">{orderInfo.trade_no}</span>
</div>
)}
</div>
</div>
)}
<div className="space-y-3">
<Link
href="/"
className="block w-full py-3 bg-gradient-to-r from-apple-blue to-blue-600 text-white rounded-full font-semibold button-hover apple-shadow"
>
返回首页
</Link>
<p className="text-xs text-apple-gray-500">
我们将在1-3个工作日内为您开通服务
</p>
</div>
</div>
)}
{orderStatus === 'pending' && (
<div className="text-center py-8">
<Loader2 className="w-16 h-16 text-orange-500 animate-spin mx-auto mb-4" />
<h2 className="text-2xl font-bold text-apple-gray-900 mb-2">
支付处理中
</h2>
<p className="text-apple-gray-600 mb-6">
您的订单已提交,正在等待支付确认
</p>
{orderNo && (
<p className="text-xs text-apple-gray-500 font-mono mb-6">
订单号:{orderNo}
</p>
)}
<p className="text-sm text-apple-gray-500">
如果已完成支付,请稍候片刻,系统会自动更新订单状态
</p>
</div>
)}
{orderStatus === 'failed' && (
<div className="text-center py-8">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<XCircle className="w-8 h-8 text-red-600" />
</div>
<h2 className="text-2xl font-bold text-apple-gray-900 mb-2">
支付失败
</h2>
<p className="text-apple-gray-600 mb-6">
订单处理失败,请检查支付信息或联系客服
</p>
<div className="space-y-3">
<Link
href="/#pricing"
className="block w-full py-3 bg-gradient-to-r from-apple-blue to-blue-600 text-white rounded-full font-semibold button-hover apple-shadow"
>
重新购买
</Link>
<Link
href="/#contact"
className="block w-full py-3 border-2 border-apple-gray-200 text-apple-gray-700 rounded-full font-semibold hover:border-apple-blue hover:text-apple-blue transition-all"
>
联系客服
</Link>
</div>
</div>
)}
</div>
</div>
</div>
)
}

View File

@ -3,6 +3,7 @@
import { Check, Star, Sparkles, Zap, TrendingUp, AlertCircle } from 'lucide-react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
const freeFeatures = [
{ text: '✅ 永久免费使用,0成本体验', highlight: true },
@ -45,8 +46,9 @@ const customFeatures = [
export default function Pricing() {
const { data: session, status } = useSession()
const router = useRouter()
const [purchasing, setPurchasing] = useState<string | null>(null)
const handlePurchaseClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
const handlePurchaseClick = async (e: React.MouseEvent<HTMLAnchorElement>, productType: string) => {
e.preventDefault()
if (status === 'loading') {
@ -55,10 +57,72 @@ export default function Pricing() {
if (!session) {
// 未登录,跳转到登录页
router.push('/auth/signin?callbackUrl=' + encodeURIComponent('/#contact'))
} else {
// 已登录,跳转到联系部分
router.push('/auth/signin?callbackUrl=' + encodeURIComponent('/#pricing'))
return
}
// 定制版跳转到联系部分
if (productType === 'custom') {
window.location.href = '#contact'
return
}
// 创建支付订单
setPurchasing(productType)
try {
const response = await fetch('/api/payment/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productType,
paymentMethod: 'alipay', // 默认支付宝,可以后续添加选择
}),
})
const data = await response.json()
if (!response.ok) {
if (data.redirect) {
// 定制版等需要跳转的情况
window.location.href = data.redirect
return
}
alert(data.message || '创建订单失败,请稍后重试')
setPurchasing(null)
return
}
if (data.success && data.paymentUrl && data.paymentParams) {
// 创建表单并提交到易支付
const form = document.createElement('form')
form.method = 'POST'
form.action = data.paymentUrl
form.target = '_blank'
// 添加支付参数
Object.keys(data.paymentParams).forEach((key) => {
const input = document.createElement('input')
input.type = 'hidden'
input.name = key
input.value = data.paymentParams[key]
form.appendChild(input)
})
document.body.appendChild(form)
form.submit()
document.body.removeChild(form)
// 跳转到支付结果页面
setTimeout(() => {
router.push(`/payment/success?order_no=${data.orderNo}`)
}, 1000)
}
} catch (error) {
console.error('创建支付订单错误:', error)
alert('创建订单失败,请稍后重试')
setPurchasing(null)
}
}
@ -226,10 +290,24 @@ export default function Pricing() {
{/* CTA Button */}
<a
href="#contact"
onClick={handlePurchaseClick}
className="block w-full py-4 bg-gradient-to-r from-apple-blue to-blue-600 text-white text-center rounded-full font-bold text-lg button-hover apple-shadow-lg cursor-pointer"
onClick={(e) => handlePurchaseClick(e, 'standard')}
className="block w-full py-4 bg-gradient-to-r from-apple-blue to-blue-600 text-white text-center rounded-full font-bold text-lg button-hover apple-shadow-lg cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
{status === 'loading' ? '加载中...' : session ? '立即购买' : '登录后购买'}
{purchasing === 'standard' ? (
<span className="flex items-center justify-center">
<svg className="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
正在创建订单...
</span>
) : status === 'loading' ? (
'加载中...'
) : session ? (
'立即购买'
) : (
'登录后购买'
)}
</a>
<p className="text-center text-sm text-apple-gray-500 mt-4">
@ -277,7 +355,7 @@ export default function Pricing() {
{/* CTA Button */}
<a
href="#contact"
onClick={handlePurchaseClick}
onClick={(e) => handlePurchaseClick(e, 'custom')}
className="block w-full py-4 bg-gradient-to-r from-purple-500 to-pink-600 text-white text-center rounded-full font-bold text-lg button-hover apple-shadow-lg cursor-pointer"
>
{status === 'loading' ? '加载中...' : session ? '咨询定制方案' : '登录后咨询'}
@ -347,10 +425,10 @@ export default function Pricing() {
</div>
<a
href="#contact"
onClick={handlePurchaseClick}
className="px-6 py-2 bg-gradient-to-r from-orange-500 to-amber-600 text-white rounded-full font-semibold text-sm button-hover apple-shadow whitespace-nowrap"
onClick={(e) => handlePurchaseClick(e, 'deployment')}
className="px-6 py-2 bg-gradient-to-r from-orange-500 to-amber-600 text-white rounded-full font-semibold text-sm button-hover apple-shadow whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
>
立即购买
{purchasing === 'deployment' ? '处理中...' : '立即购买'}
</a>
</div>
</div>

View File

@ -30,6 +30,23 @@ export interface User {
updated_at: Date
}
// 订单接口
export interface Order {
id: number
order_no: string
user_id: number
product_type: string // 'standard' | 'deployment' | 'custom'
product_name: string
amount: number
status: string // 'pending' | 'paid' | 'failed' | 'cancelled'
payment_method?: string
trade_no?: string
callback_data?: string
created_at: Date
updated_at: Date
paid_at?: Date
}
// 数据库查询函数
export const db = {
// 根据邮箱查找用户
@ -107,5 +124,82 @@ export const db = {
)
return result.rows
},
// ========== 订单相关操作 ==========
// 创建订单
async createOrder(
orderNo: string,
userId: number,
productType: string,
productName: string,
amount: number
): Promise<Order> {
const result = await pool.query(
'INSERT INTO orders (order_no, user_id, product_type, product_name, amount, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) RETURNING *',
[orderNo, userId, productType, productName, amount, 'pending']
)
return result.rows[0]
},
// 根据订单号查找订单
async findOrderByOrderNo(orderNo: string): Promise<Order | null> {
const result = await pool.query(
'SELECT * FROM orders WHERE order_no = $1',
[orderNo]
)
return result.rows[0] || null
},
// 根据ID查找订单
async findOrderById(id: number): Promise<Order | null> {
const result = await pool.query(
'SELECT * FROM orders WHERE id = $1',
[id]
)
return result.rows[0] || null
},
// 更新订单状态
async updateOrderStatus(
orderNo: string,
status: string,
tradeNo?: string,
paymentMethod?: string,
callbackData?: string
): Promise<Order> {
const updates = ['status = $2', 'updated_at = NOW()']
const values: any[] = [orderNo, status]
let index = 3
if (tradeNo) {
updates.push(`trade_no = $${index++}`)
values.push(tradeNo)
}
if (paymentMethod) {
updates.push(`payment_method = $${index++}`)
values.push(paymentMethod)
}
if (callbackData) {
updates.push(`callback_data = $${index++}`)
values.push(callbackData)
}
if (status === 'paid') {
updates.push('paid_at = NOW()')
}
const query = `UPDATE orders SET ${updates.join(', ')} WHERE order_no = $1 RETURNING *`
const result = await pool.query(query, values)
return result.rows[0]
},
// 获取用户的订单列表
async getUserOrders(userId: number): Promise<Order[]> {
const result = await pool.query(
'SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC',
[userId]
)
return result.rows
},
}

2132
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
-- 创建订单表
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
order_no VARCHAR(64) UNIQUE NOT NULL,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
product_type VARCHAR(32) NOT NULL,
product_name VARCHAR(255) NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
payment_method VARCHAR(32),
trade_no VARCHAR(128),
callback_data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
paid_at TIMESTAMP
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_orders_order_no ON orders(order_no);
CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
CREATE INDEX IF NOT EXISTS idx_orders_created_at ON orders(created_at);
-- 添加注释
COMMENT ON TABLE orders IS '订单表';
COMMENT ON COLUMN orders.order_no IS '订单号';
COMMENT ON COLUMN orders.user_id IS '用户ID';
COMMENT ON COLUMN orders.product_type IS '产品类型:standard/deployment/custom';
COMMENT ON COLUMN orders.product_name IS '产品名称';
COMMENT ON COLUMN orders.amount IS '订单金额';
COMMENT ON COLUMN orders.status IS '订单状态:pending/paid/failed/cancelled';
COMMENT ON COLUMN orders.payment_method IS '支付方式';
COMMENT ON COLUMN orders.trade_no IS '第三方交易号';
COMMENT ON COLUMN orders.callback_data IS '回调数据(JSON格式)';

10
types/next-auth.d.ts vendored
View File

@ -8,8 +8,9 @@ declare module "next-auth" {
interface Session {
user: {
id: string
email: string
name: string
email?: string | null
name?: string | null
provider?: string
} & DefaultSession["user"]
}
@ -18,6 +19,7 @@ declare module "next-auth" {
*/
interface User extends DefaultUser {
id: string
provider?: string
}
}
@ -27,6 +29,10 @@ declare module "next-auth/jwt" {
*/
interface JWT {
id: string
provider?: string
picture?: string | null
email?: string | null
name?: string | null
}
}