Skip to content

🎤 面试官:用户下单后去支付,你们是怎么跟支付宝/微信对接的?

: 我们不是自己收钱,而是调第三方支付平台,比如支付宝、微信支付,让他们来收钱。

整个流程分两步:

  1. 前端拉起支付
  2. 后端处理支付结果(回调)

下面我一步步说。


✅ 第一步:用户点“去支付”,怎么跳到支付宝?

: 不是直接跳!我们得先让后端去支付宝那边“下单”。

流程是这样的:

  1. 用户在 App 或网页点“去支付”;
  2. 前端调我们后端接口 /api/order/pay,传订单 ID;
  3. 我们后端调支付宝开放平台接口(比如 alipay.trade.page.pay),把:
    • 订单号
    • 金额
    • 商品名称
    • 回调地址(notify_url)
    • 签名 一起发给支付宝;
  4. 支付宝返回一个 form 表单 or 支付链接
  5. 我们把这玩意儿返回给前端;
  6. 前端用 document.write() 写进去 or 跳转链接,自动跳到支付宝收银台。

说白了:我们是“代用户发起支付请求”,不是让用户自己乱填。


🎤 面试官:那支付成功后,支付宝怎么通知你们?

: 这就是最关键的——支付回调(notify_url)

支付宝有个规则:

  • 用户付完钱,不管有没有点“完成”,支付宝都会主动发个 HTTPS 请求到我们配置的 notify_url
  • 这个请求里带着:订单号、金额、支付状态、签名等信息;
  • 我们收到后,必须校验签名,更新订单状态为“已支付”,然后返回 success
  • 如果我们没返回 success,支付宝会每隔几分钟重试,最多重试 8 次

所以这个接口特别重要,不能挂,也不能出错。


🎤 面试官:那如果回调失败了,或者网络抖了没收到呢?

: 这是最怕的事!用户钱付了,我们没收到通知,订单还是“待支付”,那就糟了。

我们用了三招来防:

🔹 1. 回调接口必须幂等 + 加锁

因为支付宝会重试,可能同一条消息发好几次。 所以我们处理时:

  • 先查订单状态,如果是“已支付”,直接返回 success,不再处理;
  • 更新订单时加数据库行锁:SELECT ... FOR UPDATE,防止并发冲突。

🔹 2. 主动查单(对账)

每天凌晨,我们跑一个定时任务,调支付宝的 alipay.trade.query 接口, 去查“昨天所有待支付但超时的订单”,看支付宝那边到底有没有付成功。

如果发现“支付宝说已付,我们没更新”,就手动补成“已支付”。

这叫“日切对账”,是最后一道保险。

🔹 3. 用户端刷新也能触发校验

用户如果发现“我明明付了,怎么还显示未支付”,他一刷新页面, 我们后端也会主动调一次 query 接口去查真实支付状态,避免他投诉。


🎤 面试官:怎么防止有人伪造支付回调?

: 黑产真干过这事!自己写个脚本,模拟支付宝发个“支付成功”的请求,就想白嫖。

我们靠 签名验证 来防:

  1. 支付宝发来的所有参数,都会带一个 sign 字段;
  2. 我们收到后,把所有参数按字母排序,拼成字符串,再用我们自己的 私钥 做一次签名计算;
  3. 算出来的签名和 sign 对得上,才是真的;
  4. 对不上?直接 忽略,连日志都记一笔:“疑似伪造支付回调,IP 已封”。

而且我们还加了:

  • IP 白名单:只允许支付宝官方 IP 发来的回调请求;
  • HTTPS + 证书校验:防止中间人攻击。

签名 + 白名单 + HTTPS,三道锁,基本就安全了。


🎤 面试官:回调处理很慢怎么办?比如数据库卡了。

: 我们绝对不能在回调里干太重的事!

比如不能:

  • 发优惠券
  • 推送库存
  • 调十几个服务

因为支付宝等着你回 success,你拖太久,它就认为失败,开始重试。

所以我们的做法是:

  1. 回调接口只做三件事:

    • 验签
    • 查订单
    • 更新状态为“已支付”
  2. 然后发个 MQ 消息:

    json
    { "orderId": 1001, "event": "paid" }
  3. 其他事情(发券、推履约、发通知)都由消费者异步处理。

这样回调接口200ms 内就能返回 success,支付宝满意,也不重试。


🎤 面试官:用户支付后,订单状态还没更新,就去查订单,看到的还是“待支付”?

: 这叫“状态不一致”,很常见。

我们的做法是:前端不能只信自己缓存的状态

比如用户付完款跳回我们 App,我们不直接显示“支付成功”,而是:

  1. 先调一个 GET /api/order/status?orderId=1001
  2. 这个接口会:
    • 先查数据库订单状态;
    • 如果是“待支付”,但时间已超时,就主动调一次 alipay.trade.query 去查真实状态;
    • 如果支付宝说已付,就当场更新订单,返回“已支付”。

这叫“兜底查询 + 主动对单”,保证用户看到的是真实结果。

🎤 面试官:微信支付和支付宝支付,你们是统一处理的吗?它们一样吗?

: 看似一样,实则坑特别多

