UniApp 小程序支付功能全解析:从原理到实战

随着移动互联网的发展,小程序已成为连接用户与服务的重要载体,而支付功能则是小程序实现商业闭环的核心环节。无论是电商购物、服务付费还是内容订阅,支付功能都扮演着不可或缺的角色。UniApp 作为一款跨平台开发框架,支持一次编码多端发布(如微信、支付宝、百度、字节跳动等小程序平台),极大降低了开发者的多端适配成本。

本文将围绕 UniApp 小程序支付功能展开,从支付流程原理、环境准备、前后端实现,到常见问题与最佳实践,提供一套完整的技术指南,帮助开发者快速掌握小程序支付的实现方法。

目录#

  1. 前置准备

    • 1.1 账号与资质
    • 1.2 开发环境
    • 1.3 核心概念
  2. 支付流程原理

    • 2.1 通用支付流程
    • 2.2 平台差异说明
  3. 详细实现步骤

    • 3.1 后端:生成支付参数
    • 3.2 前端:调用支付接口
    • 3.3 支付结果验证
  4. 常见问题与解决方案

    • 4.1 签名错误
    • 4.2 支付参数无效
    • 4.3 支付结果回调异常
  5. 最佳实践

    • 5.1 安全性保障
    • 5.2 用户体验优化
    • 5.3 订单状态管理
  6. 实战案例:完整支付流程示例

    • 6.1 后端代码(Node.js)
    • 6.2 前端代码(UniApp)
  7. 参考资料

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 通用支付流程#

无论哪个平台,小程序支付的核心流程一致,可概括为以下步骤:

  1. 用户发起支付:用户在小程序中点击“支付”按钮,触发支付流程。
  2. 前端请求后端:前端将订单信息(如订单号、金额)发送至商户后端。
  3. 后端生成预支付参数
    • 后端调用支付平台 API(如微信的“统一下单”接口、支付宝的“交易创建”接口),生成预支付订单。
    • 对预支付参数进行签名,返回给前端。
  4. 前端调用支付接口:前端通过 uni.requestPayment 传入预支付参数,调起平台支付界面。
  5. 用户完成支付:用户在支付界面输入密码/指纹,完成支付。
  6. 支付结果通知
    • 同步通知:支付完成后,支付平台直接返回结果给前端(不可作为最终支付凭证,可能被篡改)。
    • 异步通知:支付平台通过后端预设的 notify_url 异步通知支付结果(需后端验证签名,作为最终凭证)。
  7. 后端更新订单状态:后端接收异步通知,验证通过后更新订单状态(如“已支付”)。

2.2 平台差异说明#

不同平台的支付参数和接口细节存在差异,需注意适配:

平台核心参数示例签名算法支付接口文档
微信小程序appIdtimeStampnonceStrpackage(格式 prepay_id=xxx)、signTypepaySignHMAC-SHA256 或 MD5微信支付文档
支付宝小程序appIdbizContent(JSON 字符串)、charsetsignRSA2(SHA256WithRSA)支付宝支付文档

UniApp 的 uni.requestPayment 接口已对部分参数进行封装,但仍需根据平台传入对应参数。

3. 详细实现步骤#

3.1 后端:生成支付参数#

后端的核心任务是调用支付平台 API 生成预支付参数,并返回给前端。以下以 微信小程序支付宝小程序 为例,介绍关键步骤。

3.1.1 微信小程序支付参数生成#

步骤 1:调用“统一下单”接口
后端需调用微信支付的 统一下单 接口(https://api.mch.weixin.qq.com/pay/unifiedorder),传入以下参数(部分必选):

参数名说明示例值
appid小程序 AppIDwx1234567890abcdef
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交易类型(小程序支付固定为 JSAPIJSAPI
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 中的 appIdtimeStampnonceStrpackagesignType 进行排序,拼接成 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 平台适配注意事项#

  • 微信小程序providerwxpay,参数需包含 timeStamp(字符串类型)、nonceStrpackagesignTypepaySign
  • 支付宝小程序provideralipay,参数需包含 appIdbizContentcharsetsignTypesign
  • 多平台兼容:可通过 uni.getSystemInfoSync().platform 判断当前平台,动态选择 provider 和参数。

3.3 支付结果验证#

核心原则:前端同步通知不可信,必须依赖后端接收支付平台的异步通知进行验证。

3.3.1 后端接收异步通知#

以微信支付为例,支付完成后,微信会向 notify_url 发送 POST 请求,参数为 XML 格式,包含 out_trade_nototal_feeresult_code 等信息。后端需:

  1. 验证签名:使用 API 密钥验证通知参数的签名,确保请求来自微信。
  2. 校验订单信息:核对 out_trade_no 对应的订单金额、状态是否匹配。
  3. 返回结果:验证通过后,返回 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.getUserInfowx.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 小程序支付功能的实现逻辑和关键细节。实际开发中需根据具体平台文档和业务需求进行调整,确保支付流程的安全性和稳定性。