🎤 面试官:用户下单后去支付,你们是怎么跟支付宝/微信对接的?
我: 我们不是自己收钱,而是调第三方支付平台,比如支付宝、微信支付,让他们来收钱。
整个流程分两步:
- 前端拉起支付
- 后端处理支付结果(回调)
下面我一步步说。
✅ 第一步:用户点“去支付”,怎么跳到支付宝?
我: 不是直接跳!我们得先让后端去支付宝那边“下单”。
流程是这样的:
- 用户在 App 或网页点“去支付”;
- 前端调我们后端接口
/api/order/pay,传订单 ID;- 我们后端调支付宝开放平台接口(比如
alipay.trade.page.pay),把:
- 订单号
- 金额
- 商品名称
- 回调地址(notify_url)
- 签名 一起发给支付宝;
- 支付宝返回一个
form 表单or支付链接;- 我们把这玩意儿返回给前端;
- 前端用
document.write()写进去 or 跳转链接,自动跳到支付宝收银台。
说白了:我们是“代用户发起支付请求”,不是让用户自己乱填。
🎤 面试官:那支付成功后,支付宝怎么通知你们?
我: 这就是最关键的——支付回调(notify_url)。
支付宝有个规则:
- 用户付完钱,不管有没有点“完成”,支付宝都会主动发个 HTTPS 请求到我们配置的
notify_url;- 这个请求里带着:订单号、金额、支付状态、签名等信息;
- 我们收到后,必须校验签名,更新订单状态为“已支付”,然后返回
success;- 如果我们没返回
success,支付宝会每隔几分钟重试,最多重试 8 次。
所以这个接口特别重要,不能挂,也不能出错。
🎤 面试官:那如果回调失败了,或者网络抖了没收到呢?
我: 这是最怕的事!用户钱付了,我们没收到通知,订单还是“待支付”,那就糟了。
我们用了三招来防:
🔹 1. 回调接口必须幂等 + 加锁
因为支付宝会重试,可能同一条消息发好几次。 所以我们处理时:
- 先查订单状态,如果是“已支付”,直接返回
success,不再处理;- 更新订单时加数据库行锁:
SELECT ... FOR UPDATE,防止并发冲突。
🔹 2. 主动查单(对账)
每天凌晨,我们跑一个定时任务,调支付宝的
alipay.trade.query接口, 去查“昨天所有待支付但超时的订单”,看支付宝那边到底有没有付成功。如果发现“支付宝说已付,我们没更新”,就手动补成“已支付”。
这叫“日切对账”,是最后一道保险。
🔹 3. 用户端刷新也能触发校验
用户如果发现“我明明付了,怎么还显示未支付”,他一刷新页面, 我们后端也会主动调一次
query接口去查真实支付状态,避免他投诉。
🎤 面试官:怎么防止有人伪造支付回调?
我: 黑产真干过这事!自己写个脚本,模拟支付宝发个“支付成功”的请求,就想白嫖。
我们靠 签名验证 来防:
- 支付宝发来的所有参数,都会带一个
sign字段;- 我们收到后,把所有参数按字母排序,拼成字符串,再用我们自己的 私钥 做一次签名计算;
- 算出来的签名和
sign对得上,才是真的;- 对不上?直接 忽略,连日志都记一笔:“疑似伪造支付回调,IP 已封”。
而且我们还加了:
- IP 白名单:只允许支付宝官方 IP 发来的回调请求;
- HTTPS + 证书校验:防止中间人攻击。
签名 + 白名单 + HTTPS,三道锁,基本就安全了。
🎤 面试官:回调处理很慢怎么办?比如数据库卡了。
我: 我们绝对不能在回调里干太重的事!
比如不能:
- 发优惠券
- 推送库存
- 调十几个服务
因为支付宝等着你回 success,你拖太久,它就认为失败,开始重试。
所以我们的做法是:
回调接口只做三件事:
- 验签
- 查订单
- 更新状态为“已支付”
然后发个 MQ 消息:
json{ "orderId": 1001, "event": "paid" }其他事情(发券、推履约、发通知)都由消费者异步处理。
这样回调接口200ms 内就能返回 success,支付宝满意,也不重试。
🎤 面试官:用户支付后,订单状态还没更新,就去查订单,看到的还是“待支付”?
我: 这叫“状态不一致”,很常见。
我们的做法是:前端不能只信自己缓存的状态。
比如用户付完款跳回我们 App,我们不直接显示“支付成功”,而是:
- 先调一个
GET /api/order/status?orderId=1001;- 这个接口会:
- 先查数据库订单状态;
- 如果是“待支付”,但时间已超时,就主动调一次
alipay.trade.query去查真实状态;- 如果支付宝说已付,就当场更新订单,返回“已支付”。
这叫“兜底查询 + 主动对单”,保证用户看到的是真实结果。
🎤 面试官:微信支付和支付宝支付,你们是统一处理的吗?它们一样吗?
我: 看似一样,实则坑特别多。
从“用户点支付 → 调起收银台 → 支付成功 → 回调通知”这个流程看,逻辑是差不多的,但细节上差得挺远,代码根本没法完全复用。
我们一开始想搞个“统一支付门面”,结果写着写着发现:不是同一个物种!
✅ 一、调用方式就不一样
| 对比项 | 支付宝 | 微信支付 |
|---|---|---|
| 调起方式 | 返回一个 form 或 支付链接,前端跳转 | 返回 appId、timeStamp、paySign 等字段,前端使用JSAPI,如WeixinJSBridge.invoke |
| 前端集成 | 简单,document.write() 或者location.href就行 | 复杂,要引入 JS-SDK,还要判断是否在微信里 |
| 适用场景 | 网页、App、小程序都能用 | 小程序里最顺,网页支付限制多(必须备案域名) |
举个例子:
- 支付宝你给个链接,用户点开就付;
- 微信你得让前端写一堆 JS 代码,还要确保在微信浏览器里,否则直接“不支持”。
✅ 二、回调机制也不同
| 对比项 | 支付宝 | 微信支付 |
|---|---|---|
| 回调格式 | GET or POST,参数是 key=value&... | 只支持 POST,XML 格式! |
| 返回要求 | 回 success(字符串) | 回 <xml><return_code><![CDATA[SUCCESS]]></return_code></xml> |
| 重试策略 | 重试 8 次,间隔递增 | 重试多次,直到返回 SUCCESS |
这个特别坑! 我们一开始用 JSON 解析器去读微信回调,结果一直报错——人家发的是 XML! 后来专门写了个 XML 转 Map 的工具类,才搞定。
✅ 三、签名方式也不一样
| 对比项 | 支付宝 | 微信支付 |
|---|---|---|
| 签名算法 | RSA2(推荐) | 也是 RSA,但字段拼接规则不同 |
| 密钥管理 | 用应用私钥 + 支付宝公钥 | 用 APIv3 密钥(新版本) or 证书 |
| 验签复杂度 | 简单,官方 SDK 支持好 | 复杂,v3 版本还要用 AES 解密回调内容 |
特别是微信支付 APIv3 版本:
- 回调内容是加密的,你得用它的证书 + APIv3 密钥去解密;
- 解密后才是真正的支付结果。
比支付宝多好几步,一不留神就卡住。
✅ 四、使用场景限制大不同
| 对比项 | 支付宝 | 微信支付 |
|---|---|---|
| 网页支付 | 随便用,域名不用特别备案 | 必须备案域名,还得在商户平台配置“支付目录” |
| App 支付 | 需要集成 SDK,签名打包 | 也需要 SDK,但更依赖微信登录体系 |
| 小程序支付 | 不支持 | 原生支持,体验最好 |
举个真实例子: 我们有个小程序,用微信支付,流程丝滑; 换成支付宝小程序?得重新走一遍授权、登录、支付链路,用户流失率直接上升。
✅ 五、对账和查询接口也不同
| 对比项 | 支付宝 | 微信支付 |
|---|---|---|
| 查询订单 | alipay.trade.query | pay/orderquery |
| 退款查询 | alipay.trade.fastpay.refund.query | secapi/pay/refundquery |
| 对账文件 | CSV,下载链接在回调里 | 也是 CSV,但每天固定时间生成,需主动下载 |
而且微信的对账文件还分:
- 日账单
- 资金账单(更细,含手续费)
支付宝就一个账单,简单点。
✅ 六、我们最后是怎么处理的?
我: 我们放弃了“完全统一”的想法,改成:
抽象一个 PayService 接口:
javapublic interface PayService { PayResponse pay(Order order); void handleCallback(Map<String, String> params); boolean verifySign(Map<String, String> params); }两个实现类:
AlipayServiceImplWechatPayServiceImpl根据不同支付方式,调用不同实现:
javaPayService service = payServiceFactory.get(type); service.pay(order);公共逻辑抽出去:比如订单状态更新、发 MQ、对账任务,还是共用一套。
这样既避免重复代码,又能处理各自特殊逻辑。
🎤 面试官:微信/支付宝支付里,同步回调和异步回调有什么区别?分别做什么?
我: 这俩名字确实容易搞混,但它们的分工特别明确:
- 同步地址(return_url) → 是给“用户看的”
- 异步地址(notify_url) → 是给“系统用的”
一个管“页面跳转”,一个管“状态更新”。 下面我用一个真实场景给你串起来。
✅ 举个栗子:用户买个 99 元的会员
1. 用户点“去支付” → 跳到支付宝/微信
2. 输入密码,点“确认支付”
3. 支付成功后,发生了什么?
这时候有两个动作同时发生:
🔹 同步回调(return_url)—— 用户看到的页面跳转
它是干啥的? 告诉浏览器:“付完了,把用户带回我们 App 或网页”。
流程是这样的:
用户付完钱,支付宝/微信会把用户浏览器重定向到我们设置的
return_url如:https://yourapp.com/pay/success?out_trade_no=1001&trade_no=20210001
我们的前端页面加载,看到 URL 有
success,就显示“支付成功”页面;然后前端再调个接口:
GET /api/order/1001,查订单状态,展示详情。
关键点:
- 这个跳转是用户可见的;
- 它不保证一定能执行(比如用户点完支付就关了App);
- 所以我们不能靠它来更新订单状态!
举个反例: 有次我们傻乎乎在
return_url的接口里更新订单为“已支付”,结果黑产伪造链接:https://xxx.com/pay/success?out_trade_no=1001直接白嫖成功!后来赶紧改了。
🔹 异步回调(notify_url)—— 系统真正的“支付成功通知”
它是干啥的? 支付平台主动发 HTTPS 请求到我们的服务器,告诉我们:“钱真的收到了,赶紧改订单状态”。
流程是这样的:
用户一付钱,支付宝/微信就会后台调用我们配置的
notify_url;请求是POST的,带一堆参数和签名,比如:
xml<!-- 微信 --> <xml> <return_code>SUCCESS</return_code> <out_trade_no>1001</out_trade_no> <total_fee>9900</total_fee> <sign>...</sign> </xml>我们收到后:
- 验签(防伪造)
- 查订单
- 更新状态为“已支付”
- 发消息(如发会员权限、发券)
- 返回
success(微信) orsuccess(支付宝)
关键点:
- 这个请求是服务器之间通信,用户看不到;
- 它一定会来(除非你服务器挂了),而且会重试多次;
- 所以我们必须靠它来更新订单状态!
✅ 两者对比总结
| 对比项 | 同步回调(return_url) | 异步回调(notify_url) |
|---|---|---|
| 谁触发 | 用户浏览器 | 支付平台服务器 |
| 用途 | 页面跳转,给用户看结果 | 真实支付通知,更新订单状态 |
| 是否可靠 | ❌ 不可靠(用户可能关页面) | ✅ 可靠(会重试,直到成功) |
| 能否被伪造 | ✅ 能(直接拼URL) | ❌ 难(要过验签 + IP白名单) |
| 处理逻辑 | 前端展示,可查订单状态 | 后端更新状态,发MQ,走流程 |
| 返回要求 | 没特殊要求,正常渲染页面 | 必须返回 success,否则重试 |
✅ 正确使用姿势
我们是这么用的:
- 异步回调(notify_url):
- 只做一件事:更新订单状态为“已支付”;
- 加锁、验签、幂等处理,确保不重复更新;
- 然后发个 MQ 消息,去处理发券、发权益等后续逻辑。
- 同步回调(return_url):
- 不更新订单状态!
- 只做页面跳转,显示“支付成功” or “支付中”;
- 页面加载时,调接口查订单真实状态(可能还没回调到,就显示“处理中”)。
有些公司还会在
return_url页面加个“轮询”,每秒查一次订单状态,直到变成“已支付”。