前置准备

沙箱环境

账号:开发阶段建议使用沙箱环境:https://open.alipay.com/develop/sandbox/app

App:同时,支付宝客户端也要相应地使用沙箱版,与正常的支付宝是不冲突的。进入沙箱控制台,点击左边导航栏的“沙箱工具”项,点击“支付宝客户端沙箱版”后扫码下载安装。

证书(用于后端):在“沙箱应用”的“接口加签方式”可以查看密钥,推荐使用证书模式,下面也将按照证书模式进行演示。证书模式和公钥模式不能同时开启。开启后,点击“查看”下载对应的crt证书(如图,以非JAVA语言后端为例,一共下载三个证书,除此之外,应用私钥复制并单独保存为一个pem文件)。

一共四个文件如上图所示。其中private-key.pem 为复制的应用私钥字符串保存的文件。

Native 集成

目前支付宝的 SDK 已发布到 Maven Central,我们可使用 gradle 编译、更新支付宝支付 SDK。

下面的方法参考了网上的几篇文章:

① https://www.jianshu.com/p/af68d31ed303

② https://juejin.cn/post/7065635994195722276

③ https://juejin.cn/post/6844903519267340296

下面针对 Expo 项目进行特殊说明

如果你使用 Expo GO 对应用进行调试的话,可能不会在源代码根目录生成 android 文件夹,也无法调试支付宝的 SDK,Expo 云端不会涵盖本地的 Native 依赖,因此我们需要使用本地调试,此部分可以参考 Expo 官方文档:https://docs.expo.dev/guides/local-app-development/

如果你是使用 Expo 官方脚手架生成的项目,package.json 里面会有如下的命令配置:

此时我们可以运行 npm run androidyarn run android,如果没有,可以通过 npx expo run:android 执行或者自行添加。

首次编译时会下载相关依赖等,可能会因为网络问题导致下载失败。除此之外,JDK版本、ANDROID_HOME 环境配置都有可能导致编译失败。

当我们编译我完成后,会看到项目根目录会有 android 文件夹。

Expo 项目特殊说明结束

添加依赖

修改 android/app/build.gradle,在 dependencies 中添加如下代码:

// AliPay
api 'com.alipay.sdk:alipaysdk-android:+@aar'

如果我们要对原生模块后续进行混淆等操作,最好修改同目录下的 proguard-rules.pro 文件,添加如下内容,防止被修改出错:

# AliPay
-keep class com.alipay.android.app.IAlixPay{*;}
-keep class com.alipay.android.app.IAlixPay$Stub{*;}
-keep class com.alipay.android.app.IRemoteServiceCallback{*;}
-keep class com.alipay.android.app.IRemoteServiceCallback$Stub{*;}
-keep class com.alipay.sdk.app.PayTask{ public *;}
-keep class com.alipay.sdk.app.AuthTask{ public *;}
-keep class com.alipay.sdk.app.H5PayCallback {
    <fields>;
    <methods>;
}
-keep class com.alipay.android.phone.mrpc.core.** { *; }
-keep class com.alipay.apmobilesecuritysdk.** { *; }
-keep class com.alipay.mobile.framework.service.annotation.** { *; }
-keep class com.alipay.mobilesecuritysdk.face.** { *; }
-keep class com.alipay.tscenter.biz.rpc.** { *; }
-keep class org.json.alipay.** { *; }
-keep class com.alipay.tscenter.** { *; }
-keep class com.ta.utdid2.** { *;}
-keep class com.ut.device.** { *;}

添加原生代码暴露给 React Native

下面将以 com.example.demo 作为我们开发的应用包名进行演示:

对于不同的包名,在本文章中的影响为目录的结构,具体体现在 android/app/src/main/java/ 目录下的文件夹结构,可以理解为对包名以点进行分隔,例如我们的主程序代码在 android/app/src/main/java/com/example/demo/ (以下简称 demo 目录)

demo 文件夹默认有两个文件:

这两个文件是我们主程序文件。

我们在 demo 文件夹下新建 alipay 文件夹存放我们的原生代码。

创建模块类存放暴露给 React-Native 的方法,用于调用支付宝支付

