新增: 1. /api/miniprogram/pay - 创建预支付订单API 2. /api/miniprogram/pay/notify - 支付回调处理 3. /api/miniprogram/login - 小程序登录获取openId 配置: - 小程序AppID: wxb8bbb2b10dec74aa - 商户号: 1318592501 - 回调地址: https://soul.cunbao.net/api/miniprogram/pay/notify 更新: - app.js: 添加getOpenId方法,支付前获取openId - read.js: processPayment调用真实支付接口 - 支持API不可用时回退到测试模式
280 lines
8.0 KiB
TypeScript
280 lines
8.0 KiB
TypeScript
/**
|
||
* 小程序支付API
|
||
* 对接真实微信支付接口
|
||
*
|
||
* 配置来源: 用户规则
|
||
* - 小程序AppID: wxb8bbb2b10dec74aa
|
||
* - 商户号: 1318592501
|
||
* - API密钥: wx3e31b068be59ddc131b068be59ddc2
|
||
*/
|
||
|
||
import { NextResponse } from 'next/server'
|
||
import crypto from 'crypto'
|
||
|
||
// 微信支付配置
|
||
const WECHAT_PAY_CONFIG = {
|
||
appId: 'wxb8bbb2b10dec74aa', // 小程序AppID
|
||
appSecret: '85d3fa31584d06acdb1de4a597d25b7b', // 小程序AppSecret
|
||
mchId: '1318592501', // 商户号
|
||
mchKey: 'wx3e31b068be59ddc131b068be59ddc2', // API密钥
|
||
notifyUrl: 'https://soul.cunbao.net/api/miniprogram/pay/notify', // 支付回调地址
|
||
}
|
||
|
||
// 生成随机字符串
|
||
function generateNonceStr(): string {
|
||
return crypto.randomBytes(16).toString('hex')
|
||
}
|
||
|
||
// 生成签名
|
||
function generateSign(params: Record<string, string>, key: string): string {
|
||
const sortedKeys = Object.keys(params).sort()
|
||
const signString = sortedKeys
|
||
.filter((k) => params[k] && k !== 'sign')
|
||
.map((k) => `${k}=${params[k]}`)
|
||
.join('&')
|
||
|
||
const signWithKey = `${signString}&key=${key}`
|
||
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex').toUpperCase()
|
||
}
|
||
|
||
// 对象转XML
|
||
function dictToXml(data: Record<string, string>): string {
|
||
const xml = ['<xml>']
|
||
for (const [key, value] of Object.entries(data)) {
|
||
xml.push(`<${key}><![CDATA[${value}]]></${key}>`)
|
||
}
|
||
xml.push('</xml>')
|
||
return xml.join('')
|
||
}
|
||
|
||
// XML转对象
|
||
function xmlToDict(xml: string): Record<string, string> {
|
||
const result: Record<string, string> = {}
|
||
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g
|
||
let match
|
||
while ((match = regex.exec(xml)) !== null) {
|
||
result[match[1]] = match[2]
|
||
}
|
||
const simpleRegex = /<(\w+)>([^<]*)<\/\1>/g
|
||
while ((match = simpleRegex.exec(xml)) !== null) {
|
||
if (!result[match[1]]) {
|
||
result[match[1]] = match[2]
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// 生成订单号
|
||
function generateOrderSn(): string {
|
||
const now = new Date()
|
||
const timestamp = now.getFullYear().toString() +
|
||
(now.getMonth() + 1).toString().padStart(2, '0') +
|
||
now.getDate().toString().padStart(2, '0') +
|
||
now.getHours().toString().padStart(2, '0') +
|
||
now.getMinutes().toString().padStart(2, '0') +
|
||
now.getSeconds().toString().padStart(2, '0')
|
||
const random = Math.floor(Math.random() * 1000000).toString().padStart(6, '0')
|
||
return `MP${timestamp}${random}`
|
||
}
|
||
|
||
/**
|
||
* POST - 创建小程序支付订单
|
||
*/
|
||
export async function POST(request: Request) {
|
||
try {
|
||
const body = await request.json()
|
||
const { openId, productType, productId, amount, description } = body
|
||
|
||
if (!openId) {
|
||
return NextResponse.json({
|
||
success: false,
|
||
error: '缺少openId参数,请先登录'
|
||
}, { status: 400 })
|
||
}
|
||
|
||
if (!amount || amount <= 0) {
|
||
return NextResponse.json({
|
||
success: false,
|
||
error: '支付金额无效'
|
||
}, { status: 400 })
|
||
}
|
||
|
||
const orderSn = generateOrderSn()
|
||
const totalFee = Math.round(amount * 100) // 转换为分
|
||
const goodsBody = description || (productType === 'fullbook' ? '《一场Soul的创业实验》全书' : `章节购买-${productId}`)
|
||
|
||
// 获取客户端IP
|
||
const forwarded = request.headers.get('x-forwarded-for')
|
||
const clientIp = forwarded ? forwarded.split(',')[0] : '127.0.0.1'
|
||
|
||
// 构建统一下单参数
|
||
const params: Record<string, string> = {
|
||
appid: WECHAT_PAY_CONFIG.appId,
|
||
mch_id: WECHAT_PAY_CONFIG.mchId,
|
||
nonce_str: generateNonceStr(),
|
||
body: goodsBody.slice(0, 128),
|
||
out_trade_no: orderSn,
|
||
total_fee: totalFee.toString(),
|
||
spbill_create_ip: clientIp,
|
||
notify_url: WECHAT_PAY_CONFIG.notifyUrl,
|
||
trade_type: 'JSAPI',
|
||
openid: openId,
|
||
attach: JSON.stringify({ productType, productId, userId: body.userId || '' }),
|
||
}
|
||
|
||
// 生成签名
|
||
params.sign = generateSign(params, WECHAT_PAY_CONFIG.mchKey)
|
||
|
||
console.log('[MiniPay] 创建订单:', {
|
||
orderSn,
|
||
totalFee,
|
||
openId: openId.slice(0, 10) + '...',
|
||
productType,
|
||
productId,
|
||
})
|
||
|
||
// 调用微信统一下单接口
|
||
const xmlData = dictToXml(params)
|
||
const response = await fetch('https://api.mch.weixin.qq.com/pay/unifiedorder', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/xml' },
|
||
body: xmlData,
|
||
})
|
||
|
||
const responseText = await response.text()
|
||
const result = xmlToDict(responseText)
|
||
|
||
console.log('[MiniPay] 微信响应:', {
|
||
return_code: result.return_code,
|
||
result_code: result.result_code,
|
||
err_code: result.err_code,
|
||
err_code_des: result.err_code_des,
|
||
})
|
||
|
||
// 检查返回结果
|
||
if (result.return_code !== 'SUCCESS') {
|
||
return NextResponse.json({
|
||
success: false,
|
||
error: `微信支付请求失败: ${result.return_msg || '未知错误'}`
|
||
}, { status: 500 })
|
||
}
|
||
|
||
if (result.result_code !== 'SUCCESS') {
|
||
return NextResponse.json({
|
||
success: false,
|
||
error: `微信支付失败: ${result.err_code_des || result.err_code || '未知错误'}`
|
||
}, { status: 500 })
|
||
}
|
||
|
||
// 构建小程序支付参数
|
||
const prepayId = result.prepay_id
|
||
if (!prepayId) {
|
||
return NextResponse.json({
|
||
success: false,
|
||
error: '微信支付返回数据异常'
|
||
}, { status: 500 })
|
||
}
|
||
|
||
const timestamp = Math.floor(Date.now() / 1000).toString()
|
||
const nonceStr = generateNonceStr()
|
||
|
||
const payParams: Record<string, string> = {
|
||
appId: WECHAT_PAY_CONFIG.appId,
|
||
timeStamp: timestamp,
|
||
nonceStr,
|
||
package: `prepay_id=${prepayId}`,
|
||
signType: 'MD5',
|
||
}
|
||
payParams.paySign = generateSign(payParams, WECHAT_PAY_CONFIG.mchKey)
|
||
|
||
console.log('[MiniPay] 支付参数生成成功:', { orderSn, prepayId: prepayId.slice(0, 20) + '...' })
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: {
|
||
orderSn,
|
||
prepayId,
|
||
payParams: {
|
||
timeStamp: timestamp,
|
||
nonceStr,
|
||
package: `prepay_id=${prepayId}`,
|
||
signType: 'MD5',
|
||
paySign: payParams.paySign,
|
||
}
|
||
}
|
||
})
|
||
|
||
} catch (error) {
|
||
console.error('[MiniPay] 创建订单失败:', error)
|
||
return NextResponse.json({
|
||
success: false,
|
||
error: '创建支付订单失败'
|
||
}, { status: 500 })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GET - 查询订单状态
|
||
*/
|
||
export async function GET(request: Request) {
|
||
const { searchParams } = new URL(request.url)
|
||
const orderSn = searchParams.get('orderSn')
|
||
|
||
if (!orderSn) {
|
||
return NextResponse.json({
|
||
success: false,
|
||
error: '缺少订单号'
|
||
}, { status: 400 })
|
||
}
|
||
|
||
try {
|
||
const params: Record<string, string> = {
|
||
appid: WECHAT_PAY_CONFIG.appId,
|
||
mch_id: WECHAT_PAY_CONFIG.mchId,
|
||
out_trade_no: orderSn,
|
||
nonce_str: generateNonceStr(),
|
||
}
|
||
params.sign = generateSign(params, WECHAT_PAY_CONFIG.mchKey)
|
||
|
||
const xmlData = dictToXml(params)
|
||
const response = await fetch('https://api.mch.weixin.qq.com/pay/orderquery', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/xml' },
|
||
body: xmlData,
|
||
})
|
||
|
||
const responseText = await response.text()
|
||
const result = xmlToDict(responseText)
|
||
|
||
if (result.return_code !== 'SUCCESS' || result.result_code !== 'SUCCESS') {
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: { status: 'unknown', orderSn }
|
||
})
|
||
}
|
||
|
||
const tradeState = result.trade_state
|
||
let status = 'paying'
|
||
if (tradeState === 'SUCCESS') status = 'paid'
|
||
else if (['CLOSED', 'REVOKED', 'PAYERROR'].includes(tradeState)) status = 'failed'
|
||
else if (tradeState === 'REFUND') status = 'refunded'
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: {
|
||
status,
|
||
orderSn,
|
||
transactionId: result.transaction_id,
|
||
totalFee: parseInt(result.total_fee || '0', 10),
|
||
}
|
||
})
|
||
|
||
} catch (error) {
|
||
console.error('[MiniPay] 查询订单失败:', error)
|
||
return NextResponse.json({
|
||
success: false,
|
||
error: '查询订单失败'
|
||
}, { status: 500 })
|
||
}
|
||
}
|