257 lines
9.1 KiB
TypeScript
257 lines
9.1 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getServerSession } from 'next-auth/next'
|
||
import { authOptions } from '@/lib/authOptions'
|
||
import { db } from '@/lib/db'
|
||
import crypto from 'crypto'
|
||
|
||
// 产品配置
|
||
const PRODUCTS = {
|
||
standard: {
|
||
name: 'FLNXI标准版',
|
||
price: 599,
|
||
},
|
||
deployment: {
|
||
name: '远程部署服务',
|
||
price: 299,
|
||
},
|
||
custom: {
|
||
name: '定制版咨询',
|
||
price: 0, // 需要沟通报价
|
||
},
|
||
test: {
|
||
name: '支付测试',
|
||
price: 0.1, // 测试用,金额0.1元
|
||
},
|
||
}
|
||
|
||
// 生成订单号
|
||
function generateOrderNo(): string {
|
||
const timestamp = Date.now()
|
||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0')
|
||
return `FLNXI${timestamp}${random}`
|
||
}
|
||
|
||
// 生成嘟噜支付MD5签名
|
||
function generateSign(params: Record<string, any>, key: string): string {
|
||
// 1. 获取所有非空请求参数,剔除sign和sign_type字段
|
||
const filtered: Record<string, string> = {}
|
||
const sortedKeys = Object.keys(params).sort() // 按照ASCII码递增排序
|
||
|
||
sortedKeys.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'
|
||
) {
|
||
// 确保所有值都是字符串(不trim,保持原始值)
|
||
filtered[k] = String(params[k])
|
||
}
|
||
})
|
||
|
||
// 2. 将排序后的参数组合成"参数=参数值"的格式,用&连接
|
||
// 注意:签名时使用原始参数值(不编码),因为表单提交时浏览器会自动处理编码
|
||
// 但支付平台验证签名时使用的是收到的原始值(浏览器解码后的值)
|
||
const sortedFilteredKeys = Object.keys(filtered).sort()
|
||
|
||
// 使用原始参数值(不编码)进行签名
|
||
const signStr = sortedFilteredKeys
|
||
.map((k) => `${k}=${filtered[k]}`)
|
||
.join('&')
|
||
|
||
// 3. 在末尾加上密钥(易支付常见方式:直接拼接密钥,没有&key=前缀)
|
||
const signStrWithKey = `${signStr}${key}`
|
||
|
||
// 对比:如果使用 &key=密钥 的方式
|
||
const signStrWithKeyPrefix = `${signStr}&key=${key}`
|
||
|
||
|
||
// 详细调试日志
|
||
console.log('\n========== 签名参数调试信息 ==========')
|
||
console.log('📋 原始参数:')
|
||
Object.keys(params).sort().forEach(k => {
|
||
console.log(` ${k} = ${params[k]} (类型: ${typeof params[k]})`)
|
||
})
|
||
|
||
console.log('\n✅ 参与签名的参数(已过滤和排序):')
|
||
sortedFilteredKeys.forEach((k, index) => {
|
||
const encodedValue = encodeURIComponent(filtered[k])
|
||
console.log(` ${index + 1}. ${k} = "${filtered[k]}"`)
|
||
if (encodedValue !== filtered[k]) {
|
||
console.log(` (URL编码后: "${encodedValue}")`)
|
||
}
|
||
})
|
||
|
||
console.log('\n🔗 待签名字符串(参数值不进行URL编码):')
|
||
console.log(` ${signStr}`)
|
||
|
||
console.log('\n🔑 密钥信息:')
|
||
console.log(` 长度: ${key.length} 字符`)
|
||
console.log(` 前3个字符: ${key.substring(0, 3)}...`)
|
||
console.log(` 后3个字符: ...${key.substring(key.length - 3)}`)
|
||
|
||
console.log('\n🔐 完整签名字符串(直接拼接密钥,无&key=前缀):')
|
||
console.log(` ${signStrWithKey}`)
|
||
console.log(` 长度: ${signStrWithKey.length} 字符`)
|
||
console.log(' 格式:sign = md5(a=b&c=d&e=f + KEY)')
|
||
|
||
// 4. MD5加密
|
||
// 根据官方文档:sign = md5(a=b&c=d&e=f + KEY),MD5结果为小写
|
||
|
||
// 方式A:直接拼接密钥(官方文档标准方式)
|
||
const signatureA = crypto.createHash('md5').update(signStrWithKey, 'utf8').digest('hex') // 小写
|
||
|
||
// 方式B:使用&key=前缀(对比)
|
||
const signatureB = crypto.createHash('md5').update(signStrWithKeyPrefix, 'utf8').digest('hex') // 小写
|
||
|
||
// 默认使用方式A(直接拼接密钥,官方文档标准方式)
|
||
const signature = signatureA
|
||
|
||
console.log('\n📝 生成的签名(根据官方文档):')
|
||
console.log(' [方式A - 直接拼接密钥(官方文档标准方式)] (当前使用):')
|
||
console.log(` 签名字符串: ${signStrWithKey}`)
|
||
console.log(` 签名(小写): ${signatureA}`)
|
||
console.log(' [方式B - 使用&key=前缀(对比)]:')
|
||
console.log(` 签名字符串: ${signStrWithKeyPrefix}`)
|
||
console.log(` 签名(小写): ${signatureB}`)
|
||
console.log('\n✅ 当前使用方式A(直接拼接密钥,MD5结果为小写)')
|
||
console.log(' 根据官方文档:sign = md5(a=b&c=d&e=f + KEY),MD5结果为小写')
|
||
console.log('========================================\n')
|
||
|
||
return signature
|
||
}
|
||
|
||
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 // MD5密钥
|
||
const epayApiUrl = process.env.EPAY_API_URL || 'https://www.dulupay.com/api/pay'
|
||
|
||
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: Record<string, string> = {
|
||
pid: String(epayPid), // 商户ID
|
||
type: String(paymentMethod), // 支付方式:alipay, wxpay, qqpay等
|
||
out_trade_no: orderNo, // 商户订单号
|
||
notify_url: notifyUrl, // 异步通知地址
|
||
return_url: returnUrl, // 同步跳转地址
|
||
name: product.name, // 商品名称
|
||
money: product.price.toFixed(2), // 金额(保留2位小数)
|
||
}
|
||
|
||
// 调试:输出参数名对比
|
||
console.log('\n📝 参数名检查(易支付标准参数名,根据官方文档):')
|
||
console.log(' pid = 商户ID(必填)')
|
||
console.log(' type = 支付方式(可选)')
|
||
console.log(' out_trade_no = 商户订单号(必填)')
|
||
console.log(' notify_url = 异步通知地址(必填)')
|
||
console.log(' return_url = 同步跳转地址(必填)')
|
||
console.log(' name = 商品名称(必填)')
|
||
console.log(' money = 金额(必填,保留2位小数)')
|
||
console.log(' 注意:sign、sign_type、和空值不参与签名\n')
|
||
|
||
// 生成MD5签名
|
||
const sign = generateSign(epayParams, epayKey)
|
||
epayParams.sign = sign
|
||
epayParams.sign_type = 'MD5' // 嘟噜支付使用MD5签名
|
||
|
||
console.log('✅ 创建支付订单:', {
|
||
orderNo,
|
||
userId,
|
||
product: product.name,
|
||
amount: product.price,
|
||
})
|
||
console.log('📋 支付参数:', epayParams)
|
||
|
||
// 输出完整的测试信息,方便对比
|
||
console.log('\n🔍 完整测试信息(可用于对比Go项目):')
|
||
console.log('参数列表(按ASCII排序):')
|
||
Object.keys(epayParams).sort().forEach(k => {
|
||
console.log(` ${k} = "${epayParams[k]}"`)
|
||
})
|
||
console.log('\n建议:')
|
||
console.log('1. 查看Go项目实际发送给支付平台的参数(网络抓包或日志)')
|
||
console.log('2. 对比参数名是否一致')
|
||
console.log('3. 对比参数值是否一致')
|
||
console.log('4. 查看Go项目实际生成的签名')
|
||
console.log('5. 如果Go项目能正常工作,请提供Go项目的实际参数和签名用于对比')
|
||
console.log('6. 或联系嘟噜支付技术支持,获取签名示例代码\n')
|
||
|
||
// 返回支付参数,前端提交表单到嘟噜支付
|
||
return NextResponse.json({
|
||
success: true,
|
||
orderNo,
|
||
orderId: order.id,
|
||
paymentUrl: epayApiUrl,
|
||
paymentParams: epayParams,
|
||
})
|
||
} catch (error) {
|
||
console.error('创建支付订单错误:', error)
|
||
return NextResponse.json(
|
||
{ message: '创建订单失败,请稍后重试' },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|
||
|