demo/alipay 目录下新建 AliPayModule.java 文件,代码内容如下(对部分内容添加了注释,如果有修改的需求可以进行参考,此部分):

package com.example.demo.alipay;  // 包名为 应用包名.当前目录名

// 导入库
import com.alipay.sdk.app.PayTask;
import com.alipay.sdk.app.EnvUtils;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;

import java.util.Map;

// 此类继承自 ReactContextBaseJavaModule,用于创建模块类后续暴露给 React-Native 调用
public class AliPayModule extends ReactContextBaseJavaModule {

    private final ReactApplicationContext reactContext;

    public AliPayModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    // getName 方法用于 React-Native 导入时使用的名称
    // 例如我们在 React-Native 中导入原生模块:
    // import { NativeModules } from 'react-native';
    // 那么当我们想使用此类的方法时:
    // const { AliPay } = NativeModules;
    // 其中 AliPay 即为 getName 的返回值
    @Override
    public String getName() {
        return "AliPay";
    }

    @ReactMethod
    public void setAlipayScheme(String scheme) {}

    // 此方法用于设置是否为 沙箱环境
    // 带有 @ReactMethod 注解,即为 React-Native 可调用的方法
    @ReactMethod
    public void setAlipaySandbox(Boolean isSandbox) {
        if (isSandbox) {
            EnvUtils.setEnv(EnvUtils.EnvEnum.SANDBOX);
        } else {
            EnvUtils.setEnv(EnvUtils.EnvEnum.ONLINE);
        }
    }

    // 此方法用于吊起支付宝支付,内部使用的是异步调用,因此只能通过 callback 的方式进行处理,我们可以在 React-Native 代码中进行一次封装。
    // 第一个参数 orderInfo 为后端返回的订单信息
    // 第二个参数 promise 为回调函数,用于处理支付结果
    @ReactMethod
    public void alipay(final String orderInfo, final Callback promise) {
        Runnable payRunnable = new Runnable() {
            @Override
            public void run() {
                PayTask alipay = new PayTask(getCurrentActivity());
                Map<String, String> result = alipay.payV2(orderInfo, true);
                WritableMap map = Arguments.createMap();
                map.putString("memo", result.get("memo"));
                map.putString("result", result.get("result"));
                map.putString("resultStatus", result.get("resultStatus"));
                promise.invoke(map);
            }
        };
        // 异步调用
        Thread payThread = new Thread(payRunnable);
        payThread.start();
    }
}

上面文件给 React-Native 提供了三个方法:setAlipaySchemesetAlipaySandboxalipay,其中 setAlipayScheme 没有具体实现,代码参考链接 ①。

创建 React-Native 包类,在包中创建 Native 模块,实例化模块类并添加到 Native 模块

接着,在同级目录( demo/alipay 目录)下新建 AliPayPackage.java 文件,代码内容如下(对部分内容添加了注释):

package com.example.demo.alipay; // 包名为 应用包名.当前目录名

// 导入库
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// 此类继承自 ReactPackage,主要用于 React-Native 包的实现
public class AliPayPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        // 下面一行中 AliPayModule 为 AliPayModule.java 的类名,对其进行实例化,并添加到包中
        modules.add(new AliPayModule(reactContext));
        return modules;
    }

    // 早期版本的RN如果有报错取消这个注释即可
    // @override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

在主程序代码中添加包

上面两个文件添加完成后,在 demo/MainApplication.java 主程序代码中添加包(Package):

首先导入我们写的 alipay 包中的 AliPayPackage 类,此类用于创建 RN 包:

import com.example.demo.alipay.AliPayPackage;

getPackages() 方法中添加下面一行代码:

packages.add(new AliPayPackage());

若没有添加过其他的包,此时getPackages() 方法完整代码如下:

// 省略代码:
// 在导入库部分导入AliPayPackage:import com.example.demo.alipay.AliPayPackage;

@Override
protected List<ReactPackage> getPackages() {
    @SuppressWarnings("UnnecessaryLocalVariable")
    List<ReactPackage> packages = new PackageList(this).getPackages();
    // Packages that cannot be autolinked yet can be added manually here, for example:
    // packages.add(new MyReactNativePackage());
    // 上面的注释意思为:包不会自动 link,我们需要自己添加,第二行是示例代码。
    // 因此我们根据实例代码添加我们的包   实例化 AliPayPackage 类并添加到 packages
    packages.add(new AliPayPackage());
    return packages;
}

