在源码交易场景中,用户可能通过微信小程序浏览商品,也可能通过浏览器直接访问 H5 商城。如果为每个端单独维护一套支付系统,不仅开发成本翻倍,后续的订单对账、退款处理也会变成噩梦。一套优雅的混合支付方案,能让小程序和 H5 使用同一个后端接口、同一套订单数据,仅通过前端支付方式做差异化适配。
本文将以微信支付为例,完整讲解小程序 + H5 混合支付方案的设计思路与代码实现,涵盖统一下单、前端拉起支付、订单查询与回调通知全流程。
一、整体架构设计
核心思路很简单:后端只做一件事——生成预支付订单并返回支付参数。至于前端是用小程序 API 拉起支付,还是用 H5 跳转收银台,完全由前端根据自身环境决定。
架构分为三层:
- 前端层:小程序端调用
wx.requestPayment,H5 端使用WeixinJSBridge.invoke('getBrandWCPayRequest')或 JSAPI 跳转 - 后端统一接口:一个
/api/pay/unified接口,接收订单信息 + 支付渠道 + 客户端类型,返回不同格式的支付参数 - 支付网关层:封装微信支付 API,处理签名、请求、回调
# 请求示例 POST /api/pay/unified { "order_id": "ORD20260712001", "amount": 99.00, "subject": "转卡码系统企业版", "channel": "wechat", # 支付渠道: wechat / alipay "client_type": "mini_program" # 客户端: mini_program / h5 / app } # 小程序端返回 { "code": 0, "data": { "nonceStr": "xxx", "package": "prepay_id=wx...", "paySign": "xxx", "signType": "MD5", "timeStamp": "1720761600" } } # H5 端返回 { "code": 0, "data": { "h5_url": "https://wx.tenpay.com/cgi-bin/mmpayweb/xxx" } }
二、后端统一支付接口实现
我们使用 Python(Flask)实现统一支付接口。首先定义一个支付网关类,封装微信支付的统一下单逻辑:
import hashlib import xml.etree.ElementTree as ET import requests import random import string from datetime import datetime # 微信支付配置 WECHAT_CONFIG = { "appid": "wx_your_appid", "mch_id": "your_mch_id", "api_key": "your_api_key_32bytes", "notify_url": "https://your-domain.com/api/pay/notify" } class WechatPayGateway: @staticmethod def _gen_nonce_str(length=16): return ''.join(random.choices( string.ascii_letters + string.digits, k=length )) @staticmethod def _gen_sign(params, api_key): # 1. 按 key 字典序排序 sorted_keys = sorted(params.keys()) raw = '&'.join( f'{k}={params[k]}' for k in sorted_keys if params[k] != '' and k != 'sign' ) raw += f'&key={api_key}' # 2. MD5 签名 return hashlib.md5(raw.encode('utf-8')).hexdigest().upper() @staticmethod def _to_xml(params): root = ET.Element('xml') for k, v in params.items(): child = ET.SubElement(root, k) child.text = str(v) return ET.tostring(root, encoding='utf-8') @staticmethod def _parse_xml(xml_str): root = ET.fromstring(xml_str) return {child.tag: child.text for child in root} @classmethod def unified_order(cls, out_trade_no, total_fee, body, client_type='mini_program'): """ 统一下单 :param client_type: mini_program | h5 | app """ params = { 'appid': WECHAT_CONFIG['appid'], 'mch_id': WECHAT_CONFIG['mch_id'], 'nonce_str': cls._gen_nonce_str(), 'body': body, 'out_trade_no': out_trade_no, 'total_fee': int(total_fee * 100), # 单位: 分 'spbill_create_ip': '127.0.0.1', 'notify_url': WECHAT_CONFIG['notify_url'], 'trade_type': 'JSAPI' if client_type in ( 'mini_program', 'h5' ) else 'APP', } # H5 支付额外参数 if client_type == 'h5': params['trade_type'] = 'MWEB' params['scene_info'] = ( '{"h5_info": {"type":"Wap","wap_url":' '"https://greenfield.ltd/store/","wap_name":"源码商城"}}' ) # 小程序支付需要传 openid if client_type == 'mini_program': params['openid'] = cls._get_current_openid() params['sign'] = cls._gen_sign(params, WECHAT_CONFIG['api_key']) xml_data = cls._to_xml(params) url = 'https://api.mch.weixin.qq.com/pay/unifiedorder' resp = requests.post(url, data=xml_data, timeout=10) result = cls._parse_xml(resp.content) if result.get('return_code') == 'SUCCESS' \ and result.get('result_code') == 'SUCCESS': prepay_id = result['prepay_id'] return cls._build_frontend_params( prepay_id, client_type ) raise Exception(f"下单失败: {result.get('return_msg', '未知错误')}") @classmethod def _build_frontend_params(cls, prepay_id, client_type): # 小程序: 返回 wx.requestPayment 所需参数 if client_type == 'mini_program': pkg = f'prepay_id={prepay_id}' params = { 'appId': WECHAT_CONFIG['appid'], 'nonceStr': cls._gen_nonce_str(), 'package': pkg, 'signType': 'MD5', 'timeStamp': str(int(datetime.now().timestamp())), } params['paySign'] = cls._gen_sign( params, WECHAT_CONFIG['api_key'] ) return { 'nonceStr': params['nonceStr'], 'package': pkg, 'paySign': params['paySign'], 'signType': 'MD5', 'timeStamp': params['timeStamp'], } # H5: 返回跳转链接 if client_type == 'h5': return { 'h5_url': ( f'https://wx.tenpay.com/cgi-bin/' f'mmpayweb-bin/checkmweb?prepay_id=' f'{prepay_id}&package=xxx' ) } return {'prepay_id': prepay_id}
三、小程序端拉起支付
在小程序端,调用后端接口获取支付参数后,使用 wx.requestPayment 拉起支付面板:
// 小程序端支付代码 function doPay(orderId) { wx.request({ url: 'https://your-domain.com/api/pay/unified', method: 'POST', data: { order_id: orderId, amount: 99.00, subject: '转卡码系统企业版', channel: 'wechat', client_type: 'mini_program' }, success(res) { if (res.data.code !== 0) { wx.showToast({ title: '下单失败', icon: 'error' }); return; } const payParams = res.data.data; wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType, paySign: payParams.paySign, success() { // 支付成功,跳转到订单详情 wx.navigateTo({ url: `/pages/order/detail?orderId=${orderId}` }); }, fail(err) { wx.showToast({ title: '支付取消', icon: 'none' }); } }); } }); }
四、H5 端拉起支付
在 H5 端,如果是微信内浏览器,使用 WeixinJSBridge;如果是普通浏览器,使用 MWEB 跳转:
// H5 端支付 async function h5Pay(orderId) { const resp = await fetch( 'https://your-domain.com/api/pay/unified', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order_id: orderId, amount: 99.00, subject: '转卡码系统企业版', channel: 'wechat', client_type: 'h5' }) } ); const data = (await resp.json()).data; // 判断是否在微信内置浏览器 const isWechat = navigator.userAgent.toLowerCase() .includes('micromessenger'); if (isWechat) { // 微信浏览器中使用 WeixinJSBridge if (typeof WeixinJSBridge === 'undefined') { document.addEventListener('WeixinJSBridgeReady', () => { doWechatPay(data); }); } else { doWechatPay(data); } } else { // 普通浏览器直接跳转 MWEB window.location.href = data.h5_url; } } function doWechatPay(params) { WeixinJSBridge.invoke( 'getBrandWCPayRequest', { appId: params.appId, timeStamp: params.timeStamp, nonceStr: params.nonceStr, package: params.package, signType: params.signType, paySign: params.paySign }, function(res) { if (res.err_msg === 'get_brand_wcpay_request:ok') { window.location.href = '/store/order/success.html'; } else { alert('支付取消或失败'); } } ); }
五、回调通知与订单状态同步
支付完成后,微信会异步通知你的 notify_url。后端需要验证签名并更新订单状态:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/pay/notify', methods=['POST'])
def pay_notify():
xml_data = request.data
result = WechatPayGateway._parse_xml(xml_data)
# 验证签名
sign = result.pop('sign', '')
expected = WechatPayGateway._gen_sign(
result, WECHAT_CONFIG['api_key']
)
if sign != expected:
return WechatPayGateway._to_xml({
'return_code': 'FAIL',
'return_msg': '签名验证失败'
})
if result.get('return_code') == 'SUCCESS' \
and result.get('result_code') == 'SUCCESS':
out_trade_no = result['out_trade_no']
transaction_id = result['transaction_id']
total_fee = int(result['total_fee']) / 100.0
# 更新订单状态为已支付
update_order_status(out_trade_no, 'paid', transaction_id)
# 如果是数字商品,触发自动发货
auto_deliver(out_trade_no)
# 返回成功应答
return WechatPayGateway._to_xml({
'return_code': 'SUCCESS',
'return_msg': 'OK'
})
return WechatPayGateway._to_xml({
'return_code': 'FAIL',
'return_msg': '支付失败'
})
这里有一个容易被忽略的细节:回调通知有可能重复到达(微信最多重试 3 次)。因此 update_order_status 函数必须是幂等的:
def update_order_status(order_id, status, transaction_id):
# 使用乐观锁,避免重复更新
sql = """
UPDATE orders
SET status = %s,
transaction_id = %s,
pay_time = NOW()
WHERE order_id = %s
AND status = 'pending' -- 只更新待支付的订单
"""
# 执行 SQL...
六、订单查询与退款
实际运营中还需要提供订单查询和退款接口。后端同样只需暴露一个统一接口,前端传 client_type 区分来源:
@app.route('/api/pay/query', methods=['GET'])
def query_order():
order_id = request.args.get('order_id')
# 查询本地订单状态
order = get_order_by_id(order_id)
return jsonify({
'code': 0,
'data': {
'order_id': order.order_id,
'status': order.status,
'amount': order.amount,
'pay_time': order.pay_time
}
})
@app.route('/api/pay/refund', methods=['POST'])
def refund_order():
data = request.json
order_id = data['order_id']
refund_amount = data.get('amount', 0)
# 调用微信退款 API
order = get_order_by_id(order_id)
refund_params = {
'appid': WECHAT_CONFIG['appid'],
'mch_id': WECHAT_CONFIG['mch_id'],
'nonce_str': WechatPayGateway._gen_nonce_str(),
'out_trade_no': order_id,
'out_refund_no': f'RF{order_id}',
'total_fee': int(order.amount * 100),
'refund_fee': int(refund_amount * 100) if refund_amount
else int(order.amount * 100),
}
refund_params['sign'] = WechatPayGateway._gen_sign(
refund_params, WECHAT_CONFIG['api_key']
)
# ... 调用微信退款接口
return jsonify({'code': 0, 'msg': '退款申请已提交'})
七、部署注意事项
混合支付方案在实际部署时,有几个关键点需要特别注意:
- 域名一致性:微信支付 JSAPI 和小程序支付都需要在微信商户平台配置授权域名/IP,H5 支付还需要配置
jsapi_ticket刷新机制 - OpenID 获取:小程序端通过
wx.login获取 code 换取 openid;H5 端在微信浏览器中通过 OAuth2 静默授权获取 - 环境隔离:建议在测试环境和生产环境使用不同的商户号,避免测试数据污染
- 日志记录:每个支付请求都要记录完整的请求参数和微信返回结果,方便排查问题
# Nginx 配置反向代理到支付服务
server {
listen 443 ssl;
server_name your-domain.com;
location /api/pay/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
}
}
八、从理论到实战
以上代码展示了小程序 + H5 混合支付方案的核心骨架。你完全可以基于这套架构扩展出更多功能:支付宝小程序支付、App 内支付、多商户分账等。如果你不想从零搭建,源码商城的转卡码系统 V3 已经内置了完整的小程序 + H5 混合支付模块,开箱即用,支持微信和支付宝双通道。
对于需要 AI 编程辅助开发的同学,Codex Desktop Linux 版 可以在本地帮你编写和调试支付代码,让开发效率翻倍。