UniApp 小程序支付功能全解析:从原理到实战
随着移动互联网的发展,小程序已成为连接用户与服务的重要载体,而支付功能则是小程序实现商业闭环的核心环节。无论是电商购物、服务付费还是内容订阅,支付功能都扮演着不可或缺的角色。UniApp 作为一款跨平台开发框架,支持一次编码多端发布(如微信、支付宝、百度、字节跳动等小程序平台),极大降低了开发者的多端适配成本。
本文将围绕 UniApp 小程序支付功能展开,从支付流程原理、环境准备、前后端实现,到常见问题与最佳实践,提供一套完整的技术指南,帮助开发者快速掌握小程序支付的实现方法。
目录#
-
- 1.1 账号与资质
- 1.2 开发环境
- 1.3 核心概念
-
- 2.1 通用支付流程
- 2.2 平台差异说明
-
- 3.1 后端:生成支付参数
- 3.2 前端:调用支付接口
- 3.3 支付结果验证
-
- 4.1 签名错误
- 4.2 支付参数无效
- 4.3 支付结果回调异常
-
- 5.1 安全性保障
- 5.2 用户体验优化
- 5.3 订单状态管理
-
- 6.1 后端代码(Node.js)
- 6.2 前端代码(UniApp)
1. 前置准备#
在开始实现支付功能前,需完成以下准备工作,确保开发环境和账号资质符合要求。
1.1 账号与资质#
1.1.1 小程序账号#
- 注册平台账号:根据目标平台(微信、支付宝、百度等)注册小程序账号,完成开发者认证。
- 获取 AppID:在平台开发者后台获取小程序唯一标识(如微信的
appid、支付宝的appId)。
1.1.2 支付商户账号#
- 申请支付权限:在小程序平台关联支付商户号(如微信支付商户号、支付宝商户号),并完成资质认证(企业/个体工商户需提供营业执照等材料)。
- 配置支付密钥:在商户后台设置支付密钥(如微信的 API 密钥、支付宝的应用私钥/公钥),用于签名生成和验证。
1.2 开发环境#
- UniApp 开发工具:安装 HBuilderX,创建 UniApp 项目(选择“小程序”模板)。
- 后端开发环境:根据技术栈选择(如 Node.js、Java、Python 等),需支持 HTTPS(支付接口要求)。
- 调试工具:
- 微信开发者工具(用于微信小程序调试);
- 支付宝开发者工具(用于支付宝小程序调试)。
1.3 核心概念#
- 预支付订单:由后端调用支付平台 API 生成,包含订单金额、商品描述等信息,是支付的核心凭证。
- 签名:通过支付密钥对参数进行加密生成的字符串,用于验证请求合法性(防止参数被篡改)。
- 支付回调:支付完成后,支付平台通过预设的回调地址通知商户后端支付结果(异步通知)。
- uni.requestPayment:UniApp 提供的统一支付接口,封装了各平台的支付调用逻辑。
2. 支付流程原理#
2.1 通用支付流程#
无论哪个平台,小程序支付的核心流程一致,可概括为以下步骤:
- 用户发起支付:用户在小程序中点击“支付”按钮,触发支付流程。
- 前端请求后端:前端将订单信息(如订单号、金额)发送至商户后端。
- 后端生成预支付参数:
- 后端调用支付平台 API(如微信的“统一下单”接口、支付宝的“交易创建”接口),生成预支付订单。
- 对预支付参数进行签名,返回给前端。
- 前端调用支付接口:前端通过
uni.requestPayment传入预支付参数,调起平台支付界面。 - 用户完成支付:用户在支付界面输入密码/指纹,完成支付。
- 支付结果通知:
- 同步通知:支付完成后,支付平台直接返回结果给前端(不可作为最终支付凭证,可能被篡改)。
- 异步通知:支付平台通过后端预设的
notify_url异步通知支付结果(需后端验证签名,作为最终凭证)。
- 后端更新订单状态:后端接收异步通知,验证通过后更新订单状态(如“已支付”)。
2.2 平台差异说明#
不同平台的支付参数和接口细节存在差异,需注意适配:
| 平台 | 核心参数示例 | 签名算法 | 支付接口文档 |
|---|---|---|---|
| 微信小程序 | appId、timeStamp、nonceStr、package(格式 prepay_id=xxx)、signType、paySign | HMAC-SHA256 或 MD5 | 微信支付文档 |
| 支付宝小程序 | appId、bizContent(JSON 字符串)、charset、sign | RSA2(SHA256WithRSA) | 支付宝支付文档 |
UniApp 的 uni.requestPayment 接口已对部分参数进行封装,但仍需根据平台传入对应参数。
3. 详细实现步骤#
3.1 后端:生成支付参数#
后端的核心任务是调用支付平台 API 生成预支付参数,并返回给前端。以下以 微信小程序 和 支付宝小程序 为例,介绍关键步骤。
3.1.1 微信小程序支付参数生成#
步骤 1:调用“统一下单”接口
后端需调用微信支付的 统一下单 接口(https://api.mch.weixin.qq.com/pay/unifiedorder),传入以下参数(部分必选):
| 参数名 | 说明 | 示例值 |
|---|---|---|
appid | 小程序 AppID | wx1234567890abcdef |
mch_id | 商户号 | 1234567890 |
nonce_str | 随机字符串(32位以内) | Wm3WZYTPz0wzccnW |
sign | 签名(通过 API 密钥生成) | (自动生成) |
body | 商品描述 | 测试商品 |
out_trade_no | 商户订单号(唯一) | 20231001001 |
total_fee | 订单金额(单位:分) | 100(即 1 元) |
spbill_create_ip | 客户端 IP 地址 | 123.123.123.123 |
notify_url | 支付结果异步通知地址(HTTPS) | https://api.example.com/notify |
trade_type | 交易类型(小程序支付固定为 JSAPI) | JSAPI |
openid | 用户在该小程序的唯一标识(需前端传递) | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o |
步骤 2:处理返回结果
接口返回 prepay_id(预支付会话标识),后端需用此生成前端调用支付所需的参数:
// 微信支付参数示例(需签名)
const payParams = {
appId: 'wx1234567890abcdef', // 小程序 AppID
timeStamp: Math.floor(Date.now() / 1000).toString(), // 时间戳(秒级,字符串)
nonceStr: 'Wm3WZYTPz0wzccnW', // 随机字符串
package: 'prepay_id=wx201410272009395522657a690389285100', // 格式固定为 prepay_id=xxx
signType: 'HMAC-SHA256', // 签名类型(推荐 HMAC-SHA256)
paySign: '' // 待生成的签名
};步骤 3:生成签名
按微信支付签名规则,对 payParams 中的 appId、timeStamp、nonceStr、package、signType 进行排序,拼接成 key=value& 格式,最后拼接 API 密钥,用 HMAC-SHA256 加密并转大写,得到 paySign。
3.1.2 支付宝小程序支付参数生成#
步骤 1:调用“交易创建”接口
后端调用支付宝的 alipay.trade.create 接口,传入以下参数(JSON 格式):
{
"out_trade_no": "20231001001", // 商户订单号
"total_amount": "1.00", // 订单金额(单位:元)
"subject": "测试商品", // 商品标题
"product_code": "FAST_INSTANT_TRADE_PAY", // 固定值
"notify_url": "https://api.example.com/notify" // 异步通知地址
}步骤 2:处理返回结果
接口返回 trade_no(支付宝交易号),后端需生成前端调用支付的参数:
// 支付宝支付参数示例(需签名)
const payParams = {
appId: '2021000000000000', // 支付宝 AppID
bizContent: JSON.stringify({
outTradeNo: '20231001001',
totalAmount: '1.00',
subject: '测试商品',
tradeNo: '2023100122001404150580001234' // 支付宝交易号
}),
charset: 'utf-8',
signType: 'RSA2',
sign: '' // 待生成的签名
};步骤 3:生成签名
使用商户私钥对 bizContent 等参数进行 RSA2 签名,得到 sign。
3.2 前端:调用支付接口#
UniApp 提供 uni.requestPayment 接口统一调起各平台支付,前端需根据后端返回的参数调用该接口。
3.2.1 通用调用逻辑#
// 前端支付按钮点击事件
async handlePay() {
try {
// 1. 向后端请求支付参数
const { data } = await uni.request({
url: 'https://api.example.com/getPayParams', // 后端接口
method: 'POST',
data: { orderId: '20231001001' } // 订单 ID
});
// 2. 调用支付接口
const res = await uni.requestPayment({
provider: 'wxpay', // 支付平台(wxpay/alipay/baidu/ttpay 等)
...data.payParams // 后端返回的支付参数
});
// 3. 同步通知:支付成功(仅作参考,需以后端异步通知为准)
if (res.errMsg === 'requestPayment:ok') {
uni.showToast({ title: '支付成功', icon: 'success' });
// 跳转至订单详情页
uni.navigateTo({ url: '/pages/order/detail?id=20231001001' });
}
} catch (err) {
// 支付失败(如用户取消、网络错误等)
uni.showToast({ title: '支付失败:' + err.errMsg, icon: 'none' });
}
}3.2.2 平台适配注意事项#
- 微信小程序:
provider为wxpay,参数需包含timeStamp(字符串类型)、nonceStr、package、signType、paySign。 - 支付宝小程序:
provider为alipay,参数需包含appId、bizContent、charset、signType、sign。 - 多平台兼容:可通过
uni.getSystemInfoSync().platform判断当前平台,动态选择provider和参数。
3.3 支付结果验证#
核心原则:前端同步通知不可信,必须依赖后端接收支付平台的异步通知进行验证。
3.3.1 后端接收异步通知#
以微信支付为例,支付完成后,微信会向 notify_url 发送 POST 请求,参数为 XML 格式,包含 out_trade_no、total_fee、result_code 等信息。后端需:
- 验证签名:使用 API 密钥验证通知参数的签名,确保请求来自微信。
- 校验订单信息:核对
out_trade_no对应的订单金额、状态是否匹配。 - 返回结果:验证通过后,返回
success给微信(否则微信会重复发送通知)。
示例代码(Node.js/Express):
const xml2js = require('xml2js');
const parser = new xml2js.Parser({ explicitArray: false });
// 微信支付异步通知接口
app.post('/notify', (req, res) => {
let xml = '';
req.on('data', (chunk) => (xml += chunk));
req.on('end', async () => {
try {
// 解析 XML 为 JSON
const result = await parser.parseStringPromise(xml);
const notifyData = result.xml;
// 1. 验证签名(省略签名验证逻辑,需严格按微信文档实现)
const isSignValid = verifyWechatSign(notifyData);
if (!isSignValid) {
return res.send('<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>');
}
// 2. 校验订单状态
if (notifyData.result_code === 'SUCCESS') {
const orderId = notifyData.out_trade_no;
const order = await OrderModel.findById(orderId);
if (order && order.amount * 100 === Number(notifyData.total_fee)) { // 金额校验(单位:分)
order.status = 'paid'; // 更新订单状态为“已支付”
await order.save();
return res.send('<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>');
}
}
// 验证失败
res.send('<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单验证失败]]></return_msg></xml>');
} catch (err) {
res.send('<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[服务器错误]]></return_msg></xml>');
}
});
});4. 常见问题与解决方案#
4.1 签名错误#
现象:调用支付接口时提示“签名错误”。
原因:
- 参数排序错误(需按 ASCII 码从小到大排序);
- 签名密钥错误(微信用 API 密钥,支付宝用商户私钥);
- 参数格式错误(如
timeStamp为数字而非字符串)。
解决方案: - 使用支付平台提供的 签名校验工具 验证签名;
- 确保参数名、大小写、格式与文档一致。
4.2 支付参数无效#
现象:调起支付时提示“支付参数无效”。
原因:
prepay_id已过期(微信prepay_id有效期 2 小时);openid与小程序appid不匹配;- 支付宝
bizContent格式错误(需为 JSON 字符串)。
解决方案: - 检查后端生成
prepay_id的流程,确保参数正确; - 前端传递
openid时需通过uni.getUserInfo或wx.login获取。
4.3 支付结果回调异常#
现象:支付完成后,后端未收到异步通知。
原因:
notify_url非 HTTPS 或无法访问;- 回调接口返回非
success字符串; - 服务器防火墙拦截了支付平台的请求。
解决方案: - 确保
notify_url可公网访问且支持 HTTPS; - 验证通过后必须返回
<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>(微信)或success(支付宝); - 检查服务器日志,确认是否接收到回调请求。
5. 最佳实践#
5.1 安全性保障#
- 签名机制:所有支付参数必须签名,且后端需验证异步通知的签名。
- HTTPS 通信:前后端接口、支付回调地址均需使用 HTTPS,防止数据被篡改。
- 订单幂等性:通过
out_trade_no确保同一订单不会重复支付(后端需校验订单状态)。 - 敏感信息加密:用户
openid、支付密钥等敏感信息不可明文存储或传输。
5.2 用户体验优化#
- 加载状态:调用支付接口前显示 loading(
uni.showLoading),支付完成后隐藏。 - 错误提示:支付失败时显示具体原因(如“余额不足”“用户取消支付”)。
- 结果同步:支付成功后,前端可轮询后端接口确认订单状态,避免依赖同步通知。
5.3 订单状态管理#
- 状态流转:明确订单状态(待支付、支付中、已支付、支付失败、退款中、已退款),避免状态混乱。
- 日志记录:记录支付流程关键节点(如请求预支付、支付回调、订单更新),便于问题排查。
- 超时处理:设置订单支付超时时间(如 15 分钟),超时后自动取消订单。
6. 实战案例:完整支付流程示例#
6.1 后端代码(Node.js/Express + 微信支付)#
const express = require('express');
const crypto = require('crypto');
const xml2js = require('xml2js');
const request = require('request-promise');
const app = express();
app.use(express.raw({ type: 'application/xml' })); // 解析 XML 格式请求
// 配置
const config = {
appid: 'wx1234567890abcdef', // 小程序 AppID
mch_id: '1234567890', // 商户号
apiKey: 'your_api_key', // 微信支付 API 密钥
notifyUrl: 'https://api.example.com/notify' // 异步通知地址
};
// 生成随机字符串
function generateNonceStr() {
return Math.random().toString(36).substr(2, 15);
}
// 生成签名
function generateSign(params) {
const keys = Object.keys(params).sort();
const str = keys.map(key => `${key}=${params[key]}`).join('&') + `&key=${config.apiKey}`;
return crypto.createHash('md5').update(str).digest('hex').toUpperCase();
}
// 1. 获取支付参数接口
app.post('/getPayParams', async (req, res) => {
const { orderId, openid } = req.body; // 前端传递订单 ID 和用户 openid
// 调用微信统一下单接口
const nonceStr = generateNonceStr();
const params = {
appid: config.appid,
mch_id: config.mch_id,
nonce_str: nonceStr,
body: '测试商品',
out_trade_no: orderId,
total_fee: 100, // 1 元(单位:分)
spbill_create_ip: req.ip.replace('::ffff:', ''),
notify_url: config.notifyUrl,
trade_type: 'JSAPI',
openid: openid
};
params.sign = generateSign(params);
// 转为 XML 格式
const builder = new xml2js.Builder();
const xmlParams = builder.buildObject({ xml: params });
try {
const xmlRes = await request({
url: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
method: 'POST',
body: xmlParams
});
// 解析 XML 响应
const result = await xml2js.parseStringPromise(xmlRes, { explicitArray: false });
const prepayId = result.xml.prepay_id;
// 生成前端支付参数
const payParams = {
appId: config.appid,
timeStamp: Math.floor(Date.now() / 1000).toString(),
nonceStr: nonceStr,
package: `prepay_id=${prepayId}`,
signType: 'MD5'
};
payParams.paySign = generateSign(payParams);
res.json({ code: 0, payParams });
} catch (err) {
res.json({ code: -1, msg: '生成支付参数失败' });
}
});
// 2. 支付结果异步通知接口
app.post('/notify', (req, res) => {
// 省略签名验证和订单更新逻辑(见 3.3.1 节)
res.send('<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>');
});
app.listen(3000, () => console.log('Server running on port 3000'));6.2 前端代码(UniApp Vue)#
<template>
<view class="pay-container">
<button @click="handlePay">立即支付</button>
</view>
</template>
<script>
export default {
data() {
return {
orderId: '20231001001' // 订单 ID(实际项目中从页面参数获取)
};
},
methods: {
async handlePay() {
uni.showLoading({ title: '发起支付中...' });
try {
// 1. 获取用户 openid(需提前通过 wx.login 获取 code,再调用后端接口换 openid)
const { code } = await uni.login();
const { data: openidRes } = await uni.request({
url: 'https://api.example.com/getOpenid',
method: 'POST',
data: { code }
});
const openid = openidRes.openid;
// 2. 获取支付参数
const { data: payRes } = await uni.request({
url: 'https://api.example.com/getPayParams',
method: 'POST',
data: { orderId: this.orderId, openid }
});
if (payRes.code !== 0) {
throw new Error(payRes.msg);
}
// 3. 调用支付接口
const payResult = await uni.requestPayment({
provider: 'wxpay',
...payRes.payParams
});
if (payResult.errMsg === 'requestPayment:ok') {
uni.showToast({ title: '支付成功', icon: 'success' });
// 轮询后端确认订单状态
this.checkOrderStatus();
}
} catch (err) {
uni.showToast({ title: '支付失败:' + (err.errMsg || err.message), icon: 'none' });
} finally {
uni.hideLoading();
}
},
// 轮询确认订单状态
checkOrderStatus() {
const timer = setInterval(async () => {
const { data } = await uni.request({
url: `https://api.example.com/checkOrderStatus?orderId=${this.orderId}`
});
if (data.status === 'paid') {
clearInterval(timer);
uni.navigateTo({ url: `/pages/order/detail?id=${this.orderId}` });
}
}, 2000);
}
}
};
</script>7. 参考资料#
通过本文的指南,开发者可快速掌握 UniApp 小程序支付功能的实现逻辑和关键细节。实际开发中需根据具体平台文档和业务需求进行调整,确保支付流程的安全性和稳定性。