微信支付与交易闭环:统一下单、回调与退款

系统讲解小程序 JSAPI 微信支付完整闭环:从前端拉起支付、服务端统一下单(API v3)、回调验签与解密、退款与对账、订单状态机设计,到云函数集成与支付合规注意事项,提供可直接落地的支付集成指南。

微信支付是电商、知识付费、线下到店等小程序商业化的核心环节。与一般 API 不同,支付涉及金额、签名、回调、退款、对账与合规,任何一步出错都意味着真金白银的损失。本文将以「微信支付 API v3」为主,完整拆解 JSAPI 支付的统一下单、拉起支付、回调验签与解密、退款对账、订单状态机,并给出云函数集成与合规注意事项。

一、JSAPI 支付整体流程

JSAPI 支付是小程序内最常见的支付场景,用户在小程序内完成下单与支付:

前端 wx.login 拿到 openid
   → 后端「统一下单」拿到 prepay_id
   → 后端组签名参数返回前端
   → 前端 wx.requestPayment 拉起微信支付
   → 用户输入密码/指纹完成支付
   → 微信异步通知后端「支付成功」回调
   → 后端验签解密、更新订单状态、发货/开卡

关键原则:前端永远不碰商户密钥,所有涉及签名、金额的环节都必须在服务端完成。

二、统一下单(服务端)

2.1 API v3 下单接口

服务端调用 POST /v3/pay/transactions/jsapi(旧版 v2 为「统一下单」unifiedorder):

// 服务端 Node.js:统一下单
const axios = require('axios');
const { wxPaySign } = require('./sign');   // v3 请求签名工具

async function createJsapiOrder({ openid, outTradeNo, totalFee, description }) {
  const url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi';
  const body = {
    appid: APPID,                 // 小程序 AppID
    mchid: MCHID,                 // 商户号
    description,                  // 商品描述
    out_trade_no: outTradeNo,     // 商户订单号,唯一
    notify_url: NOTIFY_URL,       // 支付结果回调地址
    amount: {
      total: totalFee,            // 金额,单位:分
      currency: 'CNY'
    },
    payer: { openid }             // 下单用户的 openid
  };

  const resp = await axios.post(url, body, {
    headers: wxPaySign('POST', url, body)   // 注入 v3 签名头
  });
  return resp.data.prepay_id;              // 用于拉起支付
}

2.2 组前端支付参数

拿到 prepay_id 后,服务端生成前端拉起支付所需的签名参数(签名串按 appId\n timeStamp\n nonceStr\n package\n \n 拼接,使用商户私钥做 SHA256-RSA 签名):

// 服务端:生成 wx.requestPayment 参数
function buildPayParams(prepayId) {
  const timeStamp = String(Math.floor(Date.now() / 1000));
  const nonceStr = crypto.randomBytes(16).toString('hex');
  const package_ = `prepay_id=${prepayId}`;
  const signStr = `${APPID}\n${timeStamp}\n${nonceStr}\n${package_}\n`;

  const paySign = crypto
    .createSign('RSA-SHA256')
    .update(signStr)
    .sign(process.env.MCH_PRIVATE_KEY, 'base64');

  return { appId: APPID, timeStamp, nonceStr, package: package_, signType: 'RSA', paySign };
}

三、前端拉起支付:wx.requestPayment

前端拿到后端返回的支付参数后调用 wx.requestPayment:

// 小程序端
async function payOrder(orderId) {
  // 1. 请求后端统一下单,拿到支付参数
  const payParams = await request({ url: '/api/pay/create', data: { orderId } });

  // 2. 拉起微信支付面板
  wx.requestPayment({
    timeStamp: payParams.timeStamp,     // 秒级时间戳字符串
    nonceStr: payParams.nonceStr,
    package: payParams.package,         // prepay_id=xxx
    signType: 'RSA',                    // v3 为 RSA,v2 为 MD5
    paySign: payParams.paySign,
    success() {
      // 注意:success 不代表支付成功,只代表用户完成了支付动作
      // 真正以回调为准
      wx.showToast({ title: '支付完成', icon: 'success' });
    },
    fail(err) {
      if (err.errMsg.includes('cancel')) {
        wx.showToast({ title: '已取消支付', icon: 'none' });
      } else {
        wx.showToast({ title: '支付失败', icon: 'none' });
      }
    }
  });
}
参数类型说明
timeStampstring秒级时间戳
nonceStrstring随机字符串
packagestringprepay_id=${prepay_id}
signTypestringv3: RSA;v2: MD5
paySignstring支付参数签名

