微信小程序电商实战 07:微信支付完整流程——下单、支付与服务端核实

微信小程序电商实战 01 技术选型 | 02 脚手架搭建 | 03 NutUI 配置 | 04 云开发环境 | 05 商品模块 | 06 购物车与订单 | 07 微信支付 ← 本文 | 08 用户中心 | 09 性能优化 | 10 上线部署


# 微信支付完整流程 ## 前置准备 ### 微信支付商户号 ### 在 CloudBase 配置商户信息 ### 安装 CloudBase 支付模板 ## 三段式流程 ### 第一段:统一下单 - 前端调 wxpayFunctions/wxpay_order - 云函数向微信支付下单 - 返回 prepay_id + 签名参数 ### 第二段:唤起支付 - wx.requestPayment(5个必填参数) - package 格式:prepay_id=*** - signType 只支持 RSA ### 第三段:核实结果 - 前端回调不可靠! - 轮询 wxpay_query_order - 微信异步通知回调 ## 支付页面设计 ### 倒计时(15分钟) ### 支付结果跳转 ## 常见错误处理 ### 签名错误 ### 余额不足 ### 商户号未开通

一、前置准备

1. 微信支付商户号配置

在 CloudBase 控制台 → 我的应用 → 微信支付,填入:

  • 商户号(mchid)
  • 商户 API v3 密钥
  • 商户 API 证书(.p12 文件)
  • 支付通知回调 URL(CloudBase 会自动生成)

2. 安装 CloudBase 支付模板

在 CloudBase 控制台 → 模板中心,搜索「微信支付」,一键安装。

安装后自动生成云函数 wxpayFunctions,包含以下 5 个方法:

方法 用途
wxpay_order 创建支付订单(统一下单)
wxpay_query_order_by_transaction_id 按微信交易号查询
wxpay_query_order_by_out_trade_no 按商户订单号查询
wxpay_refund 申请退款
wxpay_refund_query 查询退款状态

二、完整支付流程

sequenceDiagram participant U as 小程序(前端) participant CF as wxpayFunctions(云函数) participant WXP as 微信支付服务器 U->>CF: wxpay_order(orderNo, amount, desc) CF->>WXP: JSAPI 统一下单 WXP-->>CF: prepay_id CF->>CF: 生成签名(RSA) CF-->>U: { timeStamp, nonceStr, prepay_id, paySign } U->>WXP: wx.requestPayment(5个参数) WXP-->>U: success 回调(⚠️ 不可靠) Note over U: 展示"支付结果确认中..." U->>CF: wxpay_query_order(轮询,最多3次) CF->>WXP: 查询订单状态 WXP-->>CF: trade_state: SUCCESS / NOTPAY CF-->>U: 支付状态 WXP-->>CF: 异步通知(支付成功后主动推送) CF->>CF: 更新订单状态为"待发货"

三、统一下单(封装调用)

// src/api/pay.ts
import { callCloud } from './cloud'

interface PrepayResult {
  timeStamp: string
  nonceStr: string
  prepay_id: string
  paySign: string
  signType: 'RSA'
}

export const createPayOrder = (params: {
  orderId: string
  orderNo: string
  amount: number          // 单位:分
  description: string
}): Promise<PrepayResult> =>
  callCloud<PrepayResult>('wxpayFunctions', {
    action: 'wxpay_order',
    out_trade_no: params.orderNo,
    amount: { total: params.amount, currency: 'CNY' },
    description: params.description,
    attach: params.orderId,    // 透传订单 ID,回调时用
  })

export const queryPayOrder = (orderNo: string) =>
  callCloud<{ trade_state: string; transaction_id?: string }>('wxpayFunctions', {
    action: 'wxpay_query_order_by_out_trade_no',
    out_trade_no: orderNo,
  })

四、支付页面

<!-- src/pages/order/pay.vue -->
<template>
  <view class="pay-page">
    <view class="pay-amount">
      <text class="label">需付款</text>
      <nut-price :price="order.totalAmount" size="large" />
    </view>

    <!-- 倒计时 -->
    <view class="countdown">
      <text>剩余支付时间:</text>
      <nut-countdown
        :end-time="expireTime"
        format="mm:ss"
        @end="handleExpire"
      />
    </view>

    <view class="pay-tips">
      <text>使用微信支付完成支付</text>
    </view>

    <nut-button
      type="primary"
      block
      :loading="paying"
      @tap="handlePay"
    >
      立即支付
    </nut-button>

    <!-- 支付结果确认中 -->
    <nut-overlay :visible="confirming">
      <view class="confirming-tip">
        <nut-loading color="#fff" />
        <text>支付结果确认中...</text>
      </view>
    </nut-overlay>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import { Button as NutButton, Price as NutPrice, Loading as NutLoading,
         Overlay as NutOverlay, Countdown as NutCountdown, showToast } from '@nutui/nutui-taro'
