安卓支付聚合层设计:统一支付宝、微信与银联回调链路
2026/9/15 5:22:48 网站建设 项目流程

简介:面向Android开发者的多支付集成参考资料,完整集成了支付宝、微信、银联三种主流支付SDK,覆盖开放平台注册、参数配置、支付接口调用、异步回调处理等关键环节,适合需要快速接入或深入理解支付流程的开发人员。压缩包共72个文件,以Java源码、XML配置、Gradle构建脚本、JAR依赖为主要构成,还包含签名文件、SO库与说明文档,整体仅1.6MB,结构清晰便于按模块查阅。已有708人学习下载,具备一定的参考热度。通过这份SDK,开发者可以对比研究三家支付渠道的接入差异:支付宝的RSA签名与页面支付接口、微信的WXPayEntryActivity及统一下单流程、银联的证书管理和多渠道支付方式,同时借助配套源码和Readme文档理解工程配置、混淆规则与回调验证逻辑,减少集成踩坑。无论是新手还是资深工程师,均可将其作为支付模块设计与调试的实用样本。

1. 支付聚合是安卓开发绕不开的一层设计

做支付接入三年后,我得出一个反直觉的结论:把支付宝、微信、银联三个官方 SDK 塞进同一个工程,是整件事里最简单的一步。真正难的,是三套完全不同的签名、调起与回调机制,如何收敛成一条可控的业务流程。这个标题与其说是 SDK 的广告,不如说它描述了一个标准的安卓工程命题:一个统一订单模型、一个适配器层、一套状态机,以及一条从调起到回调的全链路可观测性。你会先遇到三种回调载体——PayTask 同步结果、WXPayEntryActivity 的 onResp、银联的 onActivityResult——如果接入第一天不定好聚合边界,支付模块很快会变成改一个渠道就牵动全业务的泥潭。下面把这套链路拆开讲透:设计边界、可运行代码、回调校验,最后是灰度验证手段。

2. 设计聚合层:先看清三套 SDK 的差异,再定统一接口

2.1 三条支付链路的本质差异

用一个电商 App 的场景切入。产品需求是同时支持支付宝、微信、银联三种支付方式,只对接服务端一个统一下单接口。如果不去设计,直接在业务页面里分别写三段逻辑,结果是业务层要理解三套 SDK 的术语,改动一个流程要走三套代码分支。

先说三家的差异点。支付宝走的是「服务端签名、客户端提交」:服务端用应用的私钥对订单参数生成 RSA2 签名串,客户端拿到这个 orderInfo 直接调 PayTask,结果同步返回,由 PayResult 包装 resultStatus、result 和 memo 三个字段,不依赖特定的 Activity 承载回调。微信走的是「服务端下单、客户端二次签名」:服务端调 unifiedorder 拿到 prepay_id,客户端收到服务端拼好的参数字段后,自己组装 PayReq 再调 api.sendReq,回调只能在 WXPayEntryActivity 的 onResp 里收,而且这个 Activity 的包名、类名必须在微信开放平台里登记。银联走的是「服务端取号、客户端受理」:服务端从银联后台拿到 TN 交易流水号,客户端把 TN 丢给 UPPayAssistEx 拉起银联支付控件,结果在 onActivityResult 里返回。

三条链路的差异如果直接铺在业务层,业务代码就会到处散落渠道判断。所以聚合层的第一原则是:渠道差异只准出现在适配器内部,业务层永远只面对一个 PaymentManager。

2.2 统一抽象:门面、适配器与回调模型

我一般会把聚合层分成三层:最外层是 PaymentManager 门面,只暴露 pay(activity, request, callback) 这样的方法;中间是 ChannelAdapter 适配器,每个渠道实现一个;最底层是官方 SDK 本身。这个分层的好处是切渠道、换 SDK 版本都不会波及业务代码。

统一请求模型至少包含渠道标识、订单号、金额和签名串:

