2025-12-24 14:41:11 +08:00

257 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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