微信小程序对接微信支付


前言

本文详细讲解了微信小程序内接入微信支付的全过程,包括配置准备、后端统一下单、小程序调起支付、回调处理、常见问题排查与工程化封装。结合实际项目开发经验,避免踩坑,助力快速上线高可用微信支付功能。


一、微信支付开发前的准备工作

在动手开发之前,请确保以下准备工作完成:

项目说明
小程序 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' });
  }
}

总结

微信小程序接入微信支付,流程严谨,规范严格,每一步都必须细致处理。

本篇文章从准备工作到工程化封装,系统梳理了整个开发链路,确保你可以快速掌握并避免掉坑。

规范封装、错误处理、日志监控,是打造稳定支付系统的基础。

希望本文能帮助你真正掌握微信小程序支付开发技能!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值