微信小程序电商实战 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,实现createPayOrder和queryPayOrder - 创建
src/pages/order/pay.vue,在真机上(模拟器不支持微信支付)测试唤起支付弹窗 - 验证
package参数格式正确:prepay_id=xxx(不是直接传 prepay_id 字符串) - 在支付成功后验证不依赖
success回调,而是通过pollPayResult轮询确认 - 在云数据库验证支付成功后订单
status从 1(待付款)变为 2(待发货) - 测试用户取消支付的
cancel错误处理,确认提示文案友好 - 测试 15 分钟超时场景(可将
expireTime设为 30 秒测试)