data class UnifiedPayRequest( val channel: PayChannel, // WECHAT / ALIPAY / UNIONPAY val orderId: String, // 业务订单号,必须是幂等键 val amount: Long, // 金额,统一用分;避免各家单位不一致 val subject: String, // 商品标题,三家的必传参数 val signedPayload: String // 服务端签名后的原始支付串 )

amount 统一用「分」是我踩过的一个实际教训。支付宝的 total_amount 单位是元,微信的 total_fee 单位是分,银联虽然也以分为单位,但不同接口版本的字段名会变。聚合层一旦定了分,适配器里换算就只发生在一个地方。PayChannel 用枚举而不是字符串,是为了在 when 表达式里拿到编译期穷尽检查,加新渠道时编译器会逼你把所有分支补完。

回调模型对应定义 onSuccess、onCancel、onFailure 三个分支。刻意不把渠道的原始错误码直接透出,而是翻译成统一的 PaymentErrorCode,否则业务层还是要写 if (code == 6001) 这种依赖渠道的代码。

2.3 参数映射表与渠道路由

下面这张表是接入时对照用的,重点看调起凭据、回调载体和成功码三行,聚合层所有适配逻辑都围绕这三行展开。

环节支付宝微信支付银联云闪付
调起凭据orderInfo(RSA2 签名串)prepay_id + 二次签名字段TN 交易流水号
回调载体PayTask 同步返回WXPayEntryActivity.onResponActivityResult
成功标志resultStatus = 9000errCode = 0(ERR_OK)pay_result = "success"
取消标志resultStatus = 6001errCode = -2pay_result = "cancel"
私钥位置仅服务端签名后下发服务端签名,客户端不接触私钥仅服务端签名

路由逻辑放在工厂里,根据请求的 channel 返回对应适配器,同时把回调注册表挂到 PaymentManager 上。这个工厂同时承担环境路由的职责——生产环境返回真实适配器,测试环境返回 MockAdapter 或指向沙箱,具体写法在第 5 章展开。

3. 安卓工程里接入聚合支付:依赖、混淆与三个适配器

3.1 依赖引入与 ProGuard 配置

先处理工程接入。支付宝 SDK 走 Maven 依赖,微信也是,银联官方目前主要以 jar 包形式分发,放到 libs 目录即可。

dependencies { implementation 'com.alipay.sdk:alipaysdk-android:15.8.11' implementation 'com.tencent.mm.opensdk:wechat-sdk-android:6.8.0' implementation fileTree(dir: 'libs', include: ['UPPayPlugin*.jar']) }

版本号以你接入时的官方最新稳定版为准,上面两个版本在多数项目里已经验证过兼容性。三套 SDK 里银联控件的包名历史包袱最重,混淆规则漏一条,线上就会出现调不起支付控件的疑难问题。

-keep class com.alipay.sdk.** { *; } -keep class com.tencent.mm.opensdk.** { *; } -keep class com.unionpay.** { *; } -dontwarn com.alipay.sdk.** -dontwarn com.tencent.mm.opensdk.**

混淆规则的核心逻辑是:SDK 内部大量用反射做回调与组件注册,keep 整个包是最省事的做法。微信 SDK 还要在 AndroidManifest.xml 里注册 WXPayEntryActivity 和 WXEntryActivity,并且把 manifest 里的包名对齐微信开放平台登记的包名,否则真机调试时 onResp 永远不会被调用。

3.2 适配器工厂与支付宝实现

定义适配器接口时,我建议把 isInstalled 也放进去。微信和支付宝都提供判断本机是否安装对应 App 的能力,部分机型上判断结果会影响你是否展示对应的支付入口,这个能力贴着渠道,放在适配器里最自然。

interface PaymentAdapter { fun pay(activity: Activity, request: UnifiedPayRequest, callback: UnifiedPayCallback) fun isInstalled(context: Context): Boolean }

支付宝适配器的核心只有一段线程切换代码:

class AlipayAdapter : PaymentAdapter { override fun pay(activity: Activity, request: UnifiedPayRequest, callback: UnifiedPayCallback) { val payTask = PayTask(activity) Thread { // payV2 的第二个参数控制是否显示支付加载框, // 传 true 可以避免用户在过渡期反复点击。 val result = payTask.payV2(request.signedPayload, true) activity.runOnUiThread { when (result.get("resultStatus")) { "9000" -> callback.onSuccess(request.orderId) "6001" -> callback.onCancel(request.orderId) else -> callback.onFailure( request.orderId, result.get("resultStatus").orEmpty(), result.get("memo").orEmpty() ) } } }.start() } }

PayTask 的同步接口要求在子线程调用,放主线程上跑会出现卡顿甚至 ANR。resultStatus 只有 9000 代表用户端的支付流程成功,但这个成功同样不等同于服务端入账,第 4 章会专门讲回调校验。memo 字段里有服务端返回的原始错误描述,透传给统一回调的 message 参数,排障时能省很多时间。

3.3 微信适配器与 WXPayEntryActivity 的桥接

微信适配器的 pay 方法相对薄,真正的复杂度在 WXPayEntryActivity。因为微信的回调是 Activity 式的,聚合层必须在 WXPayEntryActivity 和业务回调之间架一座桥。常见做法是 PaymentManager 里维护一个待确认映射表,把支付请求和回调入口关联起来。

class WechatPayAdapter(private val api: IWXAPI) : PaymentAdapter { override fun pay(activity: Activity, request: UnifiedPayRequest, callback: UnifiedPayCallback) { val payload = JSONObject(request.signedPayload) PaymentManager.registerPending(request.orderId) val req = PayReq().apply { appId = payload.getString("appid") partnerId = payload.getString("partnerid") prepayId = payload.getString("prepayid") packageValue = "Sign=WXPay" nonceStr = payload.getString("noncestr") timeStamp = payload.getString("timestamp") sign = payload.getString("sign") } api.sendReq(req) } }

微信的 signedPayload 是服务端统一下单后拼好的 JSON,里面已经包含服务端算好的 final sign。客户端不需要再碰任何私钥材料,这是微信和支付宝最大的安全差异。WXPayEntryActivity 的 onResp 里,通过 errCode 把微信的返回值映射为统一回调,然后 finish 掉自己。

class WXPayEntryActivity : Activity(), IWXAPIEventHandler { override fun onResp(resp: BaseResp) { val orderId = PaymentManager.consumePending(resp.transaction) when (resp.errCode) { BaseResp.ErrCode.ERR_OK -> PaymentManager.dispatchSuccess(orderId) BaseResp.ErrCode.ERR_USER_CANCEL -> PaymentManager.dispatchCancel(orderId) else -> PaymentManager.dispatchFailure(orderId, resp.errCode.toString()) } finish() } }

这里有个容易踩的坑:不同场景下微信回调事务号格式不同,有的情况下 resp.transaction 可能是空串。所以注册 pending 时要用自己的业务订单号做 key,回调时优先用自己维护的映射关系,不要依赖微信回调里带的原始事务号。

3.4 银联适配器:onActivityResult 与调用模式

银联适配器是最省事的一个。服务端把 TN 传给客户端后,客户端直接调 UPPayAssistEx.startPay,结果在承载 Activity 的 onActivityResult 里拿到。

class UnionpayAdapter : PaymentAdapter { override fun pay(activity: Activity, request: UnifiedPayRequest, callback: UnifiedPayCallback) { // 第二个参数如果是 null,表示使用默认支付控件; // 测试环境把 mode 换成 "01" 即可连银联测试网关。 UPPayAssistEx.startPay(activity, null, null, request.signedPayload, "00") } fun handlePayResult(data: Intent?, callback: UnifiedPayCallback, orderId: String) { when (data?.getStringExtra("pay_result")) { "success" -> callback.onSuccess(orderId) "cancel" -> callback.onCancel(orderId) else -> callback.onFailure(orderId, data?.getStringExtra("pay_result") ?: "fail", "银联控件返回异常") } } }

startPay 的 mode 参数是银联特有的环境开关,"00" 是生产,"01" 是测试。环境切换在这里天然存在,我一般把它接到聚合层的环境配置上,而不是写死。银联这个 onActivityResult 的 requestCode 建议用 0x01D0 起的固定值,避免与页面里其他 requestCode 冲突。

4. 回调校验、重复通知与支付状态机:线上最常出事的三个点

4.1 客户端回调只是界面状态,服务端入账才是事实

支付模块线上事故里,最高频的一类是「客户端显示成功,订单实际未入账」。以支付宝回调为例,onSuccess 收到 9000 只代表支付流程在用户端走完了,资金是否真实转移,必须以服务端收到的异步通知为准。微信 onResp 返回 ERR_OK、银联返回 success,同理。所以聚合层在设计回调模型时,我特意把 onSuccess 的语义定义为「支付流程成功,等待服务端确认」,接口命名和注释里都强调这点,避免产品同学把客户端成功当成入账成功来引导用户。服务端的异步通知存在重复投递,服务端需要做幂等,客户端也要为重复通知准备好状态机。

4.2 幂等与订单状态机

同一个订单从用户调起到最终确定,状态机里只允许单向流转:

object PaymentOrderGuard { private val states = ConcurrentHashMap<String, Int>() const val PENDING = 0 const val PAYING = 1 const val SUCCESS = 2 const val CANCELED = 3 const val FAILED = 4 fun tryEnterPaying(orderId: String): Boolean { // putIfAbsent 原子完成:同一订单只允许一个并发调起进入 PAYING return states.putIfAbsent(orderId, PAYING) == null } fun canFinalize(orderId: String, target: Int): Boolean { val current = states[orderId] ?: return false // 终态之后不允许再被写入,用于拦截迟到的回调 return current != SUCCESS && current != CANCELED && current != FAILED } }

canFinalize 的存在是为了拦截迟到的回调。实际出现过这样的情况:用户取消了支付,取消回调先到,支付成功的回调后到,如果没有终态检查,订单会被错误地翻成成功。状态机写好后,再在聚合层入口加一层幂等校验,同一个 orderId 的重复回调直接丢弃。

提示:支付宝的 6001 取消也不一定是用户主动取消,弱网下支付请求超时同样会落到 6001。判断取消时不要只看状态码,还要看 memo 和 result 里的原始信息。

4.3 并发调起的拦截与渠道构造失败兜底

并发调起是另一个容易被忽略的场景。用户在网络抖动时连点支付按钮,或者从订单列表和详情页同时进入支付流程,都会对同一个 orderId 发起多次调起。tryEnterPaying 返回 false 时,直接提示「支付处理中」,比第二次拉起支付控件要安全得多。

渠道构造失败的兜底也要在聚合层做:微信 SDK 返回 ERR_AUTH_DENIED、支付宝抛 ActivityNotFoundException、银联控件 jar 加载失败,这些都对应「渠道不可用」。我一般会在工厂里根据 isInstalled 决定入口是否展示,同时在 pay 方法内部把渠道异常统一转成 onFailure,不让任何渠道的运行时异常穿透到业务层。

5. 灰度自检:环境路由、追踪 ID 与回调一致性脚本

5.1 环境路由:一个开关切换三家测试网关

集成调试阶段,最烦的是向服务端同学反复要测试签名串。我一般会在聚合层做一个环境路由,用一个 BuildConfig 字段控制:

buildConfigField("String", "PAY_ENV", "\"SANDBOX\"")

调试包编译为 SANDBOX,发布包改为 PRODUCTION。路由的规则是:支付宝的 orderInfo 走沙箱网关签名,微信走测试商户参数,银联的 mode 传 "01"。考虑到调试机型、系统版本繁杂,运行时的环境切换比编译期开关更实用——我习惯在 debug 界面放一个反射入口,直接调用 PaymentManager.setEnvironment(PayEnvironment.SANDBOX),不用重编包就能切换。

5.2 全链路追踪 ID:把三套回调串成一条日志

三套 SDK 的回调载体互不相同,排障时最痛苦的是对不上号。做法是每次调起支付前生成一个 traceId,塞进订单上下文的 extra 字段,并且让统一回调的三个方法都把它打印出来。日志 Tag 统一用 PayTrace,同一笔支付的调起、回调、状态变更全部带同一个 traceId,grep 一行日志就能看完整条链路。

调试期可以给支付宝适配器注入一个 MockPayTask,逐一模拟 9000、6001、4000 三种 resultStatus,市面上的「支付宝模拟器」类工具也是同样的思路——截住 PayTask 的返回,把各种回调注入进来。这比拿真机反复刷码要快得多,而且能稳定复现取消、超时这类边界路径。

5.3 用脚本核账:本地终端状态与服务端订单最终一致

灰度期每周跑一次核账脚本,是成本最低的可靠性手段。服务端会落一张支付流水表,客户端把每次回调写入本地 SQLite,核账就是比对两端同一批 orderId 的终态。下面是用 Python 做比对的最小脚本:

adb shell "cat /data/data/com.example.app/databases/pay_trace.db" > pay_trace.db python3 check_pay.py pay_trace.db --server-api https://api.example.com/pay/orders
import sqlite3, sys, urllib.request, json # 读取客户端本地终态,再拉服务端终态,输出状态不一致的订单号。 conn = sqlite3.connect(sys.argv[1]) local_final = {row[0]: row[1] for row in conn.execute( "select order_id, final_state from pay_trace where final_state in ('SUCCESS','FAILED','CANCELED')")} resp = json.load(urllib.request.urlopen(sys.argv[3])) for order_id, server_state in resp["orders"].items(): if order_id in local_final and local_final[order_id] != server_state: print(f"{order_id}: client={local_final[order_id]} server={server_state}")

脚本逻辑很直白:SELECT 出本地所有已收敛的终态订单,拉取服务端订单表,把两端终态不一致的订单号打印出来。这类 diff 脚本只输出异常行,正常情况零输出,放到 CI 或定时任务里都不会产生噪音。注意本地的 final_state 必须来自第 4 章的状态机——只有经过 canFinalize 收敛的终态才能参与比对,否则用户清数据、换手机这类行为会制造大量假阳性。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询