React Native 安卓端接入支付宝支付(附带简单的NodeJS后端处理)
前置准备
沙箱环境
账号:开发阶段建议使用沙箱环境: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
下面针对 Expo 项目进行特殊说明
如果你使用 Expo GO 对应用进行调试的话,可能不会在源代码根目录生成 android
文件夹,也无法调试支付宝的 SDK,Expo 云端不会涵盖本地的 Native 依赖,因此我们需要使用本地调试,此部分可以参考 Expo 官方文档:https://docs.expo.dev/guides/local-app-development/
如果你是使用 Expo 官方脚手架生成的项目,package.json
里面会有如下的命令配置:
此时我们可以运行 npm run android
或 yarn 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 提供了三个方法:setAlipayScheme
、setAlipaySandbox
、alipay
,其中 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。