文章目录
前言
本文详细讲解了微信小程序内接入微信支付的全过程,包括配置准备、后端统一下单、小程序调起支付、回调处理、常见问题排查与工程化封装。结合实际项目开发经验,避免踩坑,助力快速上线高可用微信支付功能。
一、微信支付开发前的准备工作
在动手开发之前,请确保以下准备工作完成:
项目 | 说明 |
---|---|
小程序 AppID | 微信公众平台获取 |
商户号 MchID | 注册微信商户平台(https://pay.weixin.qq.com) |
API密钥 | 商户平台账户中心 ➔ API安全配置 |
小程序与商户号绑定 | 商户平台 ➔ 产品中心 ➔ APPID绑定 |
HTTPS 服务端 | 后端必须部署 HTTPS(必需) |
OpenID | 小程序用户的唯一标识,用于支付关联 |
注意事项:
• 商户号必须开通【JSAPI支付】产品;
• 小程序与商户号必须绑定关系并审核通过;
• 后端服务器必须支持 HTTPS。
二、微信支付整体流程概览
理解整体流程至关重要:
用户点击支付
↓
小程序请求后端生成订单(统一下单)
↓
后端向微信服务器请求 prepay_id
↓
后端返回支付参数
↓
小程序调起 wx.requestPayment
↓
支付成功或失败
↓
微信服务器异步通知后端(回调)
↓
后端确认并更新订单状态
小程序端只负责调起支付,统一下单、生成签名、支付回调,必须由后端完成!
三、后端开发:统一下单接口实现
3.1 调用微信统一下单 API(服务端)
微信官方接口文档:JSAPI下单API
接口地址(v2版):https://api.mch.weixin.qq.com/pay/unifiedorder
请求方式:POST
数据格式:XML
3.2 统一下单 Node.js示例
// pay.ts —— 微信统一下单模块
import axios from 'axios';
import * as crypto from 'crypto';
import * as xml2js from 'xml2js';
import { createSign } from '@/utils/sign';
const config = {
appId: '你的AppID',
mchId: '你的商户号',
apiKey: '你的API密钥',
notifyUrl: '你的支付回调地址',
};
export async function unifiedOrder(params) {
const nonceStr = crypto.randomBytes(16).toString('hex');
const requestParams = {
appid: config.appId,
mch_id: config.mchId,
nonce_str: nonceStr,
body: params.body,
out_trade_no: params.outTradeNo,
total_fee: params.totalFee,
spbill_create_ip: params.spbillCreateIp,
notify_url: config.notifyUrl,
trade_type: 'JSAPI',
openid: params.openid,
};
requestParams['sign'] = createSign(requestParams, config.apiKey);
const builder = new xml2js.Builder({ rootName: 'xml', headless: true });
const xmlData = builder.buildObject(requestParams);
const res = await axios.post('https://api.mch.weixin.qq.com/pay/unifiedorder', xmlData, {
headers: { 'Content-Type': 'text/xml' },
});
const parsed = await xml2js.parseStringPromise(res.data, { explicitArray: false });
if (parsed.xml.return_code !== 'SUCCESS' || parsed.xml.result_code !== 'SUCCESS') {
throw new Error(parsed.xml.return_msg || parsed.xml.err_code_des);
}
return parsed.xml.prepay_id;
}
四、小程序端开发:调起支付流程
小程序端代码示例:
// pages/pay/index.ts
async function requestPay(payData) {
try {
await wx.requestPayment({
timeStamp: payData.timeStamp,
nonceStr: payData.nonceStr,
package: payData.package,
signType: payData.signType,
paySign: payData.paySign,
success(res) {
console.log('支付成功', res);
wx.showToast({ title: '支付成功', icon: 'success' });
},
fail(err) {
console.error('支付失败', err);
wx.showToast({ title: '支付失败', icon: 'none' });
},
});
} catch (error) {
console.error('调起支付异常', error);
}
}
注意:必须由用户点击行为触发 wx.requestPayment,否则调用失败!
五、后端开发:处理微信支付回调
// notify.ts
import * as Koa from 'koa';
import * as xml2js from 'xml2js';
import { createSign } from '@/utils/sign';
export async function handlePaymentNotify(xmlString: string, apiKey: string) {
const parsed = await xml2js.parseStringPromise(xmlString, { explicitArray: false });
const data = parsed.xml;
const receivedSign = data.sign;
const { sign, ...dataWithoutSign } = data;
const validSign = createSign(dataWithoutSign, apiKey);
if (receivedSign !== validSign) {
throw new Error('签名校验失败');
}
if (data.return_code === 'SUCCESS' && data.result_code === 'SUCCESS') {
return {
orderNo: data.out_trade_no,
transactionId: data.transaction_id,
totalFee: parseInt(data.total_fee, 10),
openid: data.openid,
};
} else {
throw new Error('微信返回支付失败');
}
}
六、小程序支付常见问题与排查指南
问题 | 原因 | 排查建议 |
---|---|---|
wx.requestPayment失败 | 非点击触发 | 保证是按钮点击触发 |
prepay_id错误 | 商户号与小程序未绑定 | 确认商户后台关联 |
签名校验失败 | 参数顺序错误或缺失字段 | 使用统一签名生成方法 |
回调收不到 | notify_url 错误或服务器异常 | 检查 HTTPS,服务器日志 |
七、工程化封装与最佳实践
为了保证工程规范性,推荐模块化封装支付功能。
目录结构示例
/services/pay/
├── unifiedOrder.ts // 后端统一下单,生成 prepay_id
├── generateParams.ts // 后端生成小程序支付参数
├── notifyHandler.ts // 后端处理微信回调通知
/utils/
├── sign.ts // 签名生成与验证工具
/pages/pay/index.ts // 小程序端调起支付页面示例
/utils/sign.ts —— 签名工具模块
封装创建签名的方法,供统一下单、回调校验复用。
// utils/sign.ts
import * as crypto from 'crypto';
/**
* 创建签名
* @param params 参与签名的参数对象
* @param apiKey 商户平台设置的 API 密钥
*/
export function createSign(params: Record<string, any>, apiKey: string): string {
const sortedString = Object.keys(params)
.filter(k => params[k] !== undefined && params[k] !== '')
.sort()
.map(k => `${k}=${params[k]}`)
.join('&') + `&key=${apiKey}`;
return crypto.createHash('md5').update(sortedString, 'utf8').digest('hex').toUpperCase();
}
/services/pay/unifiedOrder.ts —— 统一下单接口封装
// services/pay/unifiedOrder.ts
import axios from 'axios';
import * as xml2js from 'xml2js';
import { createSign } from '@/utils/sign';
const config = {
appId: '你的AppID',
mchId: '你的商户号',
apiKey: '你的API密钥',
notifyUrl: '你的回调地址',
};
/**
* 微信统一下单
*/
export async function unifiedOrder({
openid,
body,
outTradeNo,
totalFee,
spbillCreateIp,
}: {
openid: string;
body: string;
outTradeNo: string;
totalFee: number;
spbillCreateIp: string;
}): Promise<string> {
const nonceStr = crypto.randomBytes(16).toString('hex');
const params = {
appid: config.appId,
mch_id: config.mchId,
nonce_str: nonceStr,
body,
out_trade_no: outTradeNo,
total_fee: totalFee,
spbill_create_ip: spbillCreateIp,
notify_url: config.notifyUrl,
trade_type: 'JSAPI',
openid,
};
params['sign'] = createSign(params, config.apiKey);
const builder = new xml2js.Builder({ rootName: 'xml', headless: true });
const xmlData = builder.buildObject(params);
const res = await axios.post('https://api.mch.weixin.qq.com/pay/unifiedorder', xmlData, {
headers: { 'Content-Type': 'text/xml' },
});
const result = await xml2js.parseStringPromise(res.data, { explicitArray: false });
const resData = result.xml;
if (resData.return_code !== 'SUCCESS' || resData.result_code !== 'SUCCESS') {
throw new Error(resData.return_msg || resData.err_code_des);
}
return resData.prepay_id;
}
/services/pay/generateParams.ts —— 小程序支付参数生成
// services/pay/generateParams.ts
import { createSign } from '@/utils/sign';
import * as crypto from 'crypto';
const config = {
appId: '你的AppID',
apiKey: '你的API密钥',
};
/**
* 根据 prepay_id 生成小程序端支付参数
*/
export function generateMiniPayParams(prepayId: string) {
const timeStamp = Math.floor(Date.now() / 1000).toString();
const nonceStr = crypto.randomBytes(16).toString('hex');
const packageStr = `prepay_id=${prepayId}`;
const signType = 'MD5';
const paySign = createSign({
appId: config.appId,
timeStamp,
nonceStr,
package: packageStr,
signType,
}, config.apiKey);
return {
timeStamp,
nonceStr,
package: packageStr,
signType,
paySign,
};
}
/services/pay/notifyHandler.ts —— 微信支付回调处理
// services/pay/notifyHandler.ts
import * as xml2js from 'xml2js';
import { createSign } from '@/utils/sign';
import * as crypto from 'crypto';
/**
* 处理微信支付回调
*/
export async function handlePaymentNotify(xmlString: string, apiKey: string) {
const parsed = await xml2js.parseStringPromise(xmlString, { explicitArray: false });
const notifyData = parsed.xml;
// 校验签名
const receivedSign = notifyData.sign;
const { sign, ...dataWithoutSign } = notifyData;
const validSign = createSign(dataWithoutSign, apiKey);
if (receivedSign !== validSign) {
throw new Error('支付回调签名验证失败');
}
if (notifyData.return_code !== 'SUCCESS' || notifyData.result_code !== 'SUCCESS') {
throw new Error('支付回调返回失败');
}
// 支付成功,处理订单
return {
outTradeNo: notifyData.out_trade_no,
transactionId: notifyData.transaction_id,
totalFee: Number(notifyData.total_fee),
openid: notifyData.openid,
};
}
小程序端 /pages/pay/index.ts —— 发起支付示例
// pages/pay/index.ts
async function startPayment() {
try {
const res = await wx.request({
url: '你的后端统一下单接口地址',
method: 'POST',
data: {
amount: 1, // 单位分
body: '测试商品',
},
});
const payData = res.data;
await wx.requestPayment({
timeStamp: payData.timeStamp,
nonceStr: payData.nonceStr,
package: payData.package,
signType: payData.signType,
paySign: payData.paySign,
success: () => {
wx.showToast({ title: '支付成功' });
},
fail: (err) => {
wx.showToast({ title: '支付失败', icon: 'none' });
console.error('支付失败:', err);
},
});
} catch (err) {
console.error('发起支付异常:', err);
wx.showToast({ title: '发起支付异常', icon: 'none' });
}
}
总结
微信小程序接入微信支付,流程严谨,规范严格,每一步都必须细致处理。
本篇文章从准备工作到工程化封装,系统梳理了整个开发链路,确保你可以快速掌握并避免掉坑。
规范封装、错误处理、日志监控,是打造稳定支付系统的基础。
希望本文能帮助你真正掌握微信小程序支付开发技能!