至此,Native 部分结束,我们需要重新运行 npm run android 进行编译构建才能生效。

React-Native 调用

我们可以将支付方法封装起来:

import { NativeModules } from 'react-native';

// 在上面的代码注释中介绍了 AliPay 这个名字的由来
const { AliPay } = NativeModules;


interface IAliPayResponse {
  memo: string;  // 错误原因
  result: string; // 结果:是字符串化的 JSON
  resultStatus: string; // 返回码
}

interface IAliPayErrorReason {
  [key: string]: string;
}


// 配置沙箱环境,如果非沙箱环境可以删掉这行代码
AliPay.setAlipaySandbox(true);

// 错误码对应的错误原因(返回结果中 memo 可能会有错误原因,没有尝试,例如:{"memo": "支付未完成。", "result": "", "resultStatus": "6001"})
const alipayErrorReason: IAliPayErrorReason = {
  '6001': '支付取消',
  '6002': '网络连接出错',
  '4000': '支付失败',
  '5000': '重复请求',
};
// 封装支付方法(参数为后端返回的字符串)
export function aliPay(orderInfo: string) {
  return new Promise((resolve, reject) => {
    AliPay.alipay(orderInfo, (res: IAliPayResponse) => {
      const { resultStatus } = res;
      if (resultStatus === '9000') {
        // 支付成功处理
        resolve(true);
      } else {
        // 支付失败处理,可以将 res?.memo 放在首选
        const reason =
          alipayErrorReason?.[resultStatus] || res?.memo || '未知错误';
        console.log('支付失败', reason);
        reject(reason);
      }
    });
  });
}

然后在需要的地方调用 aliPay 方法即可,当我们启用了沙箱环境,会自动调用沙箱版的支付宝APP,所以请确保提前安装支付宝沙箱版并登录对应的沙箱账号,请记住不应该登录商户的沙箱账号,应该登录支付者的沙箱账号(支付宝沙箱后台会提供两个沙箱账号)。

例如:

import { aliPay } from '@api/pay';
// 假设创建支付订单
axios.post("xxxx",{})
.then(async(res: IData) => {
  // 收到后端返回的数据,调用 aliPay 进行支付
  try {
    aliPay(res.data.payload);
  } catch(err: string) {
    // 当支付失败,失败信息
    // 这部分的处理在封装支付方法的else分支中,返回的是一个处理后的字符串
    // 也就是说,用户支付成功后前端能够及时的获取结果,虽然是不可信的,但是后面的验证交给后端进行处理,不能阻塞正常业务
    console.log(err);
  }
})

后端获取订单信息供 RN 调用

这里以 NodeJS + Koa 为例。

首先安装 Koa 以及 Koa-Router 来启动一个网页服务。

npm install koa koa-router

接着安装支付宝的 SDK

npm install alipay-sdk

将“前置准备”部分的四个文件放到合适的位置,后面使用 fs 模块进行文件内容的读取。

这里演示的文件结构如下:

其中红色框起来的为四个密钥文件。

index.js 的代码如下:

// 导入相关库
const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const Router = require('koa-router');
const AlipaySdk = require('alipay-sdk');
// 新建 koa 应用
const app = new Koa();
// 新建 koa 路由
const router = new Router();




// 证书模式新建 SDK 实例
const alipaySdk = new AlipaySdk({
  appId: '123456789123456',  // 你的应用 AppID

  // 应用私钥,即复制的字符串单独保存成为文件,注意这里使用 fs 读取保存的文件,如果你没有将复制的字符串保存成文件,也可以直接将字符串内容作为值
  privateKey: fs.readFileSync('private-key.pem', 'ascii'),
  // 传入支付宝根证书、支付宝公钥证书和应用公钥证书的路径。
  alipayRootCertPath: path.join(__dirname, 'alipayRootCert.crt'),
  alipayPublicCertPath: path.join(__dirname, 'alipayPublicCert.crt'),
  appCertPath: path.join(__dirname, 'appPublicCert.crt'),

  // 这个是配置沙箱支付宝网管地址
  // !!!!注意,如果你不是沙箱环境,删掉下面的一行代码
  gateway: 'https://openapi-sandbox.dl.alipaydev.com/gateway.do',
});