从“用户点支付 → 调起收银台 → 支付成功 → 回调通知”这个流程看,逻辑是差不多的,但细节上差得挺远,代码根本没法完全复用。

我们一开始想搞个“统一支付门面”,结果写着写着发现:不是同一个物种


✅ 一、调用方式就不一样

对比项支付宝微信支付
调起方式返回一个 form支付链接,前端跳转返回 appIdtimeStamppaySign 等字段,前端使用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.querypay/orderquery
退款查询alipay.trade.fastpay.refund.querysecapi/pay/refundquery
对账文件CSV,下载链接在回调里也是 CSV,但每天固定时间生成,需主动下载

而且微信的对账文件还分:

  • 日账单
  • 资金账单(更细,含手续费)

支付宝就一个账单,简单点。


✅ 六、我们最后是怎么处理的?

: 我们放弃了“完全统一”的想法,改成:

  1. 抽象一个 PayService 接口

    java
    public interface PayService {
        PayResponse pay(Order order);
        void handleCallback(Map<String, String> params);
        boolean verifySign(Map<String, String> params);
    }
  2. 两个实现类

    • AlipayServiceImpl
    • WechatPayServiceImpl
  3. 根据不同支付方式,调用不同实现

    java
    PayService service = payServiceFactory.get(type);
    service.pay(order);
  4. 公共逻辑抽出去:比如订单状态更新、发 MQ、对账任务,还是共用一套。

这样既避免重复代码,又能处理各自特殊逻辑

🎤 面试官:微信/支付宝支付里,同步回调和异步回调有什么区别?分别做什么?

: 这俩名字确实容易搞混,但它们的分工特别明确:

  • 同步地址(return_url) → 是给“用户看的
  • 异步地址(notify_url) → 是给“系统用的

一个管“页面跳转”,一个管“状态更新”。 下面我用一个真实场景给你串起来。


✅ 举个栗子:用户买个 99 元的会员

1. 用户点“去支付” → 跳到支付宝/微信

2. 输入密码,点“确认支付”

3. 支付成功后,发生了什么?

这时候有两个动作同时发生:


🔹 同步回调(return_url)—— 用户看到的页面跳转

它是干啥的? 告诉浏览器:“付完了,把用户带回我们 App 或网页”。

流程是这样的:

  1. 用户付完钱,支付宝/微信会把用户浏览器重定向到我们设置的return_url

    如:https://yourapp.com/pay/success?out_trade_no=1001&trade_no=20210001

  2. 我们的前端页面加载,看到 URL 有 success,就显示“支付成功”页面;

  3. 然后前端再调个接口:GET /api/order/1001,查订单状态,展示详情。

关键点:

  • 这个跳转是用户可见的
  • 不保证一定能执行(比如用户点完支付就关了App);
  • 所以我们不能靠它来更新订单状态

举个反例: 有次我们傻乎乎在 return_url 的接口里更新订单为“已支付”,结果黑产伪造链接: https://xxx.com/pay/success?out_trade_no=1001 直接白嫖成功!后来赶紧改了。


🔹 异步回调(notify_url)—— 系统真正的“支付成功通知”

它是干啥的? 支付平台主动发 HTTPS 请求到我们的服务器,告诉我们:“钱真的收到了,赶紧改订单状态”。

流程是这样的:

  1. 用户一付钱,支付宝/微信就会后台调用我们配置的 notify_url

  2. 请求是POST的,带一堆参数和签名,比如:

    xml
    <!-- 微信 -->
    <xml>
      <return_code>SUCCESS</return_code>
      <out_trade_no>1001</out_trade_no>
      <total_fee>9900</total_fee>
      <sign>...</sign>
    </xml>
  3. 我们收到后:

    • 验签(防伪造)
    • 查订单
    • 更新状态为“已支付”
    • 发消息(如发会员权限、发券)
    • 返回 success(微信) or success(支付宝)

关键点:

  • 这个请求是服务器之间通信,用户看不到;
  • 一定会来(除非你服务器挂了),而且会重试多次
  • 所以我们必须靠它来更新订单状态

✅ 两者对比总结

对比项同步回调(return_url)异步回调(notify_url)
谁触发用户浏览器支付平台服务器
用途页面跳转,给用户看结果真实支付通知,更新订单状态
是否可靠❌ 不可靠(用户可能关页面)✅ 可靠(会重试,直到成功)
能否被伪造✅ 能(直接拼URL)❌ 难(要过验签 + IP白名单)
处理逻辑前端展示,可查订单状态后端更新状态,发MQ,走流程
返回要求没特殊要求,正常渲染页面必须返回 success,否则重试

✅ 正确使用姿势

我们是这么用的:

  1. 异步回调(notify_url)
    • 只做一件事:更新订单状态为“已支付”
    • 加锁、验签、幂等处理,确保不重复更新;
    • 然后发个 MQ 消息,去处理发券、发权益等后续逻辑。
  2. 同步回调(return_url)
    • 不更新订单状态!
    • 只做页面跳转,显示“支付成功” or “支付中”;
    • 页面加载时,调接口查订单真实状态(可能还没回调到,就显示“处理中”)。

有些公司还会在 return_url 页面加个“轮询”,每秒查一次订单状态,直到变成“已支付”。