一句话:wx.requestPayment 的 success 只表示用户完成了支付交互,唯一可信的支付结果来源是服务端回调,前端绝不能据此直接发货。

四、支付回调:验签与解密

4.1 回调数据结构

微信支付以 POST 通知 notify_url,v3 通知的 body 是 AES-256-GCM 加密的密文,且需用微信平台证书验签:

// 服务端:处理 v3 支付回调
async function handlePayNotify(req, res) {
  const headers = req.headers;
  // 验签:Wechatpay-Signature 用平台证书公钥验签
  const valid = verifySignature(
    headers['wechatpay-timestamp'],
    headers['wechatpay-nonce'],
    headers['wechatpay-signature'],
    headers['wechatpay-serial']
  );
  if (!valid) return res.status(401).end();

  // 解密:AES-256-GCM,key 为 APIv3 密钥
  const plain = decryptNotify(req.rawBody);   // 得到明文 JSON
  const { out_trade_no, transaction_id, trade_state, amount } = plain;

  if (trade_state === 'SUCCESS') {
    // 幂等处理:先查本地订单状态,避免重复回调
    const order = await orderService.getByOutTradeNo(out_trade_no);
    if (order && order.status === 'pending_pay' && order.totalFee === amount.total) {
      await orderService.markPaid(order, transaction_id);
    }
  }
  res.json({ code: 'SUCCESS', message: '成功' });   // 必须返回 SUCCESS
}

4.2 回调安全要点

安全项措施
验签用平台证书公钥验证签名,防伪造回调
解密用 APIv3 密钥解密通知体
幂等按 out_trade_no 防重复处理
金额校验回调金额必须与本地订单一致
响应要求必须返回 SUCCESS,否则微信会重试

一句话:回调处理的三道关卡是「验签、解密、幂等」,缺一不可;金额校验是防止中间人篡改的最后防线。

4.2 主动查单兜底

回调存在延迟或丢失风险,尤其用户支付成功后立即杀掉小程序时,回调可能晚于用户看到结果。业务层应提供「主动查单」兜底:前端在 wx.requestPayment 成功或进入订单详情时触发服务端查单,服务端通过微信查单 API 与本地订单比对:

