尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android 微信支付 App 接入:统一下单、签名、回调与异步通知

Android 微信支付 App 接入:统一下单、签名、回调与异步通知 1. 动手之前的整体设计微信支付不能照着文档抄一遍1.1 支付链路里到底有几个角色在说话很多人第一次接微信支付脑子里只有一句话调个接口把钱收了。真上手才发现这条链路上至少有四个角色在相互说话——你的 Android 客户端、你自己的服务端、微信的支付网关、以及用户手机里的微信 App。四者之间的信任关系是不对等的这一点想不清楚后面写多少代码都会翻车。先把一次成功支付的完整时序捋一遍。用户在 App 里点了“确认支付”客户端把订单信息商品 ID、数量等发给自己的服务端服务端拿着商户号、密钥向微信网关请求下单拿回一个 prepay_id服务端把这个 prepay_id 加上时间戳、随机串、签名拼成一包参数回给客户端客户端调用微信 SDK 的 sendReq 把这包参数丢给微信 App微信 App 弹出收银台用户输密码或指纹支付完成后微信先同步回调你的客户端WXPayEntryActivity再异步通知你的服务端notify_url你的服务端收到异步通知、验签、改订单状态然后回一个 success 的 XML/JSON 给微信整个流程才算闭环。这里面最关键的一条认知是客户端拿到的支付结果只能用来做 UI 展示不能用来发货。客户端可以被反编译、可以被 Hook、可以伪造回调唯一可信的凭据是微信服务器发给你服务器的异步通知。我见过不止一个项目在 onResp 里直接把订单标成已支付结果被人用一个改包工具就薅走了商品这种教训不值得重复。还有一个容易被忽略的角色分工问题签名动作永远发生在服务端。API 密钥、商户私钥这些东西一旦出现在 APK 里等于把保险柜钥匙贴在门上。哪怕你觉得自己做了混淆、做了加固反编译一个字符串常量也就是几分钟的事。所以客户端这一侧的任务非常单纯——组装、调起、收结果、通知服务端仅此而已。1.2 三种接入方式的取舍App支付、JSAPI、Native扫码微信支付不是一个单一接口而是一族产品。选错类型是新手最常见的返工原因。下面这张表是我自己整理过的对照基本覆盖了日常会碰到的场景支付类型trade_type适用场景调起方式是否需要额外资质App 支付APP原生 Android/iOS 应用内收款客户端 SDK sendReq需开放平台移动应用JSAPI 支付JSAPI公众号网页、微信内 H5WeixinJSBridge需公众号 授权域名Native 扫码NATIVEPC 网站、收银台大屏返回二维码链接需 PC 网站备案小程序支付JSAPI微信小程序内wx.requestPayment需小程序主体H5 支付MWEB微信外浏览器跳转 URL需额外申请Android 原生 App 走的就是第一行。这里有个特别典型的坑有些人为了省事在 App 内嵌 WebView 里加载网页下单然后走 JSAPI。这条路的用户体验很差——WebView 里没法直接唤起微信需要各种跳转和回跳而且微信对 JSAPI 的授权域名校验很严格稍有不符就报“当前页面的 URL 未注册”。原生 App 就用原生 App 支付别绕。至于资质这块App 支付需要在微信开放平台注册移动应用拿到 AppID并且把你的应用签名MD5 值填进去。注意是应用签名不是签名文件的 SHA1 或 SHA256是那个去掉冒号、转成小写的 MD5。这个值填错了表现就是 SDK 能初始化、能调起微信但微信那边直接拒绝回调 -1日志里什么有用信息都没有。我后面会专门讲怎么用工具把这个值取出来。1.3 一个被反复踩的坑客户端不能碰金额和签名这条单独拎出来说因为它是我见过造成线上事故最多的一条。正确的做法是客户端提交业务标识比如商品 ID、套餐编号、订单号服务端根据这些标识去数据库里查出真实金额然后下单。客户端绝对不允许把 total_fee 传上来更不允许服务端信任客户端传来的金额。原因很简单抓个包改个数字一分钱买年费会员这种事就发生了。签名同理。有些教程为了演示方便把 API 密钥写在 Android 代码里让客户端本地算签名然后直接调微信的下单接口。这种写法只能存在于 Demo 里一旦上线就是灾难。密钥泄露之后别人可以用你的商户号随意发起下单、发起退款损失是实打实的。我现在做这类项目的固定做法是服务端提供一个“创建订单”接口客户端传商品 SKU 和数量服务端落库拿到 out_trade_no然后内部再调微信下单最后把调起参数回给客户端。客户端全程不知道密钥长什么样也不知道 total_fee 是多少。这样即使 APK 被反编译能拿到的也只是几个无关痛痒的接口地址。2. 开工前的账号与工程准备2.1 商户平台侧需要拿到的五样东西在动代码之前先把账号侧的东西凑齐否则写到一半卡住会很痛苦。需要准备的东西我列个清单AppID微信开放平台移动应用的 AppID形如 wx 开头的一串字符。注意它和公众号的 AppID、小程序的 AppID 是三个不同的东西不能混用。商户号mch_id商户平台里的商户号纯数字一般是 8 到 10 位。API 密钥APIv2 key32 位字符串在商户平台“账户中心 - API 安全”里设置。这个密钥只显示一次设置完自己找地方存好。APIv3 密钥如果打算走 v3 接口还需要单独设置一个 32 位 APIv3 密钥用于回调通知的解密。商户 API 证书包含 apiclient_cert.pem 和 apiclient_key.pemv3 接口签名和敏感信息解密都要用。还有一个动作必须做在开放平台把 Android 应用签名填进去。获取方式很简单用 keytool 就行keytool -list -v -keystore your_release.jks -alias your_alias输出里会有一行 MD5 指纹形如AB:CD:EF:...。把它去掉冒号、全部转成小写就是微信要的“应用签名”。很多人只取了 SHA1填进去怎么都不对这个坑非常隐蔽。提示Debug 包和 Release 包的签名不同开放平台只能填一个。如果测试阶段用的是 debug 包那开放平台就得填 debug 的签名正式发包前记得换回来否则线上必然调不起。2.2 Android Studio 工程配置包名、签名、混淆工程侧的配置看起来琐碎但每一项都和后面的报错直接挂钩。包名applicationId在微信那边是认死了的。你注册移动应用时填的包名是什么APK 里的 applicationId 就必须是什么一个字都不能差。改包名这种事在接入支付之后就别想了要改就得回开放平台重新提审。签名配置建议用 build.gradle 里的 signingConfigs 管理别用 Android Studio 自带的“Generate Signed Bundle”手动打包那种方式容易在不同机器上产出不同的签名。android { signingConfigs { release { storeFile file(../keystore/release.jks) storePassword System.getenv(KS_PWD) keyAlias release keyPassword System.getenv(KEY_PWD) } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } }混淆规则里必须给微信 SDK 留口子。微信的 SDK 里用了反射和大量回调类混淆之后回调直接进不来表现就是“支付完成了但我的页面没反应”。稳妥的做法是加一条最宽的规则-keep class com.tencent.mm.opensdk.** { *; } -keep class com.tencent.wxop.** { *; } -keep class com.tencent.mm.sdk.** { *; }至于那些所谓“支付代币数量支持小数点吗”之类的疑问本质上都是金额精度问题。微信支付的 total_fee 单位是分类型是整数压根不存在小数。你想收 9.9 元传的就是 990。任何在服务端用浮点数做金额运算的写法都是隐患0.1 0.2 这种经典问题在订单系统里会变成一分钱的账目不平。统一的处理方式是数据库里金额存整数分展示时除以 100运算全程用整数或 BigDecimal。2.3 微信 SDK 引入与 WXEntryActivity 的注册细节SDK 的引入方式有两种早期的 jar 包和现在的 Maven 依赖。现在建议直接用 Maven版本更新更省事implementation com.tencent.mm.opensdk:wechat-sdk-android:6.8.0引入之后有两个必做动作漏掉任何一个支付都调不起来。第一个是WXPayEntryActivity。这个类的路径是死的必须是你的包名.wxapi.WXPayEntryActivity。注意.wxapi这一层小写类名大小写也要对。它不是普通的 Activity微信 App 支付完成后会直接按这个约定路径来找你的回调入口。这个名字写错了微信找不到入口回调就永远不会触发。activity android:name.wxapi.WXPayEntryActivity android:exportedtrue android:launchModesingleTop intent-filter action android:nameandroid.intent.action.VIEW / category android:nameandroid.intent.category.DEFAULT / data android:schemewx你的AppID / /intent-filter /activity这里有个容易踩的细节android:exported必须显式写成 true。Android 12 之后如果不写这个属性编译期直接报错就算编译过了微信也进不来。另外launchMode建议给 singleTop避免用户连续操作时出现多个实例回调重复触发。第二个是 Android 11API 30之后的包可见性。系统默认不允许应用随便查询其他应用是否安装而微信 SDK 内部会去判断微信有没有装。不声明的话api.isWXAppInstalled()永远返回 false用户明明装了微信却提示“请先安装微信”。queries package android:namecom.tencent.mm / /queries这段写在 manifest 的顶层和 application 平级。另外如果调起参数里带了package字段正常情况下微信会直接用这个字段去匹配不需要你在代码里手动 setPackage。但有些机型上如果不设会出现选错应用的情况稳妥起见可以在 sendReq 之前加一句req.package SignWXPay注意这里赋的是字符串本身。3. 服务端下单接口预支付订单生成的核心细节3.1 统一下单的参数清单与常见错误值服务端这一侧是整个流程的心脏。以 APIv2 的统一下单接口https://api.mch.weixin.qq.com/pay/unifiedorder为例必填参数其实不多但每个都有讲究参数名是否必填说明容易出错的地方appid是开放平台移动应用 AppID误填公众号 AppIDmch_id是商户号多商户号时选错nonce_str是32 位内随机字符串用固定值被风控body是商品描述含特殊字符导致签名不一致out_trade_no是商户订单号32 字符内重复会报错total_fee是总金额单位分传了小数或元spbill_create_ip是终端 IP传了内网 IP 或空值notify_url是异步通知地址用了 http 或外网不可达trade_type是固定 APP误填 JSAPIsign是签名字符串大小写、编码问题out_trade_no这个字段值得单独说。它的规则是同一个商户号下必须唯一重复提交会直接返回错误。有些项目用时间戳生成订单号秒级并发下就会撞车用 UUID 又太长超过 32 字符。我的做法是“日期 自增序列 随机后缀”比如20240517153000加上几位随机数既可控又不会超长。body字段看起来最无害实际上最容易翻车。如果商品名里带了、、中文标点签名拼接的时候就会错位。我一般会在服务端把 body 做一次清洗只保留中文、字母、数字和常用符号。注意notify_url 必须是公网可直接访问的地址不能带参数不能是内网 IP端口只支持 80 和 443。用测试环境的内网地址去下单表现是订单能创建成功但异步通知永远收不到排查起来非常费时间。3.2 签名算法MD5、HMAC-SHA256 与 APIv3 的区别签名是新手最头疼的部分也是报错最多的地方。APIv2 的签名逻辑其实就四步把所有非空参数按参数名的 ASCII 码从小到大排序拼成keyvaluekeyvalue的形式注意末尾不加key。在拼好的字符串末尾拼接key你的API密钥。对整个字符串做 MD5得到 32 位小写字符串。转成大写作为 sign 字段。用 Python 表达就是这几行import hashlib def build_sign(params: dict, api_key: str) - str: items [(k, v) for k, v in params.items() if v is not None and v ! and k ! sign] items.sort(keylambda x: x[0]) raw .join(f{k}{v} for k, v in items) raw f{raw}key{api_key} return hashlib.md5(raw.encode(utf-8)).hexdigest().upper()这里有几个魔鬼细节。第一空值参数不参与签名但如果你传了空字符串又参与了签名微信那边算出来的结果就不一样。第二编码必须是 UTF-8用 GBK 编码算出来的 MD5 完全是另一个值。第三大小写。APIv2 的 sign 要求大写很多人算出小写直接扔过去微信返回“签名错误”然后对着代码看半天。如果选择 HMAC-SHA256 签名方式前两步完全一样只是第三步换成用 API 密钥做 HMAC 计算结果转小写。要注意的是签名方式是在下单时通过 sign_type 字段指定的而且它参与签名本身。APIv3 是另一套体系安全性高不少。它不用 MD5而是用商户私钥做 SHA256withRSA 签名请求头里带Authorization: WECHATPAY2-SHA256-RSA2048 ...签名串由 HTTP 方法、URL、时间戳、随机串、请求体拼接而成。回调通知则用微信平台证书公钥验签再用 APIv3 密钥做 AES-256-GCM 解密。看起来复杂但好处是不会因为一个字符串排序问题就全军覆没。新项目我建议直接上 v3v2 更像是历史包袱。3.3 返回 prepay_id 之后要做什么下单成功的响应里最有价值的就是prepay_id。但千万别把这个值直接丢给客户端。客户端需要的是二次签名后的一整包参数{ appid: wx1234567890, partnerid: 1900000109, prepayid: wx17160000000000000000000000, package: SignWXPay, noncestr: 5K8264ILTKCH16CQ2502SI8ZNMTM67VS, timestamp: 1716000000, sign: 二次签名结果 }注意这里的package是固定值SignWXPay不是包名也不是订单号。这个字段迷惑性极强我第一次接的时候还以为是 APK 的包名填了 applicationId 进去结果报 -1。二次签名的规则和下单签名类似把这几个参数appid、partnerid、prepayid、package、noncestr、timestamp按字典序排列拼接 API 密钥MD5 后转大写。客户端拿到的这包参数是明文的但 sign 保证了它没法被篡改因为改任何一个字段签名就对不上。timestamp这里要特别注意类型。服务端生成的时候是秒级字符串客户端拼进请求对象时是 long。Java 里req.timeStamp 1716000000是可以的但从 SDK 6.8 之后更推荐用 long如果版本对不上会直接报参数错误。这个坑在不同 SDK 版本之间表现不一样建议以你实际引入的版本为准先在测试环境跑通再上线。4. 客户端调起支付与结果处理的完整实现4.1 一个 IWXAPI 实例贯穿全局客户端这一侧第一个要解决的问题是 SDK 的初始化。这里有个很常见的错误做法在需要支付的 Activity 里临时WXAPIFactory.createWXAPI支付完就不管了。这样做会导致onResp回调找不到归属或者回调时序错乱。正确的做法是全局单例通常在 Application 里初始化一次object WxPayManager { private var api: IWXAPI? null fun init(context: Context, appId: String) { if (api null) { api WXAPIFactory.createWXAPI(context, appId, true) } api?.registerApp(appId) } fun pay(req: PayReq): Boolean { val api api ?: return false if (!api.isWXAppInstalled) return false if (!api.isWXAppSupportAPI) return false return api.sendReq(req) } }这个单例里有两个判断必须做。isWXAppInstalled判断微信是否安装没装的话要给用户提示而不是直接调起。isWXAppSupportAPI判断微信版本是否支持当前 SDK 的接口老版本微信可能在 sendReq 时直接返回 false。还有一个经常被忽略的点registerApp 的调用时机。它需要在发送请求之前完成而且只需要调一次。有些项目在每次支付前都 registerApp 一遍虽然不算错但在某些定制 ROM 上会出现注册状态被重置导致第一次 pai 返回 false、第二次才成功。统一在 Application 里注册就规避了这个问题。提示初始化用的 AppID 必须和下单时服务端用的 AppID 完全一致。曾经遇到一个项目测试环境用的是 A 商户的 AppID服务端配置的是 B 商户结果客户端能调起微信微信那边直接提示“商户参数错误”。排查了整整一个下午。4.2 调起参数的拼装与时间戳陷阱拿到服务端返回的参数之后组装 PayReq 就可以了val req PayReq().apply { appId params.appid partnerId params.partnerid prepayId params.prepayid packageValue params.package // 注意是 packageValue nonceStr params.noncestr timeStamp params.timestamp sign params.sign } WxPayManager.pay(req)这里有个命名上的小坑PayReq 里对应package的字段名是packageValue因为 package 在 Java 里是关键字。用 Kotlin 写的时候req.packageValue SignWXPay是对的写成req.package编译不过。这个错误很蠢但真的有人卡在这里。timeStamp的类型在不同 SDK 版本里有差异。老版本 SDK 里是 String新版本改成了 long。如果你的 Gradle 里依赖版本比较老写timeStamp 1716000000是对的升级到 6.8 之后写字符串会被编译器拒绝。处理办法很简单看看编译报错就知道当前版本要的是什么类型或者干脆把时间戳统一用 long 从服务端传过来。时间戳还有一个隐藏问题它和 prepay_id 的有效期绑定。微信统一下单返回的 prepay_id 有效期是两小时但真正调起支付时微信会校验时间戳与服务器时间的偏差。如果客户端本地时间被用户改过或者时区设置异常会出现“支付参数过期”的提示。我的做法是从下单到调起之间的时间间隔尽量短最好在几十秒内完成如果用户在收银台界面停留太久才点确认也建议重新走一次下单流程拿到新的 prepay_id 再调起。4.3 WXPayEntryActivity 回调的分支处理与幂等回调处理是客户端逻辑的重头戏。前面注册的WXPayEntryActivity里onResp会收到resultCode一共三种resultCode含义客户端该做什么0支付成功提示用户通知服务端查询订单状态-1支付失败/错误提示失败允许重试带上 errCode 便于排查-2用户主动取消静默返回不做任何提示或轻提示很多人把 0 当成“钱已经到账”直接跳转成功页。这里必须强调一遍resultCode 等于 0 只代表用户完成了支付动作不代表你的服务端已经收到钱。真正的到账确认要靠服务端收到异步通知。所以客户端的正确姿势是收到 0 之后向自己的服务端发起一个“查询订单状态”的请求服务端返回已支付才跳成功页。override fun onResp(resp: BaseResp) { if (resp.type ! ConstantsAPI.COMMAND_PAY_BY_WX) return when (resp.errCode) { 0 - queryOrderFromServer() -1 - toast(支付失败请重试) -2 - { /* 用户取消什么都不做 */ } } } private fun queryOrderFromServer() { // 带上 out_trade_no 请求自己的服务端 // 服务端返回已支付 - 跳成功页 // 返回未支付 - 轮询几次仍然未支付则提示支付结果确认中 }这段代码里有个很重要的容错设计轮询。因为微信的异步通知和服务端的处理都有延迟用户刚支付完的一两秒内你的订单状态可能还是“待支付”。如果这时候直接提示失败用户体验会很差。我一般会轮询三到五次每次间隔一秒实在查不到就提示“支付结果确认中请稍后在订单列表查看”。另外WXPayEntryActivity的 onResp 回调可能会重复触发尤其是 launchMode 配置不对的时候。所以在处理里要做幂等比如用一个标记位挡住重复的跳转或者在 onResp 之后立刻 finish 掉当前 Activity。4.4 客户端结果永远不可信服务端异步通知才是准绳异步通知notify_url这一环是整条链路里唯一可信的数据源。它的处理逻辑应该长这样app.route(/wxpay/notify, methods[POST]) def wxpay_notify(): raw request.data.decode(utf-8) # 1. 验签确认是微信发的 if not verify_sign(raw): return xmlreturn_code![CDATA[FAIL]]/return_code/xml # 2. 解析参数 data parse_xml(raw) # 3. 校验金额防止被篡改 order find_order(data[out_trade_no]) if order.total_fee ! int(data[total_fee]): return FAIL # 4. 幂等处理已处理过的直接返回成功 if order.status PAID: return SUCCESS # 5. 改状态、发货、记录日志 mark_paid(order) return xmlreturn_code![CDATA[SUCCESS]]/return_code/xml这里有几个必须做的动作。验签是第一道防线没有验签的处理接口等于开放给全世界。金额校验是第二道防止有人伪造一笔小额订单的通知来骗你的商品。幂等是第三道微信的通知机制是“至少一次”同一笔订单可能收到多次通知如果没有幂等你的发货逻辑就会重复执行。注意收到通知后必须返回 SUCCESS 的 XML否则微信会按照 15 秒、15 秒、30 秒、3 分钟、10 分钟、20 分钟、30 分钟、30 分钟、30 分钟、60 分钟、3 小时、3 小时、3 小时、6 小时、6 小时的节奏反复通知一直持续 24 小时。如果问题出在你自己这边赶紧修如果只是暂时处理不过来先把成功返回给微信自己内部再补偿。5. 上线后最容易炸的几个地方排查清单与实操心得5.1 常见错误码速查表与定位思路微信支付报错的时候日志信息往往很吝啬一个 -1 什么都不告诉你。下面这张表是我这些年攒下来的对应关系基本能覆盖八成的问题现象大概率原因定位方法调起微信立刻返回 -1应用签名不匹配对比开放平台签名与 keytool 输出调起微信立刻返回 -1AppID 与商户号不匹配核对服务端与客户端 AppID提示商户参数错误prepay_id 无效或过期重新下单检查下单参数微信界面闪一下就返回 -2用户主动取消正常无需处理支付完成后没有回调WXPayEntryActivity 路径错误检查类名与包名是否严格一致支付完成后没有回调混淆把回调类混淆了检查 proguard 规则isWXAppInstalled 返回 falseAndroid 11 未声明 queries添加 package 声明下单返回签名错误拼接顺序或大小写问题打印原始签名串比对下单返回订单号重复out_trade_no 不唯一检查订单号生成规则异步通知收不到notify_url 外网不可达用在线工具从公网探测排查这类问题的核心思路是分段隔离。先确认下单接口能不能通把服务端日志打出来看微信返回的原始响应是什么下单通了之后再看调起把客户端拿到的参数完整打印出来和微信文档里的示例逐字段对比调起通了再看回调确认 WXPayEntryActivity 有没有被正确加载可以在这个类的 onCreate 里打一行日志如果连这行日志都没出现那就是路径或者 manifest 的问题。5.2 包名、签名与能调起但支付失败的组合问题有一种非常典型的现象微信能被正常调起收银台也弹出来了但用户一确认支付就报错回调 -1而 errCode 里也没有更多信息。这种“半通不通”的状态八成是包名或签名的问题。微信在调起支付时会做一次校验请求里带的 AppID 对应的移动应用其注册的包名和签名必须和你当前运行的 APK 一致。不一致就直接拒。这里有几个容易出错的地方第一多渠道包。如果你用 productFlavors 打出了多个不同 applicationId 的包但开放平台只注册了主包名那么其他渠道包全部支付失败。解决方式是所有渠道包共用同一个 applicationId只在渠道标识上做区分。第二测试包和正式包。前面说过开放平台只能填一个签名。如果测试阶段填的是 debug 签名正式包用 release 签名就必然失败。我的做法是本地测试也统一用 release 签名通过 signingConfigs 配置这样开发和线上环境完全一致避免最后关头才发现问题。第三加固之后签名变化。有些加固平台会在加固后重新签名如果用的是他们提供的签名那就和开放平台填的对不上了。加固之后务必重新取一次签名 MD5和开放平台的配置核对一遍。第四微信缓存。微信客户端会缓存应用的签名和包名信息有时候你更新了开放平台配置微信那边要过一段时间才刷新。可以在手机上清除微信缓存或者用微信的“开发者调试”功能强制刷新。实测下来清除缓存后重启微信成功率最高。5.3 金额、精度与那些绕不开的边界情况金额这个话题看起来简单实际上边界情况不少。金额单位是分这个已经说过。但还有一些衍生问题。比如一分钱支付的测试有些项目为了测试方便会把金额写死成 1 分上线时忘了改用户花一分钱买走了商品。这种事故听起来离谱但每年都能听到几例。我的建议是在服务端做一层校验如果商品的价格配置和下单金额不一致直接拒绝下单并报警。退款金额的精度同样要注意。部分退款时退款金额必须小于等于订单金额而且多次退款的总额不能超过原订单。如果用浮点数累加退款金额很容易出现99.99 ! 100.00这种问题导致最后一次退款失败。统一用整数分计算这类问题自然消失。优惠券与实付金额的关系也需要提前想清楚。如果有满减、折扣total_fee 应该传用户实际支付的金额而不是商品原价。对账的时候微信那边的金额就是实付金额如果传成了原价账目会对不上。还有一类边界是超时订单。微信统一下单支持设置time_expire参数超时之后这个 prepay_id 就失效了。如果用户在你的 App 里点开支付页放置两个小时后再点支付就会失败。体验更好的做法是在客户端做倒计时快到期时提示用户“订单即将失效请重新下单”。5.4 上线前我必做的检查清单吃了不少亏之后我给自己整理了一份上线前的固定检查项每次接入支付都过一遍开放平台的应用签名是否和 release 包一致服务端的 API 密钥是否和商户平台一致是否放在环境变量里而不是代码里notify_url 是否公网可达是否用了 https异步通知的处理接口是否有验签、金额校验、幂等客户端 onResp 里的 0 分支是否走的是服务端查询而不是直接标成功混淆规则是否包含微信 SDKAndroid 11 的 queries 声明是否加上WXPayEntryActivity 的路径和 exported 属性是否正确订单号生成规则在并发下是否会重复是否有对账和补单机制这份清单看起来啰嗦但每一项背后都有真实的事故案例。尤其是倒数第二项和最后一项前者决定你上线首日会不会被客服电话淹没后者决定你在出问题时能不能快速兜底。6. 订单生命周期的延伸查询、退款与对账6.1 主动查询与补偿任务的实现异步通知并不是百分之百可靠。网络抖动、服务器重启、部署期间的通知丢失都可能让订单卡在“待支付”状态。所以必须有一个补偿机制。最常见的做法是定时任务轮询。每隔几分钟扫一遍创建时间在 30 分钟内、状态还是“待支付”的订单调用微信的订单查询接口APIv2 是/pay/orderqueryv3 是/v3/pay/transactions/out-trade-no/{out_trade_no}如果查到已支付就补上状态和发货逻辑。def compensate_orders(): orders query_db( select * from orders where statusPENDING and created_at now() - interval 30 minute ) for order in orders: result wx_query(order.out_trade_no) if result[trade_state] SUCCESS: mark_paid(order) deliver(order)这个任务的参数要调好。扫描范围太长会浪费请求次数太短又容易漏。30 分钟到 2 小时是我觉得比较合适的区间因为微信订单的支付有效期一般也是这个量级。另外这个任务必须是幂等的和异步通知共用同一套mark_paid逻辑避免两条路走出来的状态不一样。查询接口还有一个用途是排障。用户打电话说“我明明付了钱但订单还是待支付”你拿订单号去查一下trade_state 是 SUCCESS 还是 NOTPAY一目了然。如果查到 SUCCESS 但你的库里还是待支付那就是通知丢失了手动补一下状态即可。6.2 退款流程与对账文件的处理退款是另一条独立的链路。调用退款接口需要用到商户 API 证书这一点和下单不一样v2 退款和 v3 退款都要求证书认证。所以证书文件要妥善保管并且配置好路径。退款接口的关键参数是out_refund_no商户退款单号和out_trade_no原订单号。退款单号也需要唯一规则和下单的订单号类似。退款是异步的提交成功只代表请求被接受真正到账要等一段时间所以退款也需要查状态和对账。对账这块微信提供的是对账单下载接口按天生成包含当天的所有交易明细。我的做法是每天凌晨拉取前一天的对账单和自己的订单表做比对找出三类差异微信有我没有的可能是通知全丢了、我有微信没有的可能是伪造的订单、金额不一致的得重点排查。这三类差异处理完账目基本就平了。对账单文件是压缩包下载后需要解压、解析、入库。文件格式是 CSV字段包括交易时间、商户订单号、微信订单号、金额、手续费等。用 Python 的 csv 模块几行就能处理不需要引入额外依赖。6.3 支付超时与库存回滚的联动最后说说库存。电商场景下用户下单会先锁库存如果支付超时了库存要还回去。这个逻辑和支付状态是联动的。我的做法是订单创建时就写入一个expire_at字段同时通过time_expire参数告知微信这个订单的有效期。定时任务除了补偿支付状态还负责处理超时订单——把状态改为“已关闭”同时把占用的库存加回去。这里要注意顺序。先关闭订单再还库存。如果反过来先还了库存这时用户恰好支付成功了你就要面对“库存已经还了但钱收了”的尴尬局面。正确处理是把订单标记为关闭之后再调微信的关单接口v3 的/v3/pay/transactions/out-trade-no/{}/close如果关单失败说明用户已支付就走支付成功的逻辑。这样就不会两头打架。还有一个细节是超时时间的一致性。客户端展示的倒计时、服务端的 expire_at、微信的 time_expire这三个要尽量对齐最好都以服务端的时间为准。如果客户端本地时间不准倒计时会显示得乱七八糟用户也会困惑。我自己在这些项目里最大的体会是微信支付的代码量其实不大难的是把每一个环节的边界都想清楚。下单、调起、回调、通知、查询、退款、对账每一环都有它自己的失败可能而支付这件事对失败的容忍度极低。所以别急着写代码先把时序图画出来把每个环节的输入输出和失败处理列出来后面敲键盘的时候会顺畅很多。还有一个小心得是测试阶段一定要用真机、真微信、真商户号跑通全流程模拟器上的表现和真机差别很大尤其是调起和回调这一块模拟器上跑通了不代表真机没问题。
返回列表