94、支付集成:支付宝支付、微信支付、银联支付、服务端签名

支付集成,说白了就是让用户能在你的App里花钱。嗯,这件事做得好,用户觉得顺滑;做得不好,用户可能直接卸载App。我这些年接过的支付方案,少说也有几十个了,踩过的坑能写满一本小册子。

今天咱们就聊聊支付宝、微信、银联这三家主流支付,以及那个绕不开的——服务端签名。

支付流程的核心逻辑

不管哪家支付,流程都差不多。我画了张图,你看一眼就明白了:

支付集成核心流程 Android客户端 业务服务端 支付平台 ①下单请求 ②获取支付参数 ③返回签名 ④返回支付参数 ⑤调起支付 支付SDK ⑥支付结果 结果回调 ⑦通知服务端 ⑧异步通知

流程其实就这几步:客户端请求下单 → 服务端去支付平台拿参数 → 返回签名 → 客户端调起支付 → 结果回调。嗯,这里有个关键点——签名必须在服务端做。为什么?往下看。

服务端签名:为什么不能放在客户端

我见过不少新手直接把App Key和Secret写在客户端代码里。结果呢?被反编译后,别人拿着你的Key去刷单,一夜之间亏了几十万。这不是段子,是我真实见过的案例。

服务端签名的核心原因就两个:

  • 安全:密钥放在服务端,客户端拿不到,无法被逆向
  • 可控:服务端可以做金额校验、订单校验,防止篡改
⚠️ 警告: 绝对不要在客户端硬编码任何支付密钥。我曾经接手过一个项目,支付宝的App Secret直接写在strings.xml里……嗯,那个项目后来重写了。

支付宝支付集成

支付宝的集成相对简单。我个人习惯用「App支付」方案,也就是客户端调起支付宝App进行支付。

服务端返回的支付参数是一个签名字符串,客户端拿到后直接调起:

// 支付宝支付 - 客户端代码
public void startAlipay(String orderInfo) {
    final AlipayClient alipayClient = new DefaultAlipayClient(
        "https://openapi.alipay.com/gateway.do",
        APP_ID,
        APP_PRIVATE_KEY,
        "json",
        "UTF-8",
        ALIPAY_PUBLIC_KEY,
        "RSA2"
    );
    
    AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
    request.setBizContent("{" +
        "\"out_trade_no\":\"20231001001\"," +
        "\"total_amount\":\"0.01\"," +
        "\"subject\":\"测试商品\"," +
        "\"product_code\":\"QUICK_MSECURITY_PAY\"" +
    "}");
    
    // 这里返回的是签名字符串
    AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
    String orderString = response.getBody();
    
    // 调起支付宝
    final PayTask alipay = new PayTask(activity);
    alipay.payV2(orderString, true);
}
💡 小提示: 支付宝的签名结果是一个URL编码的字符串,客户端直接传给PayTask即可。注意RSA2签名算法,别用老旧的RSA。

微信支付集成

微信支付和支付宝不太一样。微信需要先调起「预付订单」接口,拿到prepay_id,然后再调起支付。

我记得第一次接微信支付时,被那个「调起支付」的参数搞晕了——为什么要有sign?后来才明白,微信的签名是客户端自己算的,但参数是服务端给的。

// 微信支付 - 客户端代码
public void startWechatPay(WechatPayParams params) {
    IWXAPI api = WXAPIFactory.createWXAPI(context, params.getAppId());
    api.registerApp(params.getAppId());
    
    PayReq request = new PayReq();
    request.appId = params.getAppId();
    request.partnerId = params.getPartnerId();
    request.prepayId = params.getPrepayId();
    request.nonceStr = params.getNonceStr();
    request.timeStamp = params.getTimeStamp();
    request.packageValue = "Sign=WXPay";
    request.sign = params.getSign();  // 服务端生成的签名
    
    api.sendReq(request);
}
🔑 关键点: 微信支付的sign是服务端用MD5或HMAC-SHA256算出来的。客户端只负责调起,不参与任何签名计算。这样即使客户端被逆向,也无法伪造支付请求。

银联支付集成

银联支付在国内用得相对少一些,但在某些行业(比如金融、政务)是刚需。银联的集成方式比较传统,用的是「控件模式」。

银联的流程是这样的:

  1. 服务端调用银联接口获取tn(交易流水号)
  2. 客户端拿到tn后调起银联支付控件
  3. 用户在控件内完成支付
  4. 银联回调结果给客户端
// 银联支付 - 客户端代码
public void startUnionPay(String tn, String mode) {
    // mode: "00" 正式环境, "01" 测试环境
    UPPayAssistEx.startPayByJAR(context, 
        new PayActivity(), 
        null, 
        null, 
        tn, 
        mode);
}

// 在Activity中处理结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (data == null) return;
    
    String msg = "";
    String str = data.getExtras().getString("pay_result");
    if (str.equalsIgnoreCase("success")) {
        msg = "支付成功";
    } else if (str.equalsIgnoreCase("fail")) {
        msg = "支付失败";
    } else if (str.equalsIgnoreCase("cancel")) {
        msg = "用户取消";
    }
}
⚠️ 注意: 银联的测试环境和正式环境tn是不同的。测试环境用「01」,正式环境用「00」。我曾经在测试时忘了切环境,结果测试tn在正式环境上报错,排查了半天……

支付结果处理:同步 vs 异步

支付结果的处理,是很多新手容易搞混的地方。我简单说一下:

方式 说明 可靠性
客户端同步回调 支付完成后SDK直接返回结果 低(可被伪造)
服务端异步通知 支付平台主动通知服务端 高(带签名验证)

我的建议是:永远以服务端异步通知为准。 客户端回调只能用来刷新UI,不能作为订单完成的依据。为什么?因为客户端回调可以被Hook、被篡改。你想想看,如果有人写了个Xposed模块,把支付失败改成支付成功,你的App不就白给了吗?

避坑指南

这些年我踩过的坑,整理一下分享给你:

  • 签名算法不一致:支付宝用RSA2,微信用MD5/HMAC-SHA256,银联用SHA1withRSA。别搞混了。
  • 金额单位:支付宝和微信都是「元」,银联是「分」。我曾经因为单位问题,让用户付了100倍的金额……还好及时发现。
  • 订单号唯一性:每个订单号只能支付一次。如果重复使用,支付平台会直接返回失败。
  • 网络超时:支付过程中如果网络断了,客户端可能收不到回调。这时候需要主动去服务端查询订单状态。
💡 我的习惯: 在支付页面加一个「查询订单」按钮,用户如果觉得支付成功了但没跳转,可以手动查一下。这个小功能能减少很多客诉。

总结

支付集成说白了就是三件事:服务端签名、客户端调起、结果验证。签名必须在服务端做,结果必须以服务端异步通知为准。这三家支付虽然接口不同,但核心逻辑是一样的。

嗯,最后说一句:支付相关的代码,一定要做充分的异常处理。用户的钱不是小事,咱们做开发的,得对得起这份信任。


公众号:蓝海资料掘金营,微信deep3321