本文面向有微信小程序开发经验的开发者,系统梳理微信支付从申请到上线的全流程,提供可直接使用的代码示例。
一、微信支付接入前置条件
1.1 资质要求
接入微信支付,你的小程序主体需要满足以下条件:
| 项目 | 要求 |
|---|---|
| 小程序主体 | 企业主体(个体工商户仅限部分类目) |
| 小程序状态 | 已发布上线 |
| 营业执照 | 在有效期内 |
| 对公账户 | 用于结算(个体户可用法人对私账户) |
| 法人身份证 | 正反面照片 |
个人开发者无法直接接入微信支付,但可以通过第三方服务商(如微信支付服务商平台)间接受理。
1.2 商户号申请流程
- 登录 微信支付商户平台,点击"成为商家"
- 填写营业执照信息、法人信息、结算账户
- 等待审核(通常1-3个工作日)
- 审核通过后,通过邮箱查收商户号(mch_id)和API密钥设置链接
- 登录商户平台,设置 APIv3 密钥,下载微信支付平台证书
关键配置项:
mch_id:商户号appid:小程序的 AppIDAPIv3 密钥:用于回调通知解密、敏感信息加解密API 证书序列号:标识你的商户证书商户API私钥:签名使用,务必妥善保管
1.3 绑定小程序与商户号
在商户平台 → 产品中心 → AppID账号管理 中,关联你的小程序 AppID。这一步确保支付时能正确校验调用方身份。
二、完整接入流程
整体流程如下:
code复制
用户点击支付 → 小程序请求后端下单 → 后端调用统一下单API → 微信返回prepay_id
→ 后端签名返回支付参数 → 小程序调用wx.requestPayment → 用户完成支付
→ 微信异步回调后端 → 后端验签更新订单状态
2.1 配置支付域名
在商户平台 → 产品中心 → 开发配置 中,添加你的后端域名(仅 HTTPS)。同时在小程序管理后台 → 开发管理 → 开发设置中,配置 request合法域名 包含你的后端域名。
2.2 前端调起支付的参数
后端需要返回以下参数给前端:
javascript复制
{
timeStamp: '1490840669',
nonceStr: '5K8264ILTKCH16CQ2502SI8ZNMTM67VS',
package: 'prepay_id=wx20170306425458994c08807f1909503344',
signType: 'RSA',
paySign: 'oR9d8PuhnIc+YZlz...' // RSA签名
}
三、前端实现
3.1 完整的支付调用代码
javascript复制
// pages/order/confirm.js
const app = getApp()
Page({
data: {
orderInfo: null,
paying: false
},
// 发起支付
async handlePay() {
if (this.data.paying) return
this.setData({ paying: true })
try {
// 1. 先在本地创建订单(或后端创建)
const orderId = await this.createOrder()
// 2. 请求后端获取支付参数
const payParams = await this.getPayParams(orderId)
// 3. 调起微信支付
await this.wxPay(payParams)
// 4. 支付成功,跳转结果页
wx.redirectTo({
url: `/pages/pay/result?orderId=${orderId}&status=success`
})
} catch (err) {
console.error('支付流程异常:', err)
// 根据错误类型做不同处理
if (err.errMsg && err.errMsg.includes('cancel')) {
// 用户取消支付
wx.redirectTo({
url: `/pages/pay/result?status=cancel&orderId=${orderId}`
})
} else if (err.code === 'ORDER_EXPIRED') {
wx.showModal({
title: '订单已过期',
content: '当前订单已超过支付时效,请重新下单',
showCancel: false
})
} else {
wx.showToast({
title: '支付失败,请重试',
icon: 'none'
})
}
} finally {
this.setData({ paying: false })
}
},
// 调起wx.requestPayment
wxPay(payParams) {
return new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType,
paySign: payParams.paySign,
success(res) {
// 注意:success只代表用户完成了支付动作
// 真正的支付结果以后端回调为准
console.log('支付动作完成:', res)
resolve(res)
},
fail(err) {
console.error('支付失败:', err)
reject(err)
},
// 支付状态查询(用户离开微信后的兜底)
complete() {
// 可在此处做轮询查单
}
})
})
},
// 请求后端获取支付参数
async getPayParams(orderId) {
const res = await wx.request({
url: `${app.globalData.apiBase}/pay/create`,
method: 'POST',
data: { orderId },
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
}
})
if (res.statusCode !== 200 || !res.data.success) {
throw new Error(res.data.message || '获取支付参数失败')
}
return res.data.data
},
// 创建订单
async createOrder() {
const res = await wx.request({
url: `${app.globalData.apiBase}/order/create`,
method: 'POST',
data: {
items: this.data.orderInfo.items,
addressId: this.data.orderInfo.addressId,
remark: this.data.remark
}
})
return res.data.data.orderId
}
})
3.2 支付结果页设计
javascript复制
// pages/pay/result.js
Page({
onLoad(options) {
const { status, orderId } = options
this.setData({ status, orderId })
// 如果是success状态,向后端确认真实支付结果
if (status === 'success') {
this.verifyPayResult(orderId)
}
},
// 轮询确认支付结果(防止回调延迟)
verifyPayResult(orderId) {
let count = 0
const maxRetry = 6
const timer = setInterval(async () => {
count++
try {
const res = await wx.request({
url: `${getApp().globalData.apiBase}/order/${orderId}/pay-status`
})
if (res.data.data.paid) {
clearInterval(timer)
this.setData({ status: 'success', verified: true })
} else if (count >= maxRetry) {
clearInterval(timer)
this.setData({ status: 'pending' })
}
} catch (e) {
if (count >= maxRetry) clearInterval(timer)
}
}, 2000)
},
// 重新支付
retryPay() {
wx.redirectTo({
url: `/pages/order/confirm?orderId=${this.data.orderId}`
})
},
// 查看订单详情
viewOrder() {
wx.redirectTo({
url: `/pages/order/detail?orderId=${this.data.orderId}`
})
},
// 回到首页
backHome() {
wx.switchTab({ url: '/pages/index/index' })
}
})
结果页 WXML:
xml复制
<!-- pages/pay/result.wxml -->
<view class="result-page">
<!-- 成功 -->
<block wx:if="{{status === 'success'}}">
<view class="status-icon success-icon">✓</view>
<view class="status-title">支付成功</view>
<view class="status-desc" wx:if="{{verified}}">订单已确认,商家正在处理</view>
<view class="status-desc" wx:else>支付结果确认中...</view>
<view class="btn-group">
<button class="btn-primary" bindtap="viewOrder">查看订单</button>
<button class="btn-secondary" bindtap="backHome">回到首页</button>
</view>
</block>
<!-- 取消 -->
<block wx:elif="{{status === 'cancel'}}">
<view class="status-icon cancel-icon">!</view>
<view class="status-title">支付已取消</view>
<view class="status-desc">订单尚未支付,可随时重新支付</view>
<view class="btn-group">
<button class="btn-primary" bindtap="retryPay">重新支付</button>
<button class="btn-secondary" bindtap="viewOrder">查看订单</button>
</view>
</block>
<!-- 失败 -->
<block wx:elif="{{status === 'fail'}}">
<view class="status-icon fail-icon">✕</view>
<view class="status-title">支付失败</view>
<view class="status-desc">{{errorMsg || '请检查网络后重试'}}</view>
<view class="btn-group">
<button class="btn-primary" bindtap="retryPay">重新支付</button>
<button class="btn-secondary" bindtap="backHome">回到首页</button>
</view>
</block>
<!-- 待确认 -->
<block wx:elif="{{status === 'pending'}}">
<view class="status-icon pending-icon">⏳</view>
<view class="status-title">支付结果确认中</view>
<view class="status-desc">支付结果正在确认,请稍后在订单详情页查看</view>
<view class="btn-group">
<button class="btn-primary" bindtap="viewOrder">查看订单</button>
<button class="btn-secondary" bindtap="backHome">回到首页</button>
</view>
</block>
</view>
四、后端实现(Node.js)
4.1 项目依赖
bash复制
npm install @wechatpay/node-v3-utils axios crypto-js
4.2 支付配置模块
javascript复制
// config/pay.js
const fs = require('fs')
const path = require('path')
module.exports = {
appId: process.env.WX_APPID,
mchId: process.env.WX_MCH_ID,
apiKey: process.env.WX_API_KEY, // APIv2密钥(兼容用)
apiV3Key: process.env.WX_API_V3_KEY, // APIv3密钥
serialNo: process.env.WX_SERIAL_NO, // 商户证书序列号
privateKey: fs.readFileSync(
path.join(__dirname, '../certs/apiclient_key.pem'), 'utf8'
),
notifyUrl: `${process.env.API_BASE}/pay/notify`,
// 微信支付API基础URL
baseUrl: 'https://api.mch.weixin.qq.com'
}
4.3 统一下单接口
javascript复制
// services/payService.js
const axios = require('axios')
const crypto = require('crypto')
const payConfig = require('../config/pay')
const { createSign, getAuthorization } = require('../utils/wxSign')
class PayService {
/**
* JSAPI下单(小程序支付)
* @param {Object} params
* @param {string} params.orderId 商户订单号
* @param {number} params.amount 金额(元)
* @param {string} params.description 商品描述
* @param {string} params.openid 用户openid
* @returns {Object} 支付参数(给前端调wx.requestPayment)
*/
async createOrder({ orderId, amount, description, openid }) {
// 1. 金额转分(微信支付单位为分)
const total = Math.round(amount * 100)
// 2. 生成商户订单号唯一标识
const outTradeNo = `MINI_${orderId}_${Date.now()}`
// 3. 构造请求体
const body = {
appid: payConfig.appId,
mchid: payConfig.mchId,
description: description.substring(0, 127),
out_trade_no: outTradeNo,
notify_url: payConfig.notifyUrl,
amount: {
total,
currency: 'CNY'
},
payer: {
openid
}
}
// 4. 调用统一下单API
const url = `${payConfig.baseUrl}/v3/pay/transactions/jsapi`
const authorization = getAuthorization('POST', url, body)
const res = await axios.post(url, body, {
headers: {
'Authorization': authorization,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 10000
})
const { prepay_id } = res.data
// 5. 生成前端支付参数并签名
const payParams = this.buildClientPayParams(prepay_id)
// 6. 保存预支付记录
await this.savePrepayRecord(outTradeNo, orderId, total, prepay_id)
return {
...payParams,
outTradeNo
}
}
/**
* 构造前端wx.requestPayment所需参数
*/
buildClientPayParams(prepayId) {
const timeStamp = Math.floor(Date.now() / 1000).toString()
const nonceStr = crypto.randomBytes(16).toString('hex')
// 构造签名串:appid\n时间戳\n随机串\nprepay_id\n
const message = [
payConfig.appId,
timeStamp,
nonceStr,
prepayId
].join('\n')
// RSA签名
const sign = crypto.createSign('RSA-SHA256')
sign.update(message)
const paySign = sign.sign(payConfig.privateKey, 'base64')
return {
appId: payConfig.appId,
timeStamp,
nonceStr,
package: `prepay_id=${prepayId}`,
signType: 'RSA',
paySign
}
}
/**
* 保存预支付记录(防重复下单)
*/
async savePrepayRecord(outTradeNo, orderId, total, prepayId) {
// 写入数据库或Redis
// 防止同一订单重复下单
}
/**
* 查询订单状态
*/
async queryOrder(outTradeNo) {
const url = `${payConfig.baseUrl}/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=${payConfig.mchId}`
const authorization = getAuthorization('GET', url, '')
const res = await axios.get(url, {
headers: { 'Authorization': authorization }
})
return {
tradeState: res.data.trade_state,
tradeStateDesc: res.data.trade_state_desc,
paid: res.data.trade_state === 'SUCCESS',
transactionId: res.data.transaction_id,
payerOpenid: res.data.payer?.openid,
paidTime: res.data.success_time
}
}
}
module.exports = new PayService()
4.4 签名工具
javascript复制
// utils/wxSign.js
const crypto = require('crypto')
const payConfig = require('../config/pay')
/**
* 生成请求签名头
* HTTP方法\n请求URL\n请求体时间戳\n随机串\n
*/
function getAuthorization(method, url, body) {
const timeStamp = Math.floor(Date.now() / 1000).toString()
const nonceStr = crypto.randomBytes(16).toString('hex')
const bodyStr = body ? JSON.stringify(body) : ''
// 构造签名串
const signMessage = [
method,
new URL(url).pathname,
timeStamp,
nonceStr,
bodyStr
].join('\n')
// RSA-SHA256签名
const sign = crypto.createSign('RSA-SHA256')
sign.update(signMessage)
const signature = sign.sign(payConfig.privateKey, 'base64')
// 构造Authorization头
const authStr = [
`WECHATPAY2-SHA256-RSA2048`,
`mchid="${payConfig.mchId}"`,
`nonce_str="${nonceStr}"`,
`timestamp="${timeStamp}"`,
`serial_no="${payConfig.serialNo}"`,
`signature="${signature}"`
].join(', ')
return authStr
}
/**
* 验证回调通知签名
*/
function verifyNotifySign(headers, body, timestamp, nonce, signature) {
const { 'wechatpay-timestamp': ts, 'wechatpay-nonce': n, 'wechatpay-serial': serial, 'wechatpay-signature': sig } = headers
// 构造验签串
const message = `${ts}\n${n}\n${body}\n`
// 使用微信平台公钥验签(需提前下载)
const verify = crypto.createVerify('RSA-SHA256')
verify.update(message)
return verify.verify(wechatPayPublicKey, sig, 'base64')
}
module.exports = { getAuthorization, verifyNotifySign }
4.5 回调通知处理
javascript复制
// controllers/payController.js
const express = require('express')
const crypto = require('crypto')
const payConfig = require('../config/pay')
const router = express.Router()
/**
* 支付结果回调通知
* 微信会以POST方式通知该接口,重复通知直到返回200
*/
router.post('/notify', async (req, res) => {
try {
// 1. 验证签名(防止伪造回调)
const isValid = verifyNotifySign(req)
if (!isValid) {
console.error('回调签名验证失败')
return res.status(401).json({ code: 'FAIL', message: '签名验证失败' })
}
// 2. 解密通知内容(AES-256-GCM)
const { resource } = req.body
const {
ciphertext,
associated_data,
nonce
} = resource
const decrypted = decryptAES256GCM(
ciphertext,
associated_data,
nonce,
payConfig.apiV3Key
)
const orderData = JSON.parse(decrypted)
// 3. 处理不同交易状态
switch (orderData.trade_state) {
case 'SUCCESS':
await handlePaySuccess(orderData)
break
case 'NOTPAY':
console.log('订单未支付:', orderData.out_trade_no)
break
case 'CLOSED':
await handlePayClosed(orderData)
break
case 'REFUND':
await handlePayRefund(orderData)
break
default:
console.warn('未知交易状态:', orderData.trade_state)
}
// 4. 返回成功响应(重要!不返回200微信会重复通知)
res.json({ code: 'SUCCESS', message: '成功' })
} catch (err) {
console.error('支付回调处理异常:', err)
// 返回失败让微信重试
res.status(500).json({ code: 'FAIL', message: err.message })
}
})
/**
* AES-256-GCM解密
*/
function decryptAES256GCM(ciphertext, associatedData, nonce, apiKey) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
Buffer.from(apiKey, 'utf8'),
Buffer.from(nonce, 'utf8')
)
decipher.setAAD(Buffer.from(associatedData, 'utf8'))
const ciphertextBuf = Buffer.from(ciphertext, 'base64')
// GCM模式:密文末尾16字节为auth tag
const authTag = ciphertextBuf.subarray(ciphertextBuf.length - 16)
const encryptedData = ciphertextBuf.subarray(0, ciphertextBuf.length - 16)
decipher.setAuthTag(authTag)
const decrypted = Buffer.concat([
decipher.update(encryptedData),
decipher.final()
])
return decrypted.toString('utf8')
}
/**
* 处理支付成功逻辑
*/
async function handlePaySuccess(orderData) {
const { out_trade_no, transaction_id, payer, success_time, amount } = orderData
// 幂等性检查:防止重复处理同一笔回调
const processed = await checkCallbackProcessed(transaction_id)
if (processed) {
console.log('回调已处理过:', transaction_id)
return
}
// 更新订单状态
await updateOrderStatus(out_trade_no, {
paid: true,
transactionId: transaction_id,
payerOpenid: payer.openid,
paidTime: success_time,
paidAmount: amount.total
})
// 标记回调已处理
await markCallbackProcessed(transaction_id)
// 触发业务逻辑:发货通知、库存扣减等
await triggerAfterPayHooks(out_trade_no)
}
module.exports = router
五、支付安全
5.1 防重复支付中间件
javascript复制
// middleware/payGuard.js
/**
* 防重复支付中间件
* 同一订单在短时间内只能发起一次支付
*/
function preventDuplicatePay(redisClient) {
return async (req, res, next) => {
const { orderId } = req.body
const userId = req.user.id
const lockKey = `pay_lock:${userId}:${orderId}`
// 尝试获取分布式锁(5分钟过期)
const acquired = await redisClient.set(lockKey, '1', {
NX: true,
EX: 300
})
if (!acquired) {
return res.status(429).json({
success: false,
message: '支付请求处理中,请勿重复提交'
})
}
res.locals.payLockKey = lockKey
next()
}
}
/**
* 防金额篡改校验
* 下单时记录金额,回调时比对
*/
async function verifyPayAmount(outTradeNo, actualTotal) {
const order = await getOrder(outTradeNo)
if (!order) throw new Error('订单不存在')
// 订单已支付
if (order.paid) throw new Error('订单已支付')
// 金额比对
const expectedTotal = Math.round(order.amount * 100)
if (expectedTotal !== actualTotal) {
console.error(`金额不一致: 预期${expectedTotal} 实际${actualTotal}`)
throw new Error('支付金额与订单金额不一致')
}
}
/**
* 防回调伪造
* 验证微信回调的签名 + IP白名单
*/
function validateCallbackSource(req) {
// 1. 验证签名
const signatureValid = verifyNotifySign(req)
if (!signatureValid) return false
// 2. IP白名单(微信回调IP段)
const clientIp = req.ip
const wechatPayIpRanges = [
'101.226.', '101.227.', // 上海
'180.163.', // 上海
'140.207.', // 上海
'203.205.', '203.205.147.' // 广州
]
return wechatPayIpRanges.some(range => clientIp.startsWith(range))
}
module.exports = { preventDuplicatePay, verifyPayAmount, validateCallbackSource }
5.2 安全配置清单
javascript复制
// 安全检查清单
const SECURITY_CHECKLIST = {
// 1. 密钥安全
'API密钥不硬编码': '使用环境变量或密钥管理服务',
'私钥文件权限': '仅应用服务可读 (chmod 400)',
'APIv3密钥强度': '至少32位随机字符串',
// 2. 签名验证
'回调签名验证': '必须验签,不验签=裸奔',
'请求签名': '所有API请求均需签名',
'时间戳校验': '请求时间戳5分钟内有效',
// 3. 业务安全
'订单金额校验': '回调时比对原始订单金额',
'幂等处理': '同一笔回调不重复处理',
'分布式锁': '防止并发重复下单',
// 4. 数据安全
'敏感信息脱敏': '日志中不打印完整密钥/手机号',
'HTTPS强制': '回调URL必须HTTPS',
'数据库加密': '支付敏感字段加密存储'
}
六、常见问题排查
6.1 签名错误
症状 :return_code=FAIL, return_msg=签名错误
排查步骤:
- 确认签名类型:V3接口使用 RSA-SHA256,不是 MD5
- 检查签名串格式:
字段之间用\n分隔,不要有空格 - 确认商户证书序列号与密钥匹配
- 检查请求体是否与签名时使用的完全一致(JSON序列化顺序可能不同)
javascript复制
// 调试工具:打印签名串用于对比
function debugSign(method, url, body) {
const bodyStr = body ? JSON.stringify(body) : ''
console.log('签名原文:')
console.log([method, new URL(url).pathname, '时间戳', '随机串', bodyStr].join('\n'))
}
6.2 订单过期
症状 :trade_state=NOTPAY 或前端报 ORDER_EXPIRED
解决方案:
- 微信预支付订单默认2小时过期,可通过
time_expire参数自定义(最长24小时) - 建议在前端显示倒计时,过期前引导用户重新下单
- 过期订单不需要手动关闭,微信会自动关闭
6.3 回调未收到
排查清单:
code复制
□ 回调URL是否配置正确(商户平台→开发配置→回调地址)
□ 服务器是否能被公网访问
□ HTTPS证书是否有效
□ 是否正确返回了 { code: 'SUCCESS' } 响应体
□ 检查服务器防火墙和安全组规则
□ 查看微信商户平台→交易中心→订单查询中的通知状态
6.4 支付金额不一致
根本原因:前端传给后端的金额与实际支付金额存在精度问题。
javascript复制
// ✅ 正确:使用整数分计算
const totalFen = Math.round(parseFloat(amountYuan) * 100)
// ❌ 错误:浮点数直接计算
const totalFen = amountYuan * 100 // 0.1 * 100 = 10.000000000000002
强制规范:所有金额计算在服务端完成,前端仅传递数量和商品ID,后端查库计算金额。
七、微信支付新能力(2025+)
7.1 免密支付(委托代扣)
适用于周期性扣费场景(如会员续费、打车预扣费):
javascript复制
// 签约流程
// 1. 前端引导用户签署代扣协议
wx.navigateToMiniProgram({
appId: 'wx1234567890abcdef', // 微信支付签约小程序
path: 'pages/contract/index',
extraData: {
mch_id: '商户号',
plan_id: '签约计划ID',
contract_code: '合约号'
}
})
// 2. 后端调用委托代扣API扣费
// POST /v3/pay/transactions/papay/apply
7.2 微信先享卡
一种"先享后付"的营销工具,用户承诺达标后享受优惠,未达标自动扣费:
code复制
接入流程:商户平台→产品中心→先享卡→申请→配置规则
用户路径:小程序内展示先享卡→用户领取→达标享优惠/未达标扣费
7.3 分账功能
适合平台型小程序(如外卖、分销):
javascript复制
// 统一下单时指定分账规则
const body = {
// ...其他参数
settle_info: {
profit_sharing: true
}
}
// 支付成功后调用分账API
// POST /v3/profitsharing/transactions
八、总结
微信支付接入的核心要点:
- 资质先行:企业主体、商户号、API密钥缺一不可
- 安全为本:签名验证、金额校验、幂等处理是底线
- 回调可靠:异步回调是确认支付的唯一标准,前端结果仅供参考
- 错误兜底:轮询查单作为回调的补充手段
- 日志完整:全链路记录请求/响应,问题排查时事半功倍
完整代码示例可在官方文档 微信支付开发者文档 中找到更多细节,建议与本文对照阅读。