// 监听创建订单请求
router.get('/', async (ctx, next) => {
  // 这里我就写死请求参数,具体参数根据实际开发需求修改
  let result;
  try {
    // 正常请求,执行 alipay.trade.app.pay
    result = await alipaySdk.sdkExec('alipay.trade.app.pay', {
      // !!!! notify_url 通知回调地址,其作用请看文末图 1.13
      notify_url: 'http://www.bietiaop.com/notify',
      // 业务数据
      bizContent: {
        // 下面三个参数是必须的,其他业务参数可以看支付宝官方文档:
        // https://opendocs.alipay.com/open/02e7gq?scene=20
        out_trade_no: '70501111111S001111116', // 订单号,用于识别订单,不能重复
        total_amount: '0.1', // 金额
        subject: '测试订单', // 支付商品名称
      },
    });
  } catch (error) {
    // 出错
    result = error;
  }
  // 将结果写入到页面
  ctx.body = result;
});





// 这里可以写 notify_url 通知回调地址的路由,接收支付宝返回的支付结果信息,这里不做演示,可以自己尝试





// 注册路由
app.use(router.routes());

// HTTP 监听 3000 端口
const port = 3000;
app.listen(port).on('listening', () => {
  console.log(`Koa server listening on http://localhost:${port}`);
});

由于上面的代码直接将请求结果写入到页面,我们可以直接访问我们的网站,得到类似下面的内容:

这个字符串就是我们上面 React-Native 调用 aliPay 的参数。

当然,后端示例代码仅仅写了创建订单的简单 demo,实际处理就是 React-Native 请求后端创建订单,得到字符串,调用 aliPay 方法进行支付。

紧接着,前端能及时判断支付是否成功,前端此时不需要再次请求后端验证,为了用户更好的体验,直接在前端输出支付结果(详细可以返回看我们封装的 aliPay 方法),这也是支付宝建议我们的支付流程。

至于是否真正的支付成功,支付宝会通过我们配置的 notify_url 通知回调地址告知我们,如文末图中 1.13 流程。我们的回调地址会接收支付宝的请求,支付宝会告知我们是否支付成功,如果没有及时收到支付结果,可以通过 alipay.trade.query 手动查询支付信息。

至于 iOS 端,原理类似,因为我没有苹果设备,没法进行写代码以及调试,但仍可以参考我参考的三个博客进行尝试。

当然,我也尝试了微信支付,已经写好了 React-Native,但是因没有商户账号,暂时还不能进行测试。

如果有什么疑问或者写错的地方,欢迎评论区留言。

最后附上一张阿里云官方的接入流程图:

以下对重点步骤做简要说明:

  • 第 1 步用户在商户 App 客户端/小程序中购买商品下单。

  • 第 2 步商户订单信息由商户 App 客户端/小程序发送到服务端。

  • 第 3 步商家服务端调用 alipay.trade.app.pay(app支付接口2.0)通过支付宝服务端 SDK 获取 orderStr(orderStr 中包含了订单信息和签名)。

  • 第 4 步商家将 orderStr 发送给商户 App 客户端/小程序。

  • 第 5 步商家在客户端/小程序发起请求,将 orderStr 发送给支付宝。

  • 第 6 步进行支付预下单:支付宝客户端将会按照商家客户端提供的请求参数进行支付预下单。正常场景下,会唤起支付宝收银台等待用户核身;异常场景下,会返回异常信息。

  • 第 11 步返回商家 App/小程序:用户在支付宝 App 完成支付后,会跳转回商家页面,并返回最终的支付结果(即同步通知),可查看 同步通知参数说明

  • 第 13 步支付结果异步通知,支付宝会根据步骤3 传入的异步通知地址 notify_url,发送异步通知,可查看 异步通知参数说明

除了正向支付流程外,支付宝也提供交易查询、关闭、退款、退款查询以及对账等配套 API。