简介:面向Android开发者的支付集成资料,整合支付宝、微信、银联三种主流支付SDK,覆盖开放平台注册、参数配置、接口调用、安全签名及异步回调等完整链路,适合需要快速接入或深入理解移动支付原理的初中级开发者。压缩包共72个文件,以Java源码、XML配置、Gradle脚本、JAR依赖及SO动态库为主,辅以工程说明文档与签名文件,整体仅1.6MB,结构紧凑,便于对照学习。目前已有708人学习下载。资源内提供完整的pay_sdk工程,包含三种支付方式的分模块实现,可查看支付入口、回调Activity、参数组装与验签逻辑等核心代码;附带的Readme和目录组织能帮助开发者理清接入步骤,遇到问题时也可参考其中JKS签名、ProGuard混淆规则等配置,减少踩坑。
1. 三个支付 SDK 同时维护,账要对得上
同事把 pay_sdk-master 工程丢过来时,我第一反应是翻 libs 目录下有多少 jar 和 aar。这工程值钱的地方,不是把支付宝、微信、银联三家的 SDK 堆在了一起,而是把三套完全不同的初始化、回调协议和验签逻辑,压在同一 Android 工程里做了归一化。支付宝走 payTask 唤起、微信要注册 IWXAPI、银联绕不开 UPPayPlugin——三者在 manifest、混淆和网络权限上的要求互不兼容,能在一个工程里跑通,说明作者把三份官方文档真正读过了一遍。对聚合支付开发而言,这份工程的核心是三条完整链路,而不是压缩包体积。
2. 初始化差异:libs 依赖、manifest 与混淆规则的拆解
聚合支付 SDK 工程的第一步不是写代码,而是先把三个渠道的初始化机制搞清楚。三者里只有支付宝的 SDK 是“拿来即用”的,官方 jar 内部自带了支付能力,不需要在应用启动阶段做任何注册动作;微信必须先将IWXAPI注册到微信开放平台,拿到校验后的IWXAPI实例才能发起支付;银联则是通过UPPayAssistEx这个工具类动态反射调用,依赖libs下的UPPayPluginExPro.jar和UPPayAssistEx.jar。这三套机制的并存,决定了工程目录里paysdk、app、libs三个模块各自的职责边界。
2.1 工程目录与 sourceSets 的关系
解压后的pay_sdk-master目录里,app模块是演示工程,paysdk是被引用的封装库,libs目录存放三家的终端 SDK 实体文件。以下是最常见的一种组织方式:
android { compileSdkVersion 28 sourceSets { main { jniLibs.srcDirs = ['libs'] manifest.srcFile 'src/main/AndroidManifest.xml' java.srcDirs = ['src/main/java'] res.srcDirs = ['src/main/res'] } } } dependencies { implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.squareup.okhttp3:okhttp:3.12.0' }这段配置的关键点是jniLibs.srcDirs = ['libs']。银联 SDK 的.so文件如果放在libs/armeabi、libs/armeabi-v7a下,但没被声明为 jniLibs 目录,运行时就会直接抛UnsatisfiedLinkError,错误信息表现为UPPayAssistEx的静态方法调用失败。fileTree则保证libs下所有 jar 和 aar 都会被构入最终产物,不需要逐个声明依赖。
提示:如果使用 Android Studio 4.0 以上版本,
compileSdkVersion建议不低于 30,否则部分微信 SDK 的 targetSdkVersion 校验分支会走不通,但具体的编译 SDK 版本以你工程实际为准。
2.2 三套初始化 API 的对照
三个渠道的初始化差异可以直接用一张表说清楚:
| 渠道 | 初始化入口 | 是否需要应用上下文 | 失败表现 |
|---|---|---|---|
| 支付宝 | 无显式初始化,PayTask构造时传入 Activity | 不需要,但需要 Activity 实例 | 唤起支付宝后立即返回 9000 以外的结果码 |
| 微信 | IWXAPI.registerApp(appId) | 需要,建议在Application.onCreate中执行 | onResp不回调,或返回ERR_UNSUPPORTED |
| 银联 | UPPayAssistEx反射加载,无需注册 | 不需要 | 返回UPPayPluginError,常见为未安装控件 |
微信的注册这一步最容易被忽略,而它的坑也最隐蔽。registerApp必须在启动阶段完成,不能在点击支付按钮时才执行,因为 SDK 内部需要提前把 appId 和包名签名信息发送给微信客户端建立双向连接。如果注册成功,微信客户端会调用应用里的WXPayEntryActivity;注册失败则只有日志输出,界面没有任何提示,开发阶段很难定位。
2.3 proguard 规则的三段式处理
混淆规则是第三个需要单独处理的维度。三个渠道的 SDK 内部都有大量反射调用,尤其银联的UPPayAssistEx是动态反射com.unionpay.UPPayPlugin,混淆后类名和方法名一旦被改写,运行期必然抛ClassNotFoundException。完整工程的proguard-rules.pro文件至少需要以下规则:
# 支付宝 -keep class com.alipay.sdk.** { *; } -dontwarn com.alipay.sdk.** # 微信 -keep class com.tencent.mm.opensdk.** { *; } -keep class com.tencent.wxop.** { *; } -dontwarn com.tencent.mm.opensdk.** # 银联 -keep class com.unionpay.** { *; } -keep class org.json.** { *; } -dontwarn com.unionpay.**dontwarn的作用是压制非关键警告,因为微信旧版本 SDK 里存在引用android.support.annotation但未声明依赖的情况。org.json的 keep 规则是银联特有的,老版本银联 SDK 直接引用了 org.json 内部类,这部分如果不 keep,混淆后会因为 JSON 处理反射失败而返回银联支付控件加载失败。压缩包里自带的proguard-rules.pro已经给出了参考,实际接入时建议把这段规则直接抄进自己的主工程,不要只依赖paysdk模块里的那份。
3. 下单与唤起:三个支付渠道的执行链路拆开看
初始化完成之后,三条支付链路的差异会集中体现在“下单参数组装”和“结果返回”这两个环节上。先说结论:支付宝最独立,全部参数由服务端签名后返回客户端,客户端只做唤起;微信要求客户端先调统一下单接口获得prepayId,再交给IWXAPI发起二次请求;银联的流程介于两者之间,客户端拿到的tn(交易流水号)直接传给 SDK 即可。三者链路的共同点是:客户端永远不要做 RSA 私钥签名,签名必须在服务端完成。
3.1 支付宝:PayTask 的单线程约束
支付宝的PayTask在com.alipay.sdk.app.PayTask下,核心调用代码非常简单,但有一个重要的线程约束——payV2方法必须在主线程调用,并且调用成功后要把结果交由onActivityResult或回调统一处理:
private void startAlipay(Activity activity, final String orderInfo) { final PayTask payTask = new PayTask(activity); Runnable payRunnable = new Runnable() { @Override public void run() { Map<String, String> result = payTask.payV2(orderInfo, true); // payV2 是同步阻塞方法,需要切回主线程处理 Message msg = Message.obtain(); msg.what = SDK_PAY_FLAG; msg.obj = result; mHandler.sendMessage(msg); } }; new Thread(payRunnable).start(); }这里的orderInfo是服务端根据alipay.trade.page.pay接口生成的订单字符串,包含了app_id、method、sign等参数,客户端不能自行拼接。payV2的第二个布尔参数表示是否需要在唤起前弹出确认弹窗,一般传true。支付宝的结果码需要特别留意:9000 表示支付成功,但 8000 表示正在处理中,此时客户端不能直接把订单置为已支付,必须等待服务端收到异步通知后做最终确认。
3.2 微信:WXPayEntryActivity 的声明与回调
微信支付的回调入口不是通过接口拿返回值,而是通过一个特定的 Activity。这个WXPayEntryActivity需要在 manifest 里显式声明,并且包名路径必须与APP_ID的签名包名一致,否则回调永远进不来:
public class WXPayEntryActivity extends Activity implements IWXAPIEventHandler { private IWXAPI api; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); api = WXAPIFactory.createWXAPI(this, Constants.APP_ID); api.handleIntent(getIntent(), this); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); api.handleIntent(intent, this); } @Override public void onResp(BaseResp baseResp) { if (baseResp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX) { int code = baseResp.errCode; // 0 成功, -1 错误, -2 用户取消 } } }onNewIntent的重写很关键,微信客户端在部分机型上是通过单实例模式拉起的 Activity,如果只实现onCreate里的handleIntent,第二次支付时回调可能丢失。baseResp.errCode只能作为参考,0只是微信客户端“确认已支付”的回执,不代表服务端一定收到了微信支付结果通知。实际项目里通常还要在onResp里取出returnKey与服务端对账,防止中间层丢单。
3.3 银联:tn 流水号的边界情况
银联的UPPayPluginExPro调用模式在三个渠道里最特殊,它直接依赖厂商私有 APK 提供的插件控件。调用时只需要把服务端返回的tn字符串传入,SDK 会自动完成后续流程:
UPPayAssistEx.startPay(this, null, null, tn, "00", new UPPayPluginCallBack() { @Override public void onPayEnd(boolean result, String msg) { if (result) { // 支付成功回调,仍需服务端二次确认 } } });"00"表示正式环境,测试环境对应"01"。这里需要注意「控件安装」的判断逻辑:老版本银联 SDK 在用户未安装银联控件时会自动跳转下载页,但在部分国产 ROM 上跳转会被拦截,表现为onPayEnd返回false且msg为 “控件加载失败”。处理方案是先调用UPPayAssistEx.checkServiceEnabled检查环境,再决定是否弹出自定义的引导下载对话框。聚合支付场景下建议只在用户手动选择银联支付时才走这条链路,不要把它设计成默认支付方式,因为支付成功率受终端控件环境影响较大。
4. 验签、证书与密钥管理的工程化落地
三个渠道在支付结果通知上都采用了「客户端回调 + 服务端异步通知」的双通道模式。客户端回调是给用户看的,决定 UI 跳转;服务端异步通知是给账算的,决定订单状态。后者必须做验签处理,而验签算法在三个渠道之间并不统一——支付宝使用 RSA2(SHA256withRSA),微信使用 HMAC-SHA256 或 MD5,银联则是基于数字证书的验签。pay_sdk-master工程根目录下的sinoli.jks文件也隐晦地提醒了另一件事:签名证书的管理本身就是支付工程的一部分。
4.1 服务端验签的核心逻辑
以支付宝为例,异步通知的验签参数是一个扁平的Map<String, String>,开发者在服务端拿到后需要先剔除sign、sign_type两个字段,把剩余参数按字典序拼接后再做 RSA 验证:
public static boolean rsa2CheckV2(Map<String, String> params, String publicKey) { String sign = params.get("sign"); String signType = params.get("sign_type"); params.remove("sign"); params.remove("sign_type"); List<String> sortedKeys = new ArrayList<>(params.keySet()); Collections.sort(sortedKeys); StringBuilder content = new StringBuilder(); for (int i = 0; i < sortedKeys.size(); i++) { String key = sortedKeys.get(i); String value = params.get(key); content.append(i == 0 ? "" : "&") .append(key) .append("=") .append(value); } try { Signature signature = Signature.getInstance("SHA256withRSA"); PublicKey pubKey = getPublicKeyFromString(publicKey); signature.initVerify(pubKey); signature.update(content.toString().getBytes("UTF-8")); return signature.verify(Base64.decode(sign.getBytes(), Base64.NO_WRAP)); } catch (Exception e) { return false; } }参数排序是整个验签过程里最容易出错的地方。支付宝要求的是「字典序升序」,微信支付要求的是「字典序升序后先编码再拼接」,银联要求的是「证书私钥签名的原文与报文一致」。同一个聚合支付服务端,如果为三个渠道各自实现验签逻辑,代码量会成倍增长。常见做法是写一个统一接口,传入渠道枚举,内部用策略模式分发到各自的验签实现,这样后续接京东白条或云闪付时只需要扩展新的实现类。
提示:判断商户通知是否可信还有一个额外条件——来源 IP。支付宝的异步通知来源 IP 固定范围会写在官方文档里,微信也有固定的通知域名;服务端验签通过后,建议再校验一遍来源域名或 IP,避免内网 DNS 被改写时验签被绕过。
4.2 私钥与证书的存放边界
支付宝和微信都只需要在服务端存私钥,客户端存公钥或不需要存任何密钥。但银联的数字证书机制要求商户持有 pfx 证书文件,这个文件不能放在 Android 的 assets 资源目录下,否则反编译后外部可以直接拿到证书链。sinoli.jks是app模块演示用的签名证书,它解决的问题是包名与微信开放平台上登记的签名一致性,而不是银联的商户证书。两者如果混为一谈,会在「打包上线后微信支付唤不起」和「银联验签一直失败」这两个问题上反复踩坑。
实践上我的处理方式是:微信和支付宝的公钥通过接口从服务端下发,客户端不做静态存储;银联证书路径写到buildConfigField中区分 debug 和 release 环境,release 环境只保留证书指纹,不在客户端持有完整的 pfx 文件。因为银联 SDK 本身支持服务端下发交易流水号后由服务端保存证书,客户端实际上用不到 pfx。这一点很多集成文档没有写透,导致不少团队在客户端包里塞了证书却不自知。
4.3 回调幂等的数据库收口
支付结果异步通知在生产环境是重复触发的,支付宝会按一定时间策略重发 24 小时,微信的通知重试策略也类似。客户端和服务端都必须满足幂等,否则用户支付成功后订单被重复置为已支付,后续发货逻辑就会执行两次。
SQL 层面的收口方案一般是给订单表加唯一约束,或在更新订单时带上前置状态条件:
UPDATE orders SET status = 'PAID', paid_at = NOW() WHERE order_id = #{orderId} AND status = 'UNPAID'这里WHERE status = 'UNPAID'是关键。同一笔订单的通知除非极端异常,否则只会成功执行一次更新,第二次通知到达时status已经不是UNPAID,更新行数为 0,直接丢弃该通知。对于「通知到了但订单查不到」的场景,需要先记录日志再返回渠道要求的成功标记,避免渠道方误以为通知投递失败而反复重试,最终形成告警风暴。
5. 联调环境与回调防重入的验证技巧
支付 SDK 的联调最恼人的不是代码写不对,而是「真金白银地付了一分钱,结果回调没进来」。针对这类问题,我总结了一套基于日志埋点和状态机的验证方法,尤其适合在接入pay_sdk-master这类聚合工程时使用。
先做一层“三方状态机”日志埋点,以orderId为追踪维度,把所有渠道的支付结果统一转换成一组状态常量:INIT、SERVER_SIGNED、CLIENT_CALLED、CHANNEL_RETURNED、SERVER_NOTIFIED、SETTLED。每笔订单在进入下一个状态时打印一条带orderId和渠道标识的日志。联调时只用一条 grep 命令就能看清问题出在哪个环节:
adb logcat -s PaySdk | grep "orderId_20240601"如果一个订单的日志停在CLIENT_CALLED再也没有推进,说明微信的onResp或支付宝的onActivityResult没有触发,先查 manifest 里的WXPayEntryActivity路径与签名是否匹配;如果状态推进到了CHANNEL_RETURNED但服务端始终收不到SERVER_NOTIFIED,那问题在服务端异步通知的验签上,优先核对服务端的公钥和排序算法。
第二层验证技巧利用微信支付和支付宝均支持的「预下单模式」做链路测试。服务端预下单接口分为“前台收银台下单”和“纯 API 下单”两种,测试时选后者,客户端拿到签名后的订单串但不唤起收银台,而是把签名串直接丢给一个只做验签的 mock 接口,验证签名数据的完整性与服务端是否一致。这个方法可以完全避免反复扫码,测试效率提升非常明显。
银联渠道因为没有可用于测试的客户端 mock 环境,常见的做法是把tn参数复制到官方测试用例中手动跑一遍流程。银联测试环境通常允许任意金额下单,实际支付时使用测试卡号即可,不需要真实扣款。这里有一个容易被忽略的细节:银联的tn有效期非常短,如果服务端和客户端时间不一致,tn很可能在到达客户端时已经过期。联调时建议把服务端下发tn的时间戳也打进日志,配合adb shell date对比两边的时钟偏差,确认过期问题的根因。
最后一招是给回调处理逻辑加一个“安全网重复确认”机制。客户端收到回调后,不直接修改 UI,而是用orderId调一次服务端的订单查询接口,以服务端的SETTLED状态为准刷新界面。这样即使三个渠道的回调都发生了丢单,或者用户在支付成功后立刻杀掉了 App,下次进入订单页面时依然能从服务端同步到正确状态。这个机制的成本极低,却能规避掉绝大多数「用户已付款但页面还显示未支付」的客诉场景,建议所有聚合支付工程都加上。
本文还有配套的精品资源,点击获取