
记一次网页端微信支付接入复盘
前情提要
这次给一个 Vue + Spring Boot 的会员系统接入微信支付,最开始以为只要做一个二维码就够了,后来才发现这件事在网页端有三个完全不同的场景:
- 电脑浏览器打开:适合
Native扫码支付。 - 手机微信内打开:适合
JSAPI支付。 - 手机普通浏览器打开:适合
H5支付。
一开始我们只做了 Native 支付:后端调用微信支付的 Native 下单接口拿到 code_url,前端把它转成二维码展示。这在 PC 上没问题,但手机上如果用户长按识别二维码,或者截图后再扫码,会被微信支付拦截。原因是 Native 的官方场景本来就是“商户系统展示二维码,用户用微信扫一扫”,不是手机网页内长按识别。
最终方案是:桌面端继续 Native;微信内网页走 JSAPI;微信外移动端走 H5。
最终架构
支付入口仍然只有一个“创建订单”接口,但前端会根据环境传入不同的 payType:
1 | function preferredPayType(): WechatPayType { |
后端根据 payType 调用不同的微信支付服务:
native:返回codeUrl,前端生成二维码。jsapi:返回appId/timeStamp/nonceStr/package/signType/paySign,前端调用WeixinJSBridge.invoke。h5:返回h5Url,前端跳转微信收银台。
后端配置
后端用的是微信支付 Java SDK:
1 | <dependency> |
配置项要同时覆盖商户号、证书、公钥、公众号 AppID/AppSecret、回调地址、H5 场景信息:
1 | web: |
实际生产环境建议通过 production.env 注入,不要把 AppSecret、APIv3 Key、私钥写进仓库。私有仓库也不建议放密钥,因为之后很容易复制、迁移、开源或泄露。
SDK 初始化
一开始只初始化了 NativePayService,后来扩展为三个服务:
1 | this.nativePayService = new NativePayService.Builder().config(config).build(); |
这里还有一个细节:我们使用的是“微信支付公钥模式”,需要配置:
- 商户号
mchId - 商户私钥
apiclient_key.pem - 商户证书序列号
- 微信支付公钥文件
pub_key.pem - 微信支付公钥 ID,形如
PUB_KEY_ID_xxx - APIv3 Key
新商户很多时候已经不是老的“平台证书自动下载”模式了,优先确认商户平台 API 安全里实际给你的是什么。
订单创建流程
后端创建订单时先落库,再调用微信预下单:
1 | MemberOrder order = MemberOrder.builder() |
为什么先落库?因为微信回调、主动查单、前端轮询都需要通过 outTradeNo 找到本地订单。如果先调用微信再落库,极端情况下回调可能先到,反而查不到订单。
下单返回值统一带上:
1 | data.put("payType", type); |
再根据支付方式补充具体字段:
1 | if ("jsapi".equals(type)) { |
JSAPI 支付的关键:openid
微信内网页 JSAPI 支付必须要 openid,而这个 openid 必须来自同一个公众号 AppID。
前端发起支付时,如果当前在微信里打开,就先走网页授权:
1 | function redirectWechatOauth() { |
这里用的是 snsapi_base,它是静默授权,只用于拿 openid,不会弹出用户信息授权页。
后端用微信回传的 code 换 openid:
1 | URI uri = UriComponentsBuilder |
拿到 openid 后再调用 JSAPI 下单:
1 | Payer payer = new Payer(); |
前端拿到后调起微信支付:
1 | WeixinJSBridge.invoke('getBrandWCPayRequest', params, (res) => { |
H5 支付的处理
微信外的手机浏览器不在微信容器里,不能调用 WeixinJSBridge,这时走 H5 支付。
后端 H5 下单需要传 scene_info:
1 | SceneInfo sceneInfo = new SceneInfo(); |
前端拿到 h5Url 后跳转:
1 | function withH5Redirect(h5Url: string) { |
跳走前要把订单号存在 sessionStorage,用户从收银台回跳后继续轮询:
1 | sessionStorage.setItem(H5_PENDING_KEY, JSON.stringify({ |
回调和轮询
支付成功最终以微信异步回调为准:
1 | Transaction t = wechatPayConfig.notificationParser().parse(requestParam, Transaction.class); |
同时保留前端轮询和主动查单。这样即使微信回调延迟或丢失,用户前端也有机会触发后端主动向微信查单并补发会员:
1 | Transaction t = wechatPayConfig.nativePay().queryOrderByOutTradeNo(q); |
注意:查询订单接口在 SDK 里 Native/JSAPI/H5 都能查同一个商户订单,实际项目里继续复用 Native 的查询服务即可。
公众号后台和商户平台配置
这是最容易踩坑的部分。代码写完不代表能支付,后台配置必须全部对上。
公众号后台需要配置:
- 公众号必须是已认证服务号。
网页授权域名:填写vip.offerjob.cn。JS接口安全域名:也建议填写vip.offerjob.cn。- 校验文件要能从根目录访问,例如
https://vip.offerjob.cn/MP_verify_xxx.txt。
商户平台需要配置:
- 当前公众号 AppID 必须和商户号绑定。
- JSAPI 支付产品必须开通。
- H5 支付产品必须开通。
- H5 支付域名必须配置为实际站点域名。
- 商户证书、公钥、APIv3 Key 要和同一个商户号对应。
这次最终卡住的一次错误是:
1 | APPID_MCHID_NOT_MATCH |
含义非常明确:公众号 AppID 没有绑定当前商户号,或者用错了商户号/证书。解决方法是到商户平台把公众号 AppID 关联到商户号,或者换成该公众号实际绑定的那套商户号和证书。
常见错误记录
长按二维码或截图扫码被拦截
这是因为手机端误用了 Native 支付。Native 二维码适合 PC 展示给微信“扫一扫”,不适合手机网页长按识别。解决:微信内走 JSAPI,微信外手机浏览器走 H5。
此公众号没有这些 scope 的权限
通常是 AppID 用错了,或者公众号不支持网页授权。确认:
- 用的是公众号/服务号 AppID,不是小程序 AppID。
- 公众号是已认证服务号。
- 使用
snsapi_base获取 openid。
redirect_uri 域名与后台配置不一致,错误码 10003
说明公众号后台的“网页授权域名”和实际 redirect_uri 域名不匹配。注意不是 JS 接口安全域名。
正确配置类似:
1 | 网页授权域名 = vip.offerjob.cn |
代码里最好把授权回调固定成稳定域名:
1 | VITE_WECHAT_PAY_REDIRECT_ORIGIN=https://vip.offerjob.cn/ |
不要让它跟随当前页面地址,否则用户从别名域名、测试域名、带奇怪参数的页面进入时,很容易触发 10003。
发起支付失败,请重试
这个是我们后端统一包了一层友好错误。真正原因要看服务端日志,重点搜:
1 | journalctl -u pushcode --since "30 minutes ago" --no-pager | grep -E "微信预下单失败|APPID|MCH|INVALID|支付" -C 4 |
如果看到 APPID_MCHID_NOT_MATCH,就去商户平台检查 AppID 和商户号绑定关系。
部署 Checklist
下次接入微信支付,可以按这个顺序来:
- 确认产品场景:PC Native、微信内 JSAPI、微信外 H5。
- 准备商户号、APIv3 Key、商户私钥、证书序列号、微信支付公钥/平台证书。
- 确认公众号 AppID 和商户号已绑定。
- 公众号后台配置网页授权域名和 JS 接口安全域名。
- 商户平台开通 JSAPI 支付、H5 支付,并配置 H5 支付域名。
- 把微信校验文件放到前端
public,部署后确认根路径可访问。 - 后端实现统一下单接口,按
payType分流 Native/JSAPI/H5。 - 前端根据 UA 判断支付方式。
- JSAPI 前先走 OAuth
snsapi_base获取openid。 - H5 跳转前保存订单号,回跳后继续轮询。
- 回调验签解密后幂等发放权益。
- 前端轮询时保留主动查单兜底。
最终心得
微信支付真正麻烦的地方不在代码,而在“身份关系”:
AppID属于公众号或小程序。openid只在对应 AppID 下有效。mch_id属于商户平台。AppID必须绑定到mch_id才能支付。- 商户证书、公钥、APIv3 Key 必须属于同一个商户号。
- 网页授权域名、JS 安全域名、H5 支付域名是三套不同配置。
只要把这些关系理顺,代码反而比较直接。PC 用 Native,微信内用 JSAPI,微信外用 H5,这就是网页端微信支付最稳的接入方式。



