2025-12-24 16:42:58 +08:00

362 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 })
}
}