一篇搞懂微信支付:个人收款码、Native、H5、JSAPI、小程序、APP 支付全流程
做 Java 后端项目时,只要涉及:
会员充值
付费阅读
购买课程
商城下单
VIP 开通
知识付费
基本都会碰到一个问题:
用户付完钱以后,我的系统怎么知道他已经支付成功了?
很多个人项目最开始采用的是:
个人收款码
用户扫码付款之后,再通过:
截图
邮箱
微信
人工核实
确认用户已经付款。
项目稍微正规一点,则会接入:
微信支付 API
最终实现:
用户下单
↓
微信支付
↓
用户付款
↓
微信回调后端
↓
订单变成已支付
↓
自动发货 / 自动解锁
本文就从个人收款码开始,把目前开发中最常见的微信支付方式一次搞清楚。
一、先搞懂:个人收款码和微信支付 API 是两回事
很多初学者会认为:
微信收款码
=
微信支付
其实并不是。
个人收款码
最简单的方式就是:
用户
↓
扫描你的个人收款码
↓
付款 9.9 元
↓
钱进入你的微信账户
但是对于你的 Java 后端来说,会存在一个非常关键的问题:
Spring Boot
↓
???
↓
怎么知道用户付款了?
普通个人收款码并不是你的网站订单支付 API。
也就是说,你不能简单期待:
用户付款
↓
微信服务器
↓
POST /pay/notify
↓
你的 Java 系统
自动告诉你的业务系统:
订单 10086 已支付。
所以个人项目经常会设计成:
生成订单
↓
展示个人收款码
↓
用户扫码支付
↓
用户提交支付凭证
↓
作者收到邮件
↓
人工核对
↓
点击确认
↓
订单改成已支付
↓
文章解锁
这里非常有意思。
表面上看:
作者点了一下“确认收款”
实际上作者是在人工完成:
支付回调
也就是:
正式微信支付:
微信服务器
↓
自动通知系统
个人收款码:
作者
↓
人工通知系统
所以:
个人收款码最大的限制,并不是“收不到钱”,而是很难建立标准的订单、支付通知、退款等自动化支付闭环。
二、为什么正式微信支付可以自动解锁?
因为商户支付建立了一套完整的:
订单系统
+
支付系统
+
通知机制
比如用户购买:
Java 高并发课程
价格:99 元
你的系统先生成订单:
order_no = 202609020001
amount = 9900
status = NOT_PAY
注意微信支付通常使用:
分
作为金额单位。
所以:
99 元
=
9900 分
然后后端向微信支付请求:
我要创建一个 9900 分的支付订单。
微信返回支付信息。
用户付款之后:
微信支付平台
↓
POST
↓
https://xxx.com/wx/payNotify
↓
Spring Boot
↓
验签 + 解密
↓
发现订单支付成功
↓
UPDATE order
SET status = 'PAID'
然后:
课程解锁
会员开通
文章解锁
订单发货
全部可以自动执行。
这就是:
支付回调。
三、微信支付最重要的一张图
无论你使用:
Native
H5
JSAPI
小程序
APP
核心思想其实都差不多:
用户
│
点击支付
│
▼
业务系统
│
创建业务订单
│
▼
MySQL
│
调微信下单接口
│
▼
微信支付平台
│
返回支付所需参数
│
▼
前端
│
用户完成支付
│
▼
微信支付平台
│
支付结果通知
│
▼
/pay/notify
│
验签 + 解密
│
▼
更新订单
│
▼
支付成功
│
┌─────────┼─────────┐
▼ ▼ ▼
解锁文章 开通会员 发货
你以后学习任何支付 SDK,都不要被代码绕进去。
始终记住:
创建业务订单
↓
微信下单
↓
用户付款
↓
支付回调
↓
更新订单
↓
执行支付后的业务
这才是支付系统真正的主线。
四、微信支付到底有哪些方式?
目前开发中最常见的可以先记这 6 种:
| 支付方式 | 常见场景 |
|---|---|
| Native 支付 | PC 网站二维码支付 |
| H5 支付 | 微信外手机浏览器 |
| JSAPI 支付 | 微信内部网页 / 公众号 |
| 小程序支付 | 微信小程序 |
| APP 支付 | iOS / Android App |
| 付款码支付 | 线下商家扫码枪扫用户付款码 |
微信官方目前也将 Native、JSAPI、App、小程序、H5、付款码等作为主要支付场景。(WeChat Pay )
下面分别来看。
五、Native 支付:PC 网站最常见
如果你开发的是:
PC 商城
知识付费网站
课程网站
博客付费阅读
最典型的就是:
Native 支付
用户体验类似:
PC 页面
↓
点击“微信支付”
↓
出现二维码
↓
手机微信扫一扫
↓
付款
微信官方将 Native 定义为商户生成支付二维码,用户使用微信“扫一扫”完成支付的方式,也是 PC 网站非常典型的支付场景。(WeChat Pay )
后端流程:
Spring Boot
↓
调用 Native 下单
↓
微信返回 code_url
↓
weixin://xxxxxx
↓
前端生成二维码
↓
用户扫码支付
例如:
PrepayResponse response =
nativePayService.prepay(request);
String codeUrl =
response.getCodeUrl();
得到:
weixin://wxpay/bizpayurl?pr=xxxx
前端通过二维码组件:
code_url
↓
二维码
用户扫码即可。
所以:
Native 支付最核心的东西就是
code_url。
六、H5 支付:微信外手机浏览器
假设用户不是在微信里面打开网站。
而是在:
Safari
Chrome
手机系统浏览器
访问:
https://shop.xxx.com
然后点击:
微信支付
这时候通常使用:
H5 支付
流程:
手机浏览器
↓
点击微信支付
↓
后端 H5 下单
↓
微信返回 h5_url
↓
浏览器访问
↓
唤起微信
↓
用户付款
所以:
Native:
code_url
↓
生成二维码
H5:
h5_url
↓
跳转 / 唤起微信
区别马上就出来了。
微信官方目前将 H5 支付定位在微信外的手机浏览器场景,并要求对应商户开通 H5 支付权限以及配置相关支付域名。(WeChat Pay )
需要特别注意资质。
截至目前微信支付官方主体权限说明中,企业支持 H5 支付,而个体工商户和小微商户并不在基础 H5 支付支持范围内,因此最终一定以商户平台当前实际开放能力为准。(WeChat Pay )
七、JSAPI 支付:微信里面打开网页
JSAPI 最容易和 H5 混。
记一个简单规则:
普通手机浏览器
↓
H5
微信里面打开的网页
↓
JSAPI
比如:
公众号菜单
↓
进入商城
↓
点击买会员
↓
直接弹微信支付
就是非常典型的:
JSAPI 支付
流程:
微信浏览器
↓
打开网页
↓
Spring Boot 下单
↓
得到 prepay_id
↓
后端生成调起支付所需参数
↓
前端调用微信 JSAPI
↓
微信收银台
后端通常需要准备:
appId
timeStamp
nonceStr
package
signType
paySign
其中:
package =
prepay_id=xxxx
JSAPI 的网页调起支付需要重新完成签名,官方目前同样明确要求使用 appId、timeStamp、nonceStr、package、signType 等参数。(WeChat Pay )
所以 JSAPI 相比 Native 会复杂一点。
Native:
拿 code_url
↓
二维码
JSAPI:
拿 prepay_id
↓
生成签名参数
↓
前端调微信支付
八、小程序支付
如果你的业务直接运行在:
微信小程序
那么使用:
小程序支付
例如:
奶茶小程序
商城小程序
外卖小程序
课程小程序
用户:
进入小程序
↓
选择商品
↓
创建订单
↓
点击微信支付
↓
直接弹出微信支付界面
大体流程:
小程序
↓
后端创建业务订单
↓
微信支付下单
↓
返回 prepay_id
↓
后端签名
↓
小程序 wx.requestPayment()
↓
支付
这里还有一个很容易混淆的问题。
JSAPI 支付
和:
小程序支付
底层接口体系存在很多共通点,但是:
调用环境不同
微信官方现在明确说明,小程序支付用于微信小程序内部,小程序中的支付应使用小程序支付方式;JSAPI 与小程序支付共享部分权限及下单体系,但前端调起方式不同。(WeChat Pay )
所以不要写成:
小程序里面嵌一个 H5
然后随便 JSAPI 支付
支付场景必须和微信规定匹配。
九、APP 支付
如果公司开发的是:
Android APP
iOS APP
例如:
京东
美团
携程
自己的商城 APP
点击:
微信支付
然后:
APP
↓
跳到微信
↓
付款
↓
回到 APP
这就是:
APP 支付
整体过程:
APP
↓
自己的后端
↓
微信支付下单
↓
返回支付参数
↓
APP 调微信 SDK
↓
微信 APP
↓
支付
↓
返回商户 APP
微信官方对 App 支付的定义也是商户 App 对接微信支付 API 和客户端 SDK,跳转到微信 App 完成付款,再返回商户 App。(WeChat Pay )
这里除了后端:
Spring Boot
还会涉及:
Android / iOS 微信 SDK
十、付款码支付:线下商店常见
这个和前面的方向刚好反过来。
Native 是:
商户展示二维码
↓
用户扫一扫
付款码支付则是:
用户展示付款码
↓
商家扫码枪扫一扫
就是你去:
超市
便利店
餐厅
结账时打开微信:
付款码
收银员:
滴!
一下。
这就是:
付款码支付
官方也通常称它为:
被扫支付
流程:
用户
↓
展示付款二维码
商户
↓
扫码枪读取付款码
↓
商户后台请求微信
↓
扣款
↓
支付完成
微信官方当前同样把付款码支付定位在线下面对面收银,由用户展示付款码供商户扫码设备读取。(WeChat Pay )
十一、6 种支付方式怎么快速记?
可以记成一张表:
PC 网站
↓
Native
微信外手机浏览器
↓
H5
微信内网页 / 公众号
↓
JSAPI
微信小程序
↓
小程序支付
Android / iOS
↓
APP 支付
超市扫码枪扫你
↓
付款码支付
其实没那么复杂。
支付方式的区别,本质主要来自:
用户在哪里发起支付。
十二、个人收款码应该放在哪里?
把个人收款码加进来:
| 方式 | 场景 | 自动支付回调 |
|---|---|---|
| 个人收款码 | 个人项目、Demo | 通常需要人工确认 |
| Native | PC 网站 | ✅ |
| H5 | 微信外手机浏览器 | ✅ |
| JSAPI | 微信内网页 | ✅ |
| 小程序 | 微信小程序 | ✅ |
| App | 手机 App | ✅ |
| 付款码 | 线下收银 | ✅ |
因此个人项目可以:
阶段 1:
个人收款码
+
人工确认
等到有正式商户能力:
阶段 2:
微信支付 API
+
自动支付回调
+
自动发货
十三、微信支付为什么需要这么多参数?
你可能经常看到:
wx:
pay:
appId:
merchantId:
privateKey:
merchantSerialNumber:
apiV3Key:
payNotifyUrl:
第一次看非常乱。
其实分别对应不同身份。
appId
代表:
哪个微信应用
可能对应:
公众号
小程序
APP
merchantId
也就是:
mchid
代表:
谁在收钱?
也就是:
微信支付商户号
privateKey
商户:
API 私钥
主要用于:
签名
可以理解为:
我要向微信证明这个请求真的是这个商户发出来的。
merchantSerialNumber
商户证书:
序列号
用于标识对应证书。
apiV3Key
API v3 密钥。
它和:
privateKey
不是同一个东西。
初学阶段可以简单理解:
privateKey
↓
请求签名等
APIv3 Key
↓
支付通知等敏感数据解密
notifyUrl
这是最重要的:
支付结果通知地址
例如:
https://api.xxx.com/wx/payNotify
用户付款之后微信会请求:
POST /wx/payNotify
告诉你的后端支付结果。
十四、业务订单号为什么特别重要?
微信下单有一个非常重要的参数:
out_trade_no
它可以理解成:
你的系统告诉微信:“这笔支付对应我系统里的哪个订单?”
例如:
你的系统:
订单:
202609020001
请求微信:
{
"out_trade_no": "202609020001"
}
微信以后支付回调:
out_trade_no
=
202609020001
你的系统就能:
SELECT *
FROM pay_order
WHERE order_no = '202609020001';
找到业务订单。
所以:
业务订单
│
│ out_trade_no
▼
微信支付订单
就是靠它建立联系。
十五、下单对象应该怎么设计?
项目里一般不要到处直接使用微信 SDK 的 Request。
可以先设计自己的业务对象:
@Data
public class PayOrderRequest {
/**
* 业务订单号
*/
private String outTradeNo;
/**
* 商品描述
*/
private String description;
/**
* 金额:分
*/
private Integer total;
/**
* 支付方式
*/
private PayWay payWay;
/**
* JSAPI / 小程序可能需要
*/
private String openId;
}
支付方式:
public enum PayWay {
WX_NATIVE,
WX_H5,
WX_JSAPI,
WX_MINI_PROGRAM,
WX_APP
}
这样业务层只关心:
我要支付多少钱
订单是多少
用什么支付方式
而不用关心微信 SDK 的全部细节。
十六、为什么支付方式适合策略模式?
假设写成:
if (payWay == NATIVE) {
nativePay();
} else if (payWay == H5) {
h5Pay();
} else if (payWay == JSAPI) {
jsapiPay();
} else if (...) {
}
支付方式越来越多:
微信 Native
微信 H5
微信 JSAPI
支付宝
银联
Apple Pay
...
if/else 会越来越大。
因此非常适合:
策略模式
例如:
public interface PayStrategy {
PrePayResponse createOrder(
PayOrderRequest request
);
}
然后:
WxNativePayStrategy
WxH5PayStrategy
WxJsapiPayStrategy
WxMiniPayStrategy
WxAppPayStrategy
最终:
PayStrategy
▲
┌───────────┼───────────┐
│ │ │
Native H5 JSAPI
业务层:
payService.createOrder(
PayWay.WX_NATIVE,
request
);
不用知道具体微信 API 怎么调用。
这才是策略模式真正适合出现的地方:
同一个业务目标,有多种可以替换的实现方式。
十七、支付成功不能相信前端
这是支付系统最重要的安全原则之一。
假设前端支付结束以后调用:
POST /order/paySuccess
参数:
{
"orderId": 1001,
"paySuccess": true
}
后端直接:
UPDATE orders
SET status = 'PAID'
WHERE id = 1001;
那系统就完了。
攻击者自己请求:
/order/paySuccess
就可以白嫖。
所以:
不能因为前端告诉你支付成功,就认为真的支付成功。
真正可信的依据应该来自:
微信支付服务器回调
或者:
主动查询微信订单状态
前端显示:
支付成功
只能用于:
UI 展示
不能直接作为业务订单支付成功的最终凭证。
十八、支付回调才是真正核心
微信支付完成:
微信
↓
POST /wx/payNotify
↓
后端
后台要做:
读取请求
↓
验签
↓
解密
↓
获得支付信息
↓
检查订单
↓
检查金额
↓
更新订单
↓
执行业务逻辑
↓
返回成功
使用官方 SDK 后通常可以让:
NotificationParser
帮我们完成验签和通知数据解析。
伪代码:
Transaction transaction =
parser.parse(
requestParam,
Transaction.class
);
解析之后:
transaction.getOutTradeNo();
transaction.getTradeState();
transaction.getAmount();
transaction.getSuccessTime();
然后处理自己的业务。
十九、千万不要只判断 SUCCESS
假设微信告诉你:
outTradeNo = 202609020001
state = SUCCESS
是不是马上:
订单成功
还不够。
至少应该校验:
订单是否存在
订单是否已经支付
商户号是否正确
APPID 是否正确
支付金额是否一致
订单状态是否允许支付
例如你的订单:
价格:
1 分
别人伪造或业务异常导致收到:
支付金额:
0 分
显然不能直接认为成功。
所以:
支付回调不只是“收到 SUCCESS → UPDATE”。
必须进行业务校验。
二十、为什么支付回调一定要做幂等?
假设:
微信
↓
支付成功通知
↓
你的服务器处理成功
但是你的服务器返回:
200 OK
之前网络断了。
微信可能认为:
通知失败
然后:
再通知一次
于是:
第一次回调
↓
给用户加 100 积分
第二次回调
↓
又加 100
第三次
↓
又加 100
显然不行。
所以支付回调必须满足:
执行一次
=
执行十次
最终结果一样。
这就是:
幂等。
例如:
UPDATE pay_order
SET status = 'PAID'
WHERE order_no = ?
AND status = 'NOT_PAY';
然后:
只有状态真的从:
NOT_PAY
↓
PAID
的线程才能继续执行后续业务。
或者:
唯一索引
Redis 锁
数据库状态机
CAS
都可以参与实现。
二十一、为什么还需要主动查单?
可能出现:
用户已经支付成功
↓
微信发送回调
↓
你的服务器刚好挂了
结果:
微信:支付成功
你的数据库:NOT_PAY
虽然微信通常存在通知重试机制,但是优秀的支付系统不能完全把正确性寄托在一次回调上。
所以还会设计:
主动查单
例如定时任务:
每隔一段时间
↓
查询长时间处于 NOT_PAY / PAYING 的订单
↓
调用微信订单查询 API
↓
发现其实已经 SUCCESS
↓
补偿更新业务订单
于是:
支付回调
+
主动查询
共同保证支付状态最终一致。
这就是:
补偿机制。
二十二、为什么扫码支付经常配 WebSocket?
Native 支付有一个很有意思的问题。
用户在电脑打开:
订单支付页面
二维码:
████████
████████
然后拿手机扫码付款。
微信调用的是:
后端 payNotify
但是:
电脑浏览器
并不知道后端刚刚收到了支付通知。
所以一种简单方式是前端:
每 2 秒查询一次订单状态
也就是:
轮询
例如:
GET /order/status/1001
GET /order/status/1001
GET /order/status/1001
直到:
PAID
然后跳转:
支付成功页面
另外一种更优雅的方式:
WebSocket
流程:
PC 页面
│
│ WebSocket
▼
后端
用户手机支付成功
↓
微信回调
↓
后端订单更新
↓
WebSocket
↓
通知 PC:
“支付成功”
于是浏览器马上跳转。
注意:
微信支付回调
负责:
确认钱是否收到
而:
WebSocket
负责:
实时告诉网页结果
它们不是一回事。
二十三、付费阅读完整流程
回到最开始的场景。
用户购买付费文章。
数据库可能存在:
article
pay_order
user_article_permission
用户点击:
解锁文章
流程:
用户
↓
点击解锁
↓
创建业务订单
↓
pay_order
status = NOT_PAY
↓
选择微信支付
↓
Native 下单
↓
code_url
↓
二维码
↓
用户扫码
↓
微信支付成功
↓
微信通知 payNotify
↓
验签 / 解密
↓
支付订单 PAID
↓
写入阅读权限
↓
user_article_permission
user_id = 100
article_id = 666
↓
WebSocket 通知前端
↓
刷新文章
↓
全文解锁
这就是一个完整的:
付费阅读支付闭环。
二十四、个人收款码如何兼容正式微信支付?
如果项目一开始支持:
个人收款码
后来又接:
微信 Native
其实不要推倒重做。
因为业务订单本身可以统一。
例如:
pay_order
id
order_no
user_id
amount
pay_way
status
其中:
pay_way
可以:
PERSONAL_QR
WX_NATIVE
WX_H5
WX_JSAPI
个人码:
创建业务订单
↓
展示个人收款码
↓
用户付款
↓
作者人工确认
↓
updatePaymentSuccess()
微信支付:
创建业务订单
↓
微信下单
↓
微信回调
↓
updatePaymentSuccess()
注意最后:
人工确认
和
微信回调
都进入:
updatePaymentSuccess(orderNo);
后面的业务逻辑完全一样:
订单变 PAID
↓
解锁文章
↓
发积分
↓
开会员
这就是非常漂亮的设计。
前面的:
怎么收钱
可以变化。
后面的:
支付成功后做什么
统一。
二十五、一个比较好的支付系统结构
最终可以设计成:
PayService
│
创建统一业务支付订单
│
┌────────────┼────────────┐
│ │ │
Personal QR WeChat Pay Alipay
│ │
│ ┌────┼────┐
│ │ │ │
│ Native H5 JSAPI ...
│
│
└────────────┬─────────────┘
│
Payment Success
│
支付成功统一处理器
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
解锁文章 开通会员 发货
这样以后再增加:
支付宝
银行卡
Apple Pay
不会把付费阅读业务全部改一遍。
这其实就是:
支付能力和业务能力解耦。
二十六、微信支付资质怎么理解?
正式接微信支付 API,需要申请微信支付对应商户能力。
截至当前官方主体权限说明,企业主体可以申请 JSAPI、小程序、APP、H5、Native、付款码等基础支付权限;个体工商户目前支持 JSAPI、小程序、APP、Native、付款码,但基础权限列表中不支持 H5;不同主体和业务场景最终应以微信支付商户平台实际审核结果为准。(WeChat Pay )
所以不要简单理解成:
有微信
=
可以调微信支付 API
中间还有:
主体
商户号
APPID
支付产品权限
商户私钥
证书
APIv3 Key
域名 / 支付目录
回调地址
等配置。
而个人项目没有这些正式商户能力时:
个人收款码
+
人工确认
仍然可以作为 Demo 或个人项目的一种过渡方案。
二十七、一定不要把密钥写进 Git
例如:
apiV3Key: xxx
privateKey: xxx
真实生产环境不要直接:
提交到 GitHub
更不要:
把 apiclient_key.pem
上传到公开仓库
私钥一旦泄露:
必须按安全事件处理。
更合理的是:
环境变量
配置中心
Secret Manager
服务器安全目录
例如:
wx:
pay:
apiV3Key: ${WX_API_V3_KEY}
私钥:
/etc/app/cert/apiclient_key.pem
而不是:
src/main/resources
跟代码一起打包公开。
二十八、微信支付真正需要掌握的不是 SDK
很多人学支付的时候会陷入:
new PrepayRequest();
new Amount();
setAppid();
setMchid();
setDescription();
然后感觉:
微信支付代码好多。
其实这些 API 都只是:
SDK 调用细节
真正值得掌握的是:
第一层:支付场景
PC
Native
微信外浏览器
H5
微信网页
JSAPI
小程序
小程序支付
APP
APP 支付
线下
付款码支付
第二层:支付流程
业务下单
↓
三方下单
↓
用户支付
↓
回调
↓
业务处理
第三层:可靠性
验签
金额校验
幂等
状态机
主动查单
补偿机制
第四层:系统设计
策略模式
支付渠道抽象
支付业务解耦
WebSocket
异步处理
把这四层搞懂之后:
微信支付
支付宝
银联
Stripe
很多思想其实都是相通的。
二十九、一张图彻底记住微信支付
最后把所有东西压成这一张图:
用户支付
│
┌────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
个人项目 正式线上支付 线下支付
│ │ │
个人收款码 根据场景选择 付款码
│ │
│ ┌─────────┼─────────┐
│ │ │ │
│ Native H5 JSAPI
│
│ 小程序
│
│ APP
│
▼ ▼
人工确认 微信支付平台
│ │
│ 支付回调
│ │
└────────┬───────┘
│
Payment Success
│
▼
业务订单
PAID
│
┌─────────┼─────────┐
▼ ▼ ▼
解锁文章 开通会员 发货
你只需要记住:
支付方式决定“用户怎么付钱”,支付回调决定“系统怎么知道钱付了”,业务订单决定“这笔钱属于什么业务”,幂等和补偿机制决定“支付系统在异常情况下还能不能保持正确”。
总结
如果只是个人项目,没有正式商户支付能力,可以:
个人收款码
+
人工确认
人工确认本质上相当于:
人工支付回调
而正式微信支付建立的是:
业务订单
↓
微信下单
↓
用户支付
↓
微信回调
↓
验签 / 解密
↓
订单更新
↓
自动执行业务
不同终端选择不同方式:
PC 网站
→ Native
微信外手机浏览器
→ H5
微信内网页 / 公众号
→ JSAPI
微信小程序
→ 小程序支付
Android / iOS
→ APP 支付
线下商店扫码枪
→ 付款码支付
但不管使用哪一种,真正的支付系统核心始终没有变化:
创建订单
→ 创建支付
→ 用户付款
→ 支付回调
→ 验证支付结果
→ 幂等更新订单
→ 执行业务
→ 主动查单补偿
如果你能把这一条链路真正理解,后面再去看微信官方 SDK 中的:
NativePayService
JsapiService
NotificationParser
PrepayRequest
Transaction
就不会觉得它们是一堆毫无关系的类了。
因为这些代码,本质上都只是在服务同一个支付闭环。