1. 项目缘起与整体设计思路
1.1 为什么要在Unity里接支付SDK
做过移动端游戏或者App的朋友都清楚,一旦涉及到虚拟道具购买、会员订阅、去广告解锁这类变现设计,支付环节就是绕不过去的坎。Unity本身是个跨平台引擎,C#层写业务逻辑很舒服,但一碰到平台原生的东西——比如支付、推送、权限申请——就得走原生桥接的路子。微信和支付宝这两家在国内移动支付领域基本是绕不开的选择,所以“Unity接微信支付宝SDK”这件事,几乎每个做国内商业化手游或者工具类App的团队都会遇到。
这个项目的核心目标很明确:在Unity工程里,让C#脚本能够调用微信和支付宝的原生支付能力,完成从下单、拉起支付、回调处理到订单校验的完整闭环。它解决的是Unity跨平台开发中“业务层用C#、支付层用原生”的割裂问题。适合谁看?有一定Unity开发基础、需要接入国内支付渠道的开发者,或者正在做商业化模块的技术负责人。哪怕你之前没碰过Android原生开发,只要跟着思路走,也能把这条链路跑通。
1.2 整体架构怎么拆
我一开始也想过偷懒,直接找个现成的Unity插件包一贴了事。但实际用下来发现,支付这种涉及资金和合规的模块,第三方封装往往版本滞后、回调不透明,出了问题排查起来非常被动。所以最终方案还是走“Unity C#层 + Android原生层 + 平台SDK”的三层结构。
具体来说,Unity侧负责业务逻辑:什么时候发起支付、订单号怎么生成、支付结果怎么展示。Android侧负责桥接:把Unity的调用转成微信/支付宝SDK能识别的请求,再把原生回调转回Unity。平台SDK就是微信开放平台和支付宝开放平台提供的官方库。iOS侧逻辑类似,只是桥接层换成Objective-C或Swift。
这样拆的好处是职责清晰。Unity开发者不需要深入理解原生SDK的每个API,原生开发者也不需要关心游戏业务逻辑。中间通过一个统一的接口层通信,后续换渠道或者加渠道,改动范围可控。
1.3 方案选型背后的考量
在桥接方式上,常见的有两种:一种是Unity的AndroidJavaClass/AndroidJavaObject直接反射调用原生代码,另一种是写一个.aar或.jar插件包,通过AndroidJavaProxy做回调。我选的是后者,原因有三点。
第一,支付SDK的初始化、注册、权限声明这些操作,放在原生层做更自然,用反射硬调容易漏掉生命周期管理。第二,回调处理用AndroidJavaProxy比轮询或者静态变量传递更可靠,尤其是支付这种异步操作,回调时机不确定,必须有个稳定的监听机制。第三,打成插件包后,Unity工程和原生工程可以分开维护,CI流程也好做。
支付宝和微信的接入方式也有差异。支付宝的SDK相对“轻”,主要是一个alipaySdk的aar包,调用PayTask.payV2()就能拉起。微信则复杂一些,需要注册AppID、处理WXPayEntryActivity的回调、还要注意包名和签名的一致性。这些细节后面会展开讲。
2. 核心细节解析与实操要点
2.1 微信支付接入的关键环节
微信支付的接入,最容易被卡住的地方不是代码,而是配置。你得先在微信开放平台创建移动应用,拿到AppID,然后开通支付能力,绑定商户号。这里有个坑:开放平台的AppID和商户平台的商户号是两套体系,需要在商户平台里把AppID关联上,否则调起支付时会报“商户号与AppID不匹配”。
代码层面,Android侧的核心是IWXAPI的注册和PayReq的组装。PayReq里必须填的字段包括appId、partnerId(商户号)、prepayId(预支付会话标识)、packageValue、nonceStr、timeStamp、sign。这些参数全部由服务端统一下单后返回,客户端不要自己拼签名,签名算法在服务端做,客户端只负责透传。
注意:
prepayId的有效期通常是2小时,且一次下单只能调起一次支付。如果用户取消后想重新支付,必须重新走统一下单流程。
回调处理是微信支付最容易出问题的地方。微信要求你在Android工程里创建一个固定包名路径下的WXPayEntryActivity,并在AndroidManifest.xml里注册。这个Activity负责接收微信的回调,然后把结果转发给Unity。我踩过的坑是:这个Activity的onCreate里如果直接finish(),有时候回调还没处理完就销毁了,导致Unity侧收不到消息。正确的做法是在onResp里处理完再finish(),并且用Intent把结果传给Unity的代理对象。
2.2 支付宝支付接入的差异点
支付宝的接入流程相对简单,但有几个细节不能忽略。首先,支付宝的SDK不需要像微信那样注册AppID到某个Activity,它是通过PayTask直接调起的。你只需要把服务端签名后的订单字符串(orderInfo)传给payV2()方法即可。
orderInfo的格式是URL键值对,包含app_id、biz_content、charset、method、sign_type、timestamp、version、sign等字段。其中biz_content是一个JSON字符串,里面包含out_trade_no(商户订单号)、total_amount(金额)、subject(标题)、product_code(固定为QUICK_MSECURITY_PAY)。这些同样由服务端生成,客户端不要参与签名。
支付宝的回调是在payV2()的返回值里同步返回的,返回的是一个JSON字符串,包含resultStatus和result。resultStatus为9000表示支付成功,8000表示正在处理中,4000表示失败,6001表示用户取消。这里要注意:8000状态不能当作成功处理,必须等服务端异步通知确认。
实操心得:支付宝的同步回调不可靠,尤其是在网络不稳定的情况下。最终订单状态一定要以服务端的异步通知为准,客户端只做UI展示。
2.3 Unity与原生层的通信设计
Unity和Android的通信,我设计了一个统一的PayBridge类。Unity侧通过AndroidJavaClass获取到这个类的实例,然后调用Pay(string channel, string orderInfo)方法。channel参数区分“wechat”和“alipay”,orderInfo是服务端返回的订单信息。
回调方面,Unity侧实现一个IPayCallback接口,通过AndroidJavaProxy包装后传给原生层。原生层在收到支付结果后,调用onPayResult(int code, string message)方法,Unity侧就能在C#里处理了。这种设计的好处是,后续如果加新的支付渠道,只需要在原生层扩展,Unity侧的接口不用变。
iOS侧的通信机制不同,用的是DllImport和extern "C"的函数导出。Unity C#通过[DllImport("__Internal")]声明外部函数,iOS原生侧用C函数实现,再桥接到Objective-C的支付SDK。回调则通过UnitySendMessage把消息发回Unity的某个GameObject。
3. 实操过程与核心环节实现
3.1 环境准备与工程配置
先说一下我用的环境:Unity 2021.3 LTS,Android Studio 2022,微信SDK用的是wechat-sdk-android-without-mta的6.8.0版本,支付宝SDK用的是alipaysdk-android的15.8.03版本。JDK版本是11,Gradle版本是7.5。
第一步是在Android Studio里创建一个Library模块,把微信和支付宝的aar包放进去。微信的aar需要从开放平台下载,支付宝的aar可以从Maven仓库拉取。然后在build.gradle里配置依赖:
dependencies { implementation files('libs/wechat-sdk-android-without-mta-6.8.0.aar') implementation 'com.alipay.sdk:alipaysdk-android:15.8.03' }第二步是配置AndroidManifest.xml。微信需要声明WXPayEntryActivity,支付宝需要声明一些权限(如网络权限)。另外,微信的WXPayEntryActivity必须放在你的包名.wxapi目录下,这是硬性要求,包名写错回调就收不到。
第三步是把编译好的aar包导入Unity。在Unity的Assets/Plugins/Android目录下放入aar文件,并在mainTemplate.gradle里添加依赖。如果用的是Unity 2019以上的版本,还需要在gradleTemplate.properties里加上android.useAndroidX=true和android.enableJetifier=true,因为微信和支付宝的新版SDK都依赖AndroidX。
3.2 微信支付的核心代码实现
原生侧的核心类是WxPayEntryActivity,它继承自Activity并实现IWXAPIEventHandler接口。在onCreate里注册微信API:
IWXAPI api = WXAPIFactory.createWXAPI(this, APP_ID, false); api.handleIntent(getIntent(), this);onResp方法是回调入口,判断resp.getType()是否为ConstantsAPI.COMMAND_PAY_BY_WX,然后根据resp.errCode处理结果。errCode为0表示成功,-1表示错误,-2表示用户取消。处理完后,通过UnityPlayer.UnitySendMessage把结果发给Unity的GameObject。
Unity侧发起支付的代码:
public void PayWithWechat(string orderInfo) { AndroidJavaClass bridge = new AndroidJavaClass("com.example.paybridge.PayBridge"); bridge.CallStatic("payWithWechat", orderInfo); }orderInfo是一个JSON字符串,包含服务端返回的prepayId、nonceStr、timeStamp、sign等字段。原生侧解析后组装成PayReq对象,调用api.sendReq(req)拉起微信。
注意:微信支付要求应用签名与开放平台登记的一致。调试时如果换了签名,需要重新在开放平台更新签名,否则会报“签名错误”。
3.3 支付宝支付的核心代码实现
支付宝的原生侧代码更简洁:
PayTask payTask = new PayTask(activity); String result = payTask.payV2(orderInfo, true);result是一个JSON字符串,解析后拿到resultStatus和result。resultStatus为9000时,再解析result里的alipay_trade_app_pay_response,确认code为10000才算真正成功。
Unity侧的调用方式和微信类似,只是channel参数不同。回调处理上,支付宝是同步返回,所以原生侧拿到结果后直接通过UnitySendMessage发给Unity即可,不需要额外的Activity。
这里有个细节:支付宝的payV2方法必须在主线程调用,否则会抛异常。如果你的Unity逻辑在子线程里,记得切回主线程再调。
3.4 服务端下单与签名校验
客户端不参与签名,这是铁律。服务端的流程是:接收客户端的下单请求(包含商品ID、金额、用户ID),生成商户订单号,调用微信/支付宝的统一下单接口,拿到预支付信息后,用自己的私钥签名,再把签名后的订单信息返回给客户端。
微信的签名算法是MD5或HMAC-SHA256,支付宝是RSA2。签名的原文是参与请求的所有参数按字典序排序后拼接,再加上密钥。这一步如果出错,支付调起时会直接报“签名验证失败”。
实操心得:服务端下单接口一定要做幂等处理。同一个商户订单号重复下单,微信会返回相同的
prepayId,支付宝会返回错误。建议用用户ID+商品ID+时间戳生成订单号,并在数据库里做唯一索引。
订单校验环节,微信和支付宝都会在支付成功后异步通知服务端。服务端收到通知后,要验证签名、核对金额、更新订单状态。客户端展示的“支付成功”只是UI层面的反馈,真正的订单状态以服务端为准。
4. 常见问题与排查技巧实录
4.1 微信支付调不起的几种情况
微信支付调不起,是最常见的问题。我整理了一个排查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 点击支付无反应 | AppID未注册或注册失败 | 检查IWXAPI的registerApp返回值 |
| 提示“签名错误” | 应用签名与开放平台不一致 | 用微信提供的签名生成工具重新生成 |
| 提示“商户号与AppID不匹配” | 开放平台未关联商户号 | 登录商户平台检查AppID绑定 |
| 回调收不到 | WXPayEntryActivity包名或路径错误 | 确认包名是否为xxx.wxapi |
| 支付成功后游戏内未到账 | 服务端异步通知未处理 | 检查服务端回调日志 |
其中“回调收不到”这个问题,我遇到过好几次。有一次是因为WXPayEntryActivity放在了错误的包名路径下,微信找不到这个Activity,回调自然就丢了。还有一次是因为Activity的launchMode设置成了singleTask,导致onNewIntent没触发。后来改成standard模式就正常了。
4.2 支付宝支付返回6001的处理
6001表示用户取消支付。这个状态本身不是错误,但很多开发者会把它当成失败处理,弹一个“支付失败”的提示,用户体验很差。正确的做法是:6001时静默处理,或者提示“您已取消支付”,不要报错。
另外,8000状态表示支付结果未知,正在处理中。这时候不能直接告诉用户“支付成功”,也不能说“支付失败”,应该提示“支付处理中,请稍后查看订单状态”。然后客户端可以轮询服务端接口,确认最终结果。
注意:支付宝的同步回调在极端情况下可能不返回,比如用户支付后直接杀掉了App。所以服务端的异步通知是唯一的可靠来源。
4.3 Unity与原生通信的坑
AndroidJavaProxy在Unity里用起来方便,但有个坑:如果原生层在子线程里回调,Unity侧的方法会在子线程执行,而Unity的很多API(比如GameObject.Find)只能在主线程调用。解决办法是在原生层用runOnUiThread把回调切回主线程,或者在Unity侧用MainThreadDispatcher做线程调度。
还有一个坑是混淆。如果原生层开了ProGuard,AndroidJavaProxy对应的接口类可能被混淆,导致回调失败。需要在proguard-rules.pro里加上:
-keep class com.example.paybridge.** { *; } -keep interface com.example.paybridge.** { *; }iOS侧的问题主要是UnitySendMessage的字符串编码。如果支付结果里有中文,直接传可能会乱码。建议原生侧把结果转成Base64或者URL编码后再传给Unity,Unity侧解码。
4.4 订单金额与货币单位
微信支付的金额单位是“分”,支付宝是“元”。这个差异很容易搞混。比如用户支付1元,微信的total_fee要传100,支付宝的total_amount要传1.00。如果传错了,要么支付金额不对,要么直接报参数错误。
我的做法是在服务端统一用“分”作为内部单位,返回给客户端时再根据渠道转换。这样业务逻辑里只用处理一种单位,减少出错概率。
5. 多渠道扩展与后续维护
5.1 加新渠道的接口设计
前面提到,Unity侧的PayBridge设计成了Pay(string channel, string orderInfo)的形式。如果要加一个新的支付渠道,比如某手机厂商的游戏中心支付,只需要在原生层实现对应的支付逻辑,然后在Pay方法里加一个switch分支即可。Unity侧的业务代码完全不用改。
回调接口也是统一的onPayResult(int code, string message)。code的定义可以统一成:0成功,1失败,2取消,3处理中。不同渠道的原生回调映射到这个统一编码上,Unity侧只认这套编码。
5.2 日志与监控
支付模块的日志一定要打全。我一般在原生侧和Unity侧都加日志,关键节点包括:发起支付、调起SDK、收到回调、服务端校验。日志里要包含订单号、渠道、金额、结果码,方便对账和排查。
监控方面,可以统计每个渠道的支付成功率、平均耗时、失败原因分布。如果某个渠道的成功率突然下降,可能是SDK版本问题或者平台配置变更,能第一时间发现。
5.3 版本升级的注意事项
微信和支付宝的SDK版本更新比较频繁,升级时要注意几点。第一,先看更新日志,确认有没有破坏性变更。第二,在测试环境充分验证,尤其是回调流程。第三,如果用了混淆,检查新的SDK有没有需要额外keep的类。第四,iOS侧的SDK升级可能涉及Info.plist的配置变更,比如URL Scheme的白名单。
我个人的习惯是,支付SDK的版本不追新,稳定优先。除非有安全漏洞或者必须的新功能,否则不轻易升级。升级前一定在沙箱环境跑通全流程,再上生产。
5.4 安全方面的几个底线
支付模块的安全底线不能破。第一,签名必须在服务端做,客户端不碰私钥。第二,订单金额以服务端为准,客户端传的金额只做展示,不做校验依据。第三,异步通知必须验签,防止伪造回调。第四,订单状态以服务端数据库为准,客户端展示的成功只是参考。
还有一点,支付相关的日志不要打印敏感信息,比如完整的订单信息、签名、密钥。日志脱敏在支付模块里尤其重要。
6. 个人实操体会与建议
这个项目做下来,最大的感受是:支付接入的难点不在代码,而在配置和流程。代码写起来可能就几百行,但配置环节的坑能让你调一整天。我的建议是,先把微信和支付宝的开放平台文档通读一遍,把该申请的权限、该绑定的关系、该配置的参数全部列成清单,逐项确认。不要一边写代码一边查配置,那样效率很低。
另外,测试阶段一定要用真实的小额支付来验证,比如支付0.01元。沙箱环境虽然方便,但和真实环境的差异还是有的,尤其是回调的时机和签名校验。我遇到过沙箱正常、生产报签名错误的情况,最后发现是生产环境的密钥配错了。
最后说一个细节:Unity的AndroidJavaProxy在IL2CPP模式下和Mono模式下行为略有差异。如果打包时用的是IL2CPP,建议在开发阶段就用IL2CPP模式测试,避免上线后才发现回调失效。这个坑我踩过一次,排查了大半天才定位到是脚本后端的问题。