// 服务端:按商户订单号主动查单
async function queryOrder(outTradeNo) {
  const url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=${MCHID}`;
  const resp = await axios.get(url, { headers: wxPaySign('GET', url, '') });
  const { trade_state, transaction_id } = resp.data;
  if (trade_state === 'SUCCESS') {
    await orderService.markPaidByIdempotent(outTradeNo, transaction_id);
  }
  return trade_state;   // SUCCESS / NOTPAY / CLOSED / REFUND 等
}

前端在「支付完成」进入确认页时,可先展示「支付确认中」状态,再通过查单结果刷新,避免用户看到与实际支付结果不一致的页面。

五、退款与对账

5.1 退款

退款走 POST /v3/refund/domestic/refunds,同样需要签名:

// 服务端:发起退款
async function createRefund({ outTradeNo, refundNo, refundFee, totalFee }) {
  const url = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds';
  const body = {
    out_trade_no: outTradeNo,          // 原支付订单号
    out_refund_no: refundNo,           // 商户退款单号
    notify_url: REFUND_NOTIFY_URL,     // 退款结果回调(可配置)
    amount: {
      refund: refundFee,               // 退款金额(分)
      total: totalFee,                 // 原订单金额(分)
      currency: 'CNY'
    }
  };
  const resp = await axios.post(url, body, { headers: wxPaySign('POST', url, body) });
  return resp.data;                    // { refund_id, status: 'PROCESSING'|'SUCCESS' }
}

5.2 对账

  • 每日对账:用「下载交易账单」接口(/v3/bill/tradebill)拉取日账单,与本地订单流水比对,发现不一致即告警。
  • 订单金额核对:微信账单金额与本地数据库 totalFee 逐一匹配。
  • 异常处理:对不上的单子进人工核查队列,避免长期挂账。
动作接口/渠道频率
查询订单GET /v3/pay/transactions/out-trade-no/{no}实时兜底
下载账单GET /v3/bill/tradebill每日
退款查询GET /v3/refund/domestic/refunds/{no}按需

六、订单状态机设计

支付相关订单建议设计严谨的状态机,杜绝「支付了没发货」「退款了还显示待支付」等状态错乱:

创建订单(pending_pay)
   │  wx.requestPayment 成功
   ▼
待支付确认(pending_confirm) ← 等待回调
   │  回调成功
   ▼
已支付(paid) ── 发起退款 → 退款中(refunding)
   │                        │ 退款成功
   ▼                        ▼
已完成(completed)        已退款(refunded)
   │
   └─ 超时未支付 → 已取消(cancelled)
// 状态机校验示例
const ALLOWED_TRANSITIONS = {
  'pending_pay': ['pending_confirm', 'cancelled'],
  'pending_confirm': ['paid', 'cancelled'],
  'paid': ['refunding', 'completed'],
  'refunding': ['refunded'],
  'cancelled': [],
  'refunded': [],
  'completed': []
};

function transitionOrder(order, nextStatus) {
  if (!ALLOWED_TRANSITIONS[order.status].includes(nextStatus)) {
    throw new Error(`非法状态流转:${order.status} → ${nextStatus}`);
  }
  return { ...order, status: nextStatus, updatedAt: new Date() };
}

一句话:订单状态机的核心是「每个状态只有有限个合法出口」,把非法流转挡在代码层,比事后对账补救便宜得多。

6.1 超时未支付自动关闭

订单在 pending_pay 停留过久会造成库存占压,应配置定时任务自动关闭超时订单:

// 服务端:定时扫描超时未支付订单
const TIMEOUT_MS = 30 * 60 * 1000;   // 30 分钟未支付自动关闭

async function closeExpiredOrders() {
  const expired = await db.orders.find({
    status: 'pending_pay',
    createdAt: { $lt: new Date(Date.now() - TIMEOUT_MS) }
  });
  for (const order of expired) {
    await transitionOrder(order, 'cancelled');
  }
  console.log(`已关闭 ${expired.length} 笔超时订单`);
}

注意:关闭订单后若微信回调仍到达(极端竞态),需在回调处理中判断订单已为 cancelled 并跳过发货流程,保证幂等。

七、与云函数/服务端集成

7.1 自建服务端

商户私钥、APIv3 密钥等敏感配置放服务端环境变量,统一封装支付 SDK 模块,对外提供 createPayment / handleNotify / refund 三个方法,业务层只调用不碰签名细节。

7.2 云开发集成

云开发提供了云支付能力,wx-server-sdk 内置 cloud.cloudPay:

// cloudfunctions/pay/index.js
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });

exports.main = async (event) => {
  const { orderId, totalFee } = event;
  const wxContext = cloud.getWXContext();

  const res = await cloud.cloudPay.unifiedOrder({
    body: '商品购买',
    outTradeNo: orderId,
    spbillCreateIp: '127.0.0.1',
    subMchId: '商户号',
    totalFee,                        // 单位:分
    envId: '你的云环境 ID',
    functionName: 'pay-callback'     // 支付回调云函数
  });

  return res.payment;   // { timeStamp, nonceStr, package, signType, paySign }
};

前端拿到 payment 后同样调用 wx.requestPayment 拉起支付。

八、支付合规注意事项

  • 主体资质:支付能力与小程序主体绑定,个体工商户/企业需完成相应认证。
  • 经营类目:类目与交易场景须一致,虚拟商品、金融、医疗等类目有额外资质要求。
  • 费率与结算:标准费率通常为 0.6%,部分行业/服务有差异,结算周期 T+1 或更长,需做好资金规划。
  • 支付凭证留存:保留订单、回调、退款流水,满足对账与审计要求。
  • 隐私合规:支付相关个人信息(订单、金额)按隐私政策收集与保护。

九、总结

环节关键接口/机制核心要点
统一下单POST /v3/pay/transactions/jsapi服务端持密钥,产出 prepay_id
拉起支付wx.requestPayment前端只做「拉起」,success 不等于成功
回调处理验签 + AES-GCM 解密幂等 + 金额校验,回 SUCCESS
退款POST /v3/refund/domestic/refunds独立退款单,异步结果
对账下载账单 API每日比对,异常告警
订单状态机白名单流转杜绝非法状态迁移
云开发cloud.cloudPay免自建签名服务

微信支付是「签名、回调、状态、资金」四重逻辑的交汇点。守住三条底线——签名永远在服务端、支付结果以回调为准、金额校验与幂等贯穿始终——再配合严谨的订单状态机与每日对账,交易闭环才能在真实流量下稳定运行。将支付集成到完整业务中,可参考本专题的电商全栈项目实战。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「miniprogram」更多文章

  1. 小程序动画与 Canvas 实践:交互动效、海报生成与可视化
  2. 小程序分包加载与性能优化进阶:主包瘦身、预下载与按需注入
  3. 小程序页面路由与导航架构:页面栈、tabBar 与自定义导航