支付宝的接入,权当笔记。
PC网页端的支付,官方文档在:开发文档 / 电脑网站支付 / 快速接入。这篇应该说是完整的tutorial,里面也包含了支付的整个流程。
官方配图:
总觉得支付宝的这个流程图有点问题,并没有很好解释流程顺序。这里整理一个文字版。
流程涉及到几个角色:
支付流程如下:
用户
打开PC网站网页的商品页用户
对商品进行下单PC网站网页
发送下单请求到PC网站后台PC网站后台
在自身系统内生成订单(生成内部订单号),并返回给网页PC网站网页
获得内部订单信息,并将用户导向到支付页面用户
确认订单内容,并开始付款流程PC网站网页
发起支付请求到PC网站后台PC网站后台
向支付宝系统发起下单请求
PC网站后台
将支付宝返回的信息交付给PC网站网页PC网站网页
收到支付宝返回信息,使用其中的信息将页面导向到支付宝的付款页面用户
在支付宝的付款页面完成付款支付宝系统
确认支付完成,将用户网页重定向到return_url的页面支付宝系统
确认支付完成,向notify_url发送回调信息PC网站后台
验证支付宝回调完成,标记订单状态为已支付移动端原生支付和PC网页支付差异不是很大,官方文档在:开发文档 / App支付 / 快速接入。
官方配图:
这个图还算可以,老样子整理一个文字版。
流程涉及到几个角色:
支付流程如下:
用户
打开手机APP的商品页用户
对商品进行下单手机APP
发送下单请求到APP后台APP后台
在自身系统内生成订单(生成内部订单号),并返回给APP手机APP
获得内部订单信息,并将用户导向到支付页面用户
确认订单内容,并开始付款流程手机APP
发起支付请求到APP后台APP后台
向支付宝系统发起下单请求,获得支付宝APP唤醒参数,并交付给手机APP
手机APP
收到支付宝返回信息,使用其中的参数唤醒支付宝APP用户
在支付宝APP完成付款支付宝系统
确认支付完成,返回手机APP支付宝系统
确认支付完成,向notify_url发送回调信息APP后台
验证支付宝回调完成,标记订单状态为已支付在正式接入支付宝之前,还需要做一些准备工作,主要是一系列账号、应用、秘钥的申请。
公司账号申请都是老板做的事情,虽然繁琐但和技术人员没什么关系。创建完成后可以通过账户成员管理将员工账号加入到管理群组里,方便员工直接进行开发。
员工可以通过这个链接,登入到支付宝进行账号和应用的管理。
见官方文档:开发文档 / 开发指南 / 创建应用。
刚创建出来的应用是没有任何功能的,支付宝的各种功能(包括最基础的支付功能)都是支付应用下的应用功能
,一般来说每个都需要单独开通,某些开通还需要一定的条件,比如说网页支付开通的话你必须首先要有一个已经上线的网页而且要有明确的支付功能(申请失败如下)。等等。见:签约功能。
在这一步最重要的是要把支付应用的APP_ID记录下来。
见官方文档:开发文档 / 签名专区 / 第一步:生成 RSA 密钥。
官方有提供一个工具支付宝开放平台开发助手
,可以用这个工具进行秘钥制作,比较方便:
这一步操作完成后应该会获得三个文件,需要妥善保存:
-----BEGIN PUBLIC KEY-----
开头-----END PUBLIC KEY-----
结尾-----BEGIN PUBLIC KEY-----
开头-----END PUBLIC KEY-----
结尾-----BEGIN RSA PRIVATE KEY-----
开头-----END RSA PRIVATE KEY-----
结尾下载下来的公钥和私钥都是不断行的连续字符串,还需要借助像是Online tool to format private key. - SAMLTool.com这样的工具进行格式转换,断行。如果没有文件头和文件尾的话,还需要手动添加。
Node.js进行开发的话,有两个npm库可选:
官方SDK的使用需要了解:
此外,官方SDK有一个比较严重的BUG:设置了 passbackParams 会导致 alipaySdk.checkNotifySign 失败 #45,会直接影响使用,需要注意。
解决方案:
payload.passback_params = encodeURIComponent(payload.passback_params)
const verified = alipaySdk.checkNotifySign(payload)
官方的API文档在:支付API。
支付宝的API的接口,参数都分为公共请求参数
以及请求参数
。简单点来说:
请求参数会放在公共请求参数的biz_content
里,至于怎么放进去的可以不用关心,SDK都会直接封装掉。
API的测试可以通过:在线调试(Beta)来模拟使用。
官方SDK的初始化,沙箱和正式用的是两套秘钥,这块需要动态处理下:
import AlipaySdk from "alipay-sdk/lib/alipay";
import AlipayFormData from "alipay-sdk/lib/form";
// ...
const vendorConfig = Config.get().getVendor(Constant.VENDOR_ALIPAY_ID) as IVendorConfigAlipay;
const isSandbox = Config.get().getRaw().sandbox;
this.notifyUrl = vendorConfig.callbackUrl;
this.returnUrl = vendorConfig.returnUrl;
const pubKeyPath = LibPath.join(
__dirname, "../../../pem/alipay", isSandbox ? "sandbox/alipay.pub.txt" : "alipay.pub.txt",
);
const priKeyPath = LibPath.join(
__dirname, "../../../pem/alipay", isSandbox ? "sandbox/app.pri.txt" : "app.pri.txt",
);
this.gateway = isSandbox
? "https://openapi.alipaydev.com/gateway.do"
: "https://openapi.alipay.com/gateway.do";
console.log("alipay::sdk::pubKeyPath", pubKeyPath);
console.log("alipay::sdk::priKeyPath", priKeyPath);
console.log("alipay::sdk::gateway", this.gateway);
this.aliPay = new AlipaySdk({
appId: vendorConfig.appId,
alipayPublicKey: LibFs.readFileSync(pubKeyPath).toString(),
privateKey: LibFs.readFileSync(priKeyPath).toString(),
gateway: this.gateway,
signType: "RSA2",
});
范例代码:
const formData = new AlipayFormData();
formData.addField("returnUrl", this.returnUrl);
formData.addField("notifyUrl", this.notifyUrl);
formData.addField("bizContent", {
outTradeNo: orderId.toString(),
productCode: "FAST_INSTANT_TRADE_PAY",
totalAmount: price,
subject: name,
body: desc,
passbackParams: encodeURIComponent(JSON.stringify({}})),
timeoutExpress: `${Math.floor(seconds / 60)}m`,
qrPayMode: 0,
});
return await this.aliPay.exec(
"alipay.trade.page.pay",
{},
{formData, log: {info: console.log, error: console.log}},
);
范例返回:
<form action="https://openapi.alipaydev.com/gateway.do?method=alipay.trade.page.pay&app_id=...&charset=utf-8&version=1.0&sign_type=RSA2×tamp=2019-XX-XX%2015%3A20%3A24&return_url=...¬ify_url=...&sign=..." method="post" name="alipaySDKSubmit1577431224474" id="alipaySDKSubmit1577431224474">
<input type="hidden" name="alipay_sdk" value="alipay-sdk-nodejs-3.0.8" /><input type="hidden" name="biz_content" value="{"out_trade_no":143123,"product_code":"FAST_INSTANT_TRADE_PAY...}" />
</form>
<script>document.forms["alipaySDKSubmit1577431224474"].submit();</script>
返回的是一个字符串,这串字符串交付给网页,让JS直接append到当前页面上,就会进行跳转(form.submit),到支付宝的页面让用户付款。
文档在:alipay.trade.app.pay。
范例代码:
const formData = new AlipayFormData();
formData.setMethod("get");
formData.addField("notifyUrl", this.notifyUrl);
formData.addField("bizContent", {
outTradeNo: orderId.toString(),
productCode: "QUICK_MSECURITY_PAY",
totalAmount: price.toString(),
subject: name,
body: desc,
passbackParams: encodeURIComponent(JSON.stringify({}})),
timeoutExpress: `${Math.floor(seconds / 60)}m`,
});
return await this.aliPay.exec(
"alipay.trade.app.pay",
{},
{formData, log: {info: console.log, error: console.log}},
) as string;
范例返回:
https://openapi.alipaydev.com/gateway.do?method=alipay.trade.app.pay&app_id=...&charset=utf-8&version=1.0&sign_type=RSA2×tamp=2019-XX-XX%2015%3A23%3A25¬ify_url=...
这一串字符串需要去掉头部的gateway:https://openapi.alipaydev.com/gateway.do?
,只需要把剩下的参数部分交付给移动APP即可,移动APP会使用这些参数唤醒支付宝APP。
文档在:alipay.trade.query。
范例代码:
return await this.aliPay.exec("alipay.trade.query", {
bizContent: {out_trade_no: orderId.toString()},
}, {log: {info: console.log, error: console.log}}) as any;
返回的结果详见官方API文档。
在return_url上收到的参数如下:
{
"charset": "utf-8",
"out_trade_no": "143123",
"method": "alipay.trade.page.pay.return",
"total_amount": "0.01",
"sign": "B5wBu/icxG9u12XyBKUu...",
"auth_app_id": "201...422",
"version": "1.0",
"app_id": "201...422",
"sign_type": "RSA2",
"seller_id": "208...342",
"timestamp": "2019-XX-XX 15:48:17"
}
在notify_url上收到的参数如下:
{
"gmt_create": "2019-XX-XX 15:48:01",
"charset": "utf-8",
"subject": "测试商品名",
"sign": "OMcW3K9Sy...",
"buyer_id": "208...342",
"body": "测试商品描述",
"invoice_amount": "0.01",
"notify_id": "2019...589",
"fund_bill_list": "[{\"amount\":\"0.01\",\"fundChannel\":\"ALIPAYACCOUNT\"}]",
"notify_type": "trade_status_sync",
"trade_status": "TRADE_SUCCESS",
"receipt_amount": "0.01",
"app_id": "201...422",
"buyer_pay_amount": "0.01",
"sign_type": "RSA2",
"seller_id": "208...342",
"gmt_payment": "2019-XX-XX 15:48:15",
"notify_time": "2019-XX-XX 15:48:16",
"passback_params": "%7B%22orderId%22%3A143123%2C%22salt%22%3A%221fvwtia2k4nuszx2%22%7D",
"version": "1.0",
"out_trade_no": "143123",
"total_amount": "0.01",
"trade_no": "2019...383",
"auth_app_id": "201...422",
"point_amount": "0.00"
}
官方文档在:开发文档 / 开发指南 / 使用沙箱环境。移动APP的沙箱使用还需要一些额外的调整:开发文档 / App支付 / 沙箱联调指南。
沙箱不需要额外申请,在支付应用创建完成之后,其沙箱自动创建完成。
沙箱有单独的管理页面,上面主要要处理几个事情:
沙箱环境有单独的一个调试用支付宝APP(用正常的支付宝APP扫码会失败,也无法被沙箱应用唤醒),只有android版本:下载链接。
在沙箱管理页面上能获取到对应的账号信息等。使用这些信息登入支付宝沙箱APP,然后就可以进行测试支付了。
无论对错,业务服务器后端都需要以text/plain
的格式响应,如果回调处理没有问题,就返回success
,如果报错,则返回fail
。
try {
// 在这里处理业务
ctx.type = "text/plain";
ctx.body = "success";
} catch (err) {
console.log(err);
ctx.type = "text/plain";
ctx.body = "fail";
}
在某些情况下,回调可能没有收到,或者一直报错导致回调没有正常处理。表现在用户视角,就是支付完成后订单状态无法正常改为支付完成(也就不会发货)。这是很糟糕的,因此需要制作修复订单功能。
客户端在检查到订单状态为等待支付之后,可以提供一个入口给用户,用户点击之后,应用后端应该向支付宝查询订单信息,如果在支付宝这里已经是已支付
而在应用这边还是等待支付
的话,就立即将业务订单改为已支付,并向用户发货。
开发完成(甚至在这之前)就可以上线应用,之前在申请应用的时候提到过的功能开通,签约等,很大一部分都需要应用上线之后才可以做,所以这个操作可以尽早。最基础的支付功能基本上签约没什么难度,早点签约早点开通。
EOF