Files
soul-yongping/app/api/miniprogram/pay/route.ts
卡若 0a5d470fef feat: 小程序真实微信支付功能
新增:
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不可用时回退到测试模式
2026-01-23 05:44:21 +08:00

280 lines
8.0 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.

/**
* 小程序支付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 })
}
}