import { getOrderDetail } from '@/api/order'
import { createPayOrder, queryPayOrder } from '@/api/pay'

const { orderId } = Taro.getCurrentInstance().router?.params ?? {}
const order = ref<any>({})
const paying = ref(false)
const confirming = ref(false)
const expireTime = ref(Date.now() + 15 * 60 * 1000)  // 15 分钟过期

onMounted(async () => {
  order.value = await getOrderDetail(orderId as string)
})

const handlePay = async () => {
  paying.value = true
  try {
    // 第一段:统一下单
    const prepay = await createPayOrder({
      orderId: order.value._id,
      orderNo: order.value.orderNo,
      amount: Math.round(order.value.totalAmount * 100),  // 转为分
      description: '商城订单',
    })

    // 第二段:唤起支付
    await new Promise<void>((resolve, reject) => {
      Taro.requestPayment({
        timeStamp: prepay.timeStamp,
        nonceStr: prepay.nonceStr,
        package: `prepay_id=${prepay.prepay_id}`,  // 格式固定!
        signType: 'RSA',
        paySign: prepay.paySign,
        success: () => resolve(),
        fail: (err) => reject(new Error(err.errMsg)),
      })
    })

    // 第三段:服务端核实(前端 success 不可靠!)
    paying.value = false
    confirming.value = true
    await pollPayResult(order.value.orderNo)

  } catch (err: any) {
    if (err.message?.includes('cancel')) {
      showToast.text('已取消支付')
    } else {
      showToast.fail('支付失败,请重试')
    }
    paying.value = false
    confirming.value = false
  }
}

// 轮询支付结果(最多 3 次,每次间隔 2 秒)
async function pollPayResult(orderNo: string, retry = 0) {
  if (retry >= 3) {
    confirming.value = false
    showToast.text('支付结果确认中,请稍后查看订单')
    Taro.navigateTo({ url: '/pages/order/list' })
    return
  }

  await new Promise(r => setTimeout(r, 2000))
  const result = await queryPayOrder(orderNo)

  if (result.trade_state === 'SUCCESS') {
    confirming.value = false
    showToast.success('支付成功!')
    Taro.redirectTo({ url: `/pages/order/result?orderId=${orderId}&status=success` })
  } else {
    await pollPayResult(orderNo, retry + 1)
  }
}

const handleExpire = () => {
  showToast.fail('支付超时,订单已取消')
  Taro.redirectTo({ url: '/pages/order/list' })
}
</script>

五、常见支付错误处理

错误码 / 信息 原因 解决方式
requestPayment:fail cancel 用户主动取消 正常,回到支付页
sign_type error signType 不是 RSA 确认填 'RSA',不是 'MD5'
package 格式错误 package 没加 prepay_id= 前缀 改为 prepay_id=${prepay_id}
appid 和下单 appid 不一致 签名 appid 与下单 appid 不同 保证前后端用同一个 appid
NOTPAY 轮询时用户还没付 继续轮询或提示用户
CLOSED 订单已关闭(超时/取消) 引导用户重新下单

六、退款(备用)

// 申请退款(商家后台操作,不在小程序端暴露)
export const refundOrder = (params: {
  orderNo: string
  refundNo: string
  amount: number
  reason: string
}) =>
  callCloud('wxpayFunctions', {
    action: 'wxpay_refund',
    out_trade_no: params.orderNo,
    out_refund_no: params.refundNo,
    amount: {
      refund: Math.round(params.amount * 100),
      total: Math.round(params.amount * 100),
      currency: 'CNY',
    },
    reason: params.reason,
  })

微信小程序电商实战 01 技术选型 | 02 脚手架搭建 | 03 NutUI 配置 | 04 云开发环境 | 05 商品模块 | 06 购物车与订单 | 07 微信支付 ← 本文 | 08 用户中心 | 09 性能优化 | 10 上线部署

实操清单

  • 注册微信支付商户号(个人可用"个人收款",但功能受限;企业主体功能完整)
  • 在 CloudBase 控制台填入商户号、APIv3密钥、API证书
  • 在 CloudBase 模板中心安装微信支付模板,确认 wxpayFunctions 云函数已生成
  • 封装 src/api/pay.ts,实现 createPayOrderqueryPayOrder
  • 创建 src/pages/order/pay.vue,在真机上(模拟器不支持微信支付)测试唤起支付弹窗
  • 验证 package 参数格式正确:prepay_id=xxx(不是直接传 prepay_id 字符串)
  • 在支付成功后验证不依赖 success 回调,而是通过 pollPayResult 轮询确认
  • 在云数据库验证支付成功后订单 status 从 1(待付款)变为 2(待发货)
  • 测试用户取消支付的 cancel 错误处理,确认提示文案友好
  • 测试 15 分钟超时场景(可将 expireTime 设为 30 秒测试)