微信接完了,支付宝照着抄一遍抄不了
微信支付接完了,回头接支付宝,心想「都是支付,照着抄一遍」。
抄不了。同一套系统里,类名带 Wechat 的有 69 个,带 AliPay 的只有 7 个 ——
差的这 62 个不是废代码,是微信那边多出来的公众号、小程序、视频号、
Native 扫码、App 分安卓和 iOS。
而且金额单位不一样、签名放的位置不一样、回调要回什么也不一样。
照抄的每一处,都是一个坑。
踩错的代价
这两套的差异,最恶心的地方在于它们长得像但不一样。
完全不同的东西你会认真看文档。而这两个都是「下单-支付-回调-退款」,流程一模一样,字段名也差不多——于是你放松警惕,直接复制粘贴改个名字。
代价通常是这三个:
金额差 100 倍。 微信收分,支付宝收元。测试的时候用 1 块钱测,微信传 100、支付宝传 1.00。抄错了,要么传给支付宝变成 100 倍,要么变成一百块。一分钱那种你可能上线好几天才发现。
回调一直重推。 你收到了、处理了、返回了 200,但没按人家要的格式返回。对方认为你没收到,于是隔一会儿再推一次。你的订单逻辑如果不幂等,一笔订单发了五次货。
微信没有沙箱。 你在支付宝沙箱里舒舒服服调了两天,转头接微信,发现只能拿真钱测。
先看这两套在代码里各占多大
「差在哪」是个感觉。数一下就变成事实了。
这套系统两边都接了,类名前缀一数:
| 微信 | 支付宝 | |
|---|---|---|
| 相关类 | 69 个 | 7 个 |
| 支付服务实现 | 815 行 | 506 行 |
| 回调服务实现 | 1089 行 | 23 行 |
最后一行的比例是 47 : 1。
先说清楚这个数为什么不能直接当「难度比」:微信那 1089 行里塞了不止支付——
退款回调、分账回调、发货信息上传,全在同一个类里;支付宝那 23 行是纯转发,
真正的处理在别处。所以 47 倍是文件组织方式的差异,不完全是工作量的差异。
但把这层折掉,剩下的部分仍然是真的:69 比 7。
微信那 69 个类里,有相当一部分是支付宝这边根本不存在的东西——
公众号、小程序、视频号、Native 扫码、App 分安卓和 iOS,
每一种调起方式的参数组装都不一样。而支付宝这边,
手机网页、App、PC 三种调起共用一套参数结构。
「微信支付」不是一个接口,是一族接口。「支付宝」更接近一个。
这才是接第二家时真正的落差来源:你以为在接一个对等的东西,
你第一次接的那个才是最复杂的,第二次接的会轻松得多——
而反过来先接支付宝再接微信的人,会觉得被骗了。
六个真实差异
一、金额单位:分 vs 元
这是最容易翻车、后果最直接的一个。
| 微信支付 | 支付宝 | |
|---|---|---|
| 单位 | 分 | 元 |
| 类型 | 整数 | 字符串 |
| 1 元怎么传 | 100 | "1.00" |
// 同一个订单金额,两边要转两次
// 1.00 元
BigDecimal amt = order.getAmount();
// 微信收「分」,要整数 → 100
int wxFee = amt.multiply(HUNDRED)
.setScale(0, HALF_UP)
.intValue();
// 支付宝收「元」,要两位小数 → "1.00"
String aliFee = amt.setScale(2, HALF_UP)
.toPlainString();别用 double 做这个转换,浮点精度会在某些金额上给你惊喜。用 BigDecimal,setScale 明确写出来。
二、签名放在哪:请求头 vs 业务参数
这个差异会直接影响你的代码结构,抄不过来。
微信 v3:签名放在 HTTP 请求头的 Authorization 里,格式是一串带商户号、序列号、时间戳、随机串、签名的结构化文本。
支付宝:签名是业务参数里的一个 sign 字段,跟其他参数平级,一起提交。
所以:微信的签名逻辑在拦截器/请求包装那一层;支付宝的在参数组装那一层。你要是想写一个统一的支付抽象层,这里是第一个分叉点。
两边都是 RSA-SHA256,但签名的内容和位置完全不同,能复用的只有「调用 RSA 签名」那一个方法。
三、回调:一个是加密的,一个是明文
微信 v3 的回调是加密的。 收到的是密文,得用 APIv3 密钥(AES-256-GCM)解密才能看到订单信息。这也是为什么 APIv3 密钥配错的时候,表现是「下单成功但回调解密失败」——签名那套是对的,解密那套不对。
支付宝的回调是明文表单 POST,参数直接可读,你要做的是验签:用支付宝公钥验证这堆参数没被篡改。
别因为参数可读就跳过验签。 明文意味着任何人都能构造一个假回调打到你的接口,说某笔订单支付成功了。验签是唯一的防线。
四、回调怎么算「收到了」:一个看状态码,一个看报文
这条最容易被忽略,而且错了不会立刻报错,只会一直重推。
而且两家判定成功的位置根本不在一个层面:
微信 v3 看 HTTP 状态码。 返回 200 或 204 就算成功,body 可以是空的。
失败才要回 4XX/5XX 加一段 JSON:
{"code": "FAIL", "message": "签名验证失败"}注意这里最容易记反:带 code 的报文是失败时用的。
回一个 {"code":"SUCCESS"} 不会报错,因为微信压根不看 body——
但很多人以为「必须返这串」,其实返空就行。
支付宝看报文内容。 要纯文本,就七个字母:
success小写,不带引号,不带 JSON 结构,不带任何其他内容。
你返回 {"code":"success"} 它不认,返回 SUCCESS 它也不认。
这个差异会一路渗到你的方法签名上。真实项目里两家的回调方法长这样:
// 微信:成败靠状态码表达,得能控制它
ResponseEntity<String> wechatCallback(
String notifyData,
HttpServletRequest req);
// 支付宝:返回那个单词就行,String 够了
String alipayCallback(
HttpServletRequest req);连返回类型都不一样。 你想把两家塞进同一个方法签名,从这一步就开始别扭了。
回错了会怎样:对方认为投递失败,按策略重推。支付宝会在一天多的时间里推好几次。如果你的回调处理不幂等,一笔订单会被处理多次——发多次货、加多次余额、送多次积分。
所以回调幂等不是「最佳实践」,是这两家的重试机制逼你必须做的事。这块坑够多,我另外单独写一篇。
五、沙箱:一个有,一个没有
支付宝有完整的沙箱环境:独立的账号、独立的密钥、假的余额,随便测。
微信支付 v3 没有沙箱。 v2 时代有过,现在接 v3 只能拿真实商户号测。
实际做法是用一分钱订单在生产环境测,测完退款。听起来糙,但这是目前的常规做法。
这个差异会影响你的排期:支付宝那边可以并行开发,微信那边必须等商户号和证书都到位才能开始真正的联调。
六、错误信息:一个说人话,一个不说
支付宝的错误返回里有 sub_code 和 sub_msg,通常能直接告诉你哪个参数不对。
微信很多时候只给你一个 SIGN_ERROR 或者 401,然后你自己猜是八个配置里的哪一个错了。
这不是吐槽,是排查策略上的实际差别:接支付宝时看错误信息,接微信时看走到哪一步断的。(微信那边怎么按环节反推,我在证书密钥那篇里写了。)
一张总表
| 维度 | 微信支付 v3 | 支付宝 |
|---|---|---|
| 金额单位 | 分,整数 | 元,字符串两位小数 |
| 签名算法 | RSA-SHA256 | RSA2(RSA-SHA256) |
| 签名位置 | HTTP 头 Authorization | 业务参数 sign |
| 回调内容 | 加密,需 APIv3 密钥解密 | 明文表单,需验签 |
| 回调应答 | 看状态码:200/204 即可,body 可空 | 看报文:必须是纯文本 success |
| 沙箱 | 无 | 有 |
| 错误信息 | 含糊 | 相对明确 |
| 凭据数量 | 多(证书、序列号、公钥、APIv3 密钥) | 少(应用私钥、支付宝公钥) |
我的判断
如果两个都要接,先接微信。
不是因为微信更重要,是因为微信的坑更多、更早暴露。证书、序列号、公钥、APIv3 密钥、没有沙箱——这些约束会逼你把支付这块的代码结构做扎实。等你把微信趟完,接支付宝会顺很多。
反过来,先接支付宝(有沙箱、凭据少、错误信息友好)会让你产生一种「支付挺简单」的错觉,然后在微信这边全部还回来。
别写「统一支付抽象层」,至少一开始别写。
我见过的通用做法是先抽一个 PayService 接口,然后发现两边的差异根本不在接口那一层——在参数结构、签名位置、回调格式上。硬抽出来的抽象层,最后变成一堆 if (channel == WECHAT)。
先各写各的,两边都跑通、都上线过、都踩过坑之后,再看哪些是真共性。 那时候抽出来的东西才有用。
常见报错对照表
| 现象 | 真正的原因 | 解法 |
|---|---|---|
| 支付宝金额变成 1/100 或 100 倍 | 微信的分直接传给了支付宝 | 微信传整数分,支付宝传元字符串两位小数 |
| 回调收到了、也处理了,但对方一直重推 | 微信:返了非 2xx,或超过 5 秒才应答;支付宝:报文不是 success | 微信先应答再异步处理业务;支付宝检查返回的字节 |
| 一笔订单发了多次货 | 重推 + 回调处理不幂等 | 按商户订单号做幂等,先判重再处理 |
| 微信回调解密失败,但下单正常 | APIv3 密钥错了(签名那套是对的) | 回商户平台核对是 APIv3 密钥不是 v2 的 |
| 支付宝验签失败 | 用了应用公钥去验,或密钥格式带了换行/空格 | 验签用支付宝公钥,不是自己的;检查密钥字符串 |
| 金额用 double 算完对不上一分钱 | 浮点精度 | 用 BigDecimal + 明确 setScale |
| 想在微信沙箱里测,找不到入口 | v3 没有沙箱 | 用一分钱真实订单测,测完退款 |
这条链路上的其他几篇
- 《微信支付 v3 的证书和密钥,四类东西别再配混了》
- 《微信小程序支付,从关联商户号到收到第一笔钱》
- 1014 深 关联弹出「主体不一致」,前两种解法 0 行
