362 lines
10 KiB
TypeScript
362 lines
10 KiB
TypeScript
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<string, any>, key: string): boolean {
|
||
const sign = params.sign
|
||
if (!sign) return false
|
||
|
||
// 1. 获取所有非空请求参数,剔除sign和sign_type字段
|
||
const filtered: Record<string, any> = {}
|
||
Object.keys(params)
|
||
.sort() // 按照ASCII码递增排序
|
||
.forEach((k) => {
|
||
// 排除空值、数组、字节类型参数,以及sign和sign_type字段
|
||
if (
|
||
k !== 'sign' &&
|
||
k !== 'sign_type' &&
|
||
params[k] !== '' &&
|
||
params[k] !== null &&
|
||
params[k] !== undefined &&
|
||
!Array.isArray(params[k]) &&
|
||
typeof params[k] !== 'object'
|
||
) {
|
||
filtered[k] = params[k]
|
||
}
|
||
})
|
||
|
||
// 2. 将排序后的参数组合成"参数=参数值"的格式,用&连接
|
||
// 注意:参数值不要进行URL编码(根据官方文档)
|
||
const signStr = Object.keys(filtered)
|
||
.map((k) => `${k}=${filtered[k]}`)
|
||
.join('&')
|
||
|
||
// 3. 直接拼接密钥(无&key=前缀)
|
||
// 根据官方文档:sign = md5(a=b&c=d&e=f + KEY)
|
||
const signStrWithKey = `${signStr}${key}`
|
||
|
||
// 4. MD5加密(结果为小写,根据官方文档)
|
||
const calculatedSign = crypto.createHash('md5').update(signStrWithKey, 'utf8').digest('hex')
|
||
|
||
return calculatedSign === sign.toLowerCase()
|
||
}
|
||
|
||
function parseCallbackData(raw?: string | null): Record<string, any> {
|
||
if (!raw) return {}
|
||
try {
|
||
return JSON.parse(raw)
|
||
} catch (e) {
|
||
return { raw }
|
||
}
|
||
}
|
||
|
||
function mergeCallbackData(raw: string | null | undefined, patch: Record<string, any>) {
|
||
const base = parseCallbackData(raw)
|
||
return JSON.stringify({ ...base, ...patch })
|
||
}
|
||
|
||
// 映射本地商品类型到许可证类型(按文档:STANDARD / PRO)
|
||
const productPlanMap: Record<string, 'STANDARD' | 'PRO'> = {
|
||
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<string, any> = {
|
||
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()
|
||
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 // MD5密钥
|
||
if (!epayKey) {
|
||
console.error('❌ 嘟噜支付MD5密钥未配置')
|
||
return new NextResponse('fail', { status: 500 })
|
||
}
|
||
|
||
// 验证签名
|
||
if (!verifySign(params, epayKey)) {
|
||
console.error('❌ 签名验证失败:', params)
|
||
return new NextResponse('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 new NextResponse('fail', { status: 400 })
|
||
}
|
||
|
||
// 查找订单
|
||
const order = await db.findOrderByOrderNo(orderNo)
|
||
if (!order) {
|
||
console.error('❌ 订单不存在:', orderNo)
|
||
return new NextResponse('fail', { status: 404 })
|
||
}
|
||
|
||
// 检查订单状态
|
||
if (order.status === 'paid') {
|
||
console.log('✅ 订单已支付,跳过处理:', orderNo)
|
||
return new NextResponse('success')
|
||
}
|
||
|
||
// 验证支付状态
|
||
// 易支付通常 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, mergedMeta)
|
||
|
||
console.log('✅ 订单支付成功:', {
|
||
orderNo,
|
||
tradeNo,
|
||
amount: order.amount,
|
||
paymentMethod,
|
||
licenseGenerated: !!licenseData,
|
||
})
|
||
|
||
// TODO: 这里可以添加其他业务逻辑,比如:
|
||
// - 发送邮件通知
|
||
// - 更新用户权限
|
||
// - 记录日志等
|
||
|
||
return new NextResponse('success')
|
||
} else {
|
||
// 支付失败
|
||
await db.updateOrderStatus(
|
||
orderNo,
|
||
'failed',
|
||
tradeNo,
|
||
paymentMethod,
|
||
JSON.stringify(params)
|
||
)
|
||
|
||
console.log('❌ 订单支付失败:', {
|
||
orderNo,
|
||
tradeStatus,
|
||
params,
|
||
})
|
||
|
||
return new NextResponse('success') // 即使失败也要返回SUCCESS,避免重复通知
|
||
}
|
||
} catch (error) {
|
||
console.error('处理支付回调错误:', error)
|
||
return new NextResponse('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 // MD5密钥
|
||
if (!epayKey) {
|
||
console.error('❌ 嘟噜支付MD5密钥未配置')
|
||
return new NextResponse('fail', { status: 500 })
|
||
}
|
||
|
||
// 验证签名
|
||
if (!verifySign(params, epayKey)) {
|
||
console.error('❌ 签名验证失败:', params)
|
||
return new NextResponse('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 new NextResponse('fail', { status: 400 })
|
||
}
|
||
|
||
// 查找订单
|
||
const order = await db.findOrderByOrderNo(orderNo)
|
||
if (!order) {
|
||
console.error('❌ 订单不存在:', orderNo)
|
||
return new NextResponse('fail', { status: 404 })
|
||
}
|
||
|
||
// 检查订单状态
|
||
if (order.status === 'paid') {
|
||
console.log('✅ 订单已支付,跳过处理:', orderNo)
|
||
return new NextResponse('success')
|
||
}
|
||
|
||
// 验证支付状态
|
||
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, mergedMeta)
|
||
|
||
console.log('✅ 订单支付成功:', {
|
||
orderNo,
|
||
tradeNo,
|
||
amount: order.amount,
|
||
paymentMethod,
|
||
})
|
||
|
||
return new NextResponse('success')
|
||
} else {
|
||
// 支付失败
|
||
await db.updateOrderStatus(
|
||
orderNo,
|
||
'failed',
|
||
tradeNo,
|
||
paymentMethod,
|
||
JSON.stringify(params)
|
||
)
|
||
|
||
console.log('❌ 订单支付失败:', {
|
||
orderNo,
|
||
tradeStatus,
|
||
params,
|
||
})
|
||
|
||
return new NextResponse('success')
|
||
}
|
||
} catch (error) {
|
||
console.error('处理支付回调错误:', error)
|
||
return new NextResponse('FAIL', { status: 500 })
|
||
}
|
||
}
|
||
|