ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

安卓支付聚合层设计:统一支付宝、微信与银联回调链路

安卓支付聚合层设计:统一支付宝、微信与银联回调链路 简介面向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 参数映射表与渠道路由下面这张表是接入时对照用的重点看调起凭据、回调载体和成功码三行聚合层所有适配逻辑都围绕这三行展开。环节支付宝微信支付银联云闪付调起凭据orderInfoRSA2 签名串prepay_id 二次签名字段TN 交易流水号回调载体PayTask 同步返回WXPayEntryActivity.onResponActivityResult成功标志resultStatus 9000errCode 0ERR_OKpay_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 SignWXPay 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 ConcurrentHashMapString, 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同一笔支付的调起、回调、状态变更全部带同一个 traceIdgrep 一行日志就能看完整条链路。调试期可以给支付宝适配器注入一个 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/ordersimport 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 收敛的终态才能参与比对否则用户清数据、换手机这类行为会制造大量假阳性。本文还有配套的精品资源点击获取
返回列表