All Articles

支付宝接入

1. 前言

支付宝的接入,权当笔记。

2. 支付流程

2.1 网页端

PC网页端的支付,官方文档在:开发文档 / 电脑网站支付 / 快速接入。这篇应该说是完整的tutorial,里面也包含了支付的整个流程。

官方配图:

总觉得支付宝的这个流程图有点问题,并没有很好解释流程顺序。这里整理一个文字版。

流程涉及到几个角色:

  • PC网站网页
  • PC网站后台
  • 支付宝系统

支付流程如下:

  • 用户打开PC网站网页的商品页
  • 用户对商品进行下单
  • PC网站网页发送下单请求到PC网站后台
  • PC网站后台在自身系统内生成订单(生成内部订单号),并返回给网页
  • PC网站网页获得内部订单信息,并将用户导向到支付页面
  • 用户确认订单内容,并开始付款流程
  • PC网站网页发起支付请求到PC网站后台
  • PC网站后台向支付宝系统发起下单请求
    • 其中附加了notify_url,会在支付完成后收到回调
    • 且附加了return_url,会在支付完成后将页面导向到该页面
  • PC网站后台将支付宝返回的信息交付给PC网站网页
  • PC网站网页收到支付宝返回信息,使用其中的信息将页面导向到支付宝的付款页面
  • 用户在支付宝的付款页面完成付款
  • 支付宝系统确认支付完成,将用户网页重定向到return_url的页面
  • 支付宝系统确认支付完成,向notify_url发送回调信息
  • PC网站后台验证支付宝回调完成,标记订单状态为已支付

2.2 移动原生

移动端原生支付和PC网页支付差异不是很大,官方文档在:开发文档 / App支付 / 快速接入

官方配图:

这个图还算可以,老样子整理一个文字版。

流程涉及到几个角色:

  • 手机APP
  • APP后台
  • 支付宝系统

支付流程如下:

  • 用户打开手机APP的商品页
  • 用户对商品进行下单
  • 手机APP发送下单请求到APP后台
  • APP后台在自身系统内生成订单(生成内部订单号),并返回给APP
  • 手机APP获得内部订单信息,并将用户导向到支付页面
  • 用户确认订单内容,并开始付款流程
  • 手机APP发起支付请求到APP后台
  • APP后台向支付宝系统发起下单请求,获得支付宝APP唤醒参数,并交付给手机APP
    • 其中附加了notify_url,会在支付完成后收到回调
  • 手机APP收到支付宝返回信息,使用其中的参数唤醒支付宝APP
  • 用户在支付宝APP完成付款
  • 支付宝系统确认支付完成,返回手机APP
  • 支付宝系统确认支付完成,向notify_url发送回调信息
  • APP后台验证支付宝回调完成,标记订单状态为已支付

3. 准备工作

在正式接入支付宝之前,还需要做一些准备工作,主要是一系列账号、应用、秘钥的申请。

3.1 账号申请

公司账号申请都是老板做的事情,虽然繁琐但和技术人员没什么关系。创建完成后可以通过账户成员管理将员工账号加入到管理群组里,方便员工直接进行开发。

员工可以通过这个链接,登入到支付宝进行账号和应用的管理。

3.2 应用申请

见官方文档:开发文档 / 开发指南 / 创建应用

刚创建出来的应用是没有任何功能的,支付宝的各种功能(包括最基础的支付功能)都是支付应用下的应用功能,一般来说每个都需要单独开通,某些开通还需要一定的条件,比如说网页支付开通的话你必须首先要有一个已经上线的网页而且要有明确的支付功能(申请失败如下)。等等。见:签约功能

在这一步最重要的是要把支付应用的APP_ID记录下来。

3.3 秘钥申请

见官方文档:开发文档 / 签名专区 / 第一步:生成 RSA 密钥

官方有提供一个工具支付宝开放平台开发助手,可以用这个工具进行秘钥制作,比较方便:

后续研发上遇到签名问题可以参考:自助排查以及常见问题

这一步操作完成后应该会获得三个文件,需要妥善保存:

  • alipay的公钥
    • 文件以-----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这样的工具进行格式转换,断行。如果没有文件头和文件尾的话,还需要手动添加。

4. API & SDK

4.1 SDK选择

Node.js进行开发的话,有两个npm库可选:

  • alipay-node-sdk:这个库非官方,但维护的很好,而且使用的人也不少,可以一用
  • Alipay SDK:这个库是官方的,但封装得还没有上一个好,而且文档也很糟糕,但毕竟是官方的,所以还是首选

官方SDK的使用需要了解:

此外,官方SDK有一个比较严重的BUG:设置了 passbackParams 会导致 alipaySdk.checkNotifySign 失败 #45,会直接影响使用,需要注意。

解决方案:

payload.passback_params = encodeURIComponent(payload.passback_params)
const verified = alipaySdk.checkNotifySign(payload)

4.2 API使用基础

官方的API文档在:支付API

支付宝的API的接口,参数都分为公共请求参数以及请求参数。简单点来说:

  • 公共请求参数:支付宝级别比较顶层的参数,比如说:app_id、sign等
  • 请求参数:非公共请求参数之外的其他一切参数

请求参数会放在公共请求参数的biz_content里,至于怎么放进去的可以不用关心,SDK都会直接封装掉。

API的测试可以通过:在线调试(Beta)来模拟使用。

4.3 常用接口

官方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",
});

4.3.1 PC网页支付下单

文档在:alipay.trade.page.pay

范例代码:

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&timestamp=2019-XX-XX%2015%3A20%3A24&return_url=...&notify_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="{&quot;out_trade_no&quot;:143123,&quot;product_code&quot;:&quot;FAST_INSTANT_TRADE_PAY...}" />
</form>
<script>document.forms["alipaySDKSubmit1577431224474"].submit();</script>

返回的是一个字符串,这串字符串交付给网页,让JS直接append到当前页面上,就会进行跳转(form.submit),到支付宝的页面让用户付款。

4.3.2 APP支付下单

文档在: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&timestamp=2019-XX-XX%2015%3A23%3A25&notify_url=...

这一串字符串需要去掉头部的gateway:https://openapi.alipaydev.com/gateway.do?,只需要把剩下的参数部分交付给移动APP即可,移动APP会使用这些参数唤醒支付宝APP。

4.3.3 支付宝订单查询

文档在: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文档。

4.4 回调处理

4.4.1 return_url

在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"
}

4.4.2 notify_url

在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"
}

5. 沙箱使用

官方文档在:开发文档 / 开发指南 / 使用沙箱环境。移动APP的沙箱使用还需要一些额外的调整:开发文档 / App支付 / 沙箱联调指南

5.1 沙箱申请

沙箱不需要额外申请,在支付应用创建完成之后,其沙箱自动创建完成。

5.2 沙箱管理

沙箱有单独的管理页面,上面主要要处理几个事情:

  • 获取沙箱的APP ID;沙箱应用是单独的应用,和正式应用是隔离的,在沙箱环境使用正式应用的APP ID会告知没有该应用,反之亦然
  • 制作沙箱单独的秘钥;该秘钥和正式环境是隔离的,需要单独制作
  • 获取沙箱支付宝APP的账号和密码以及支付密码;在测试的时候会使用到

5.3 沙箱支付

沙箱环境有单独的一个调试用支付宝APP(用正常的支付宝APP扫码会失败,也无法被沙箱应用唤醒),只有android版本:下载链接

在沙箱管理页面上能获取到对应的账号信息等。使用这些信息登入支付宝沙箱APP,然后就可以进行测试支付了。

6. 错误处理

6.1 回调处理

无论对错,业务服务器后端都需要以text/plain的格式响应,如果回调处理没有问题,就返回success,如果报错,则返回fail

try {
    // 在这里处理业务

    ctx.type = "text/plain";
    ctx.body = "success";
} catch (err) {
    console.log(err);
    ctx.type = "text/plain";
    ctx.body = "fail";
}

6.2 修复订单

在某些情况下,回调可能没有收到,或者一直报错导致回调没有正常处理。表现在用户视角,就是支付完成后订单状态无法正常改为支付完成(也就不会发货)。这是很糟糕的,因此需要制作修复订单功能。

客户端在检查到订单状态为等待支付之后,可以提供一个入口给用户,用户点击之后,应用后端应该向支付宝查询订单信息,如果在支付宝这里已经是已支付而在应用这边还是等待支付的话,就立即将业务订单改为已支付,并向用户发货。

7. 上线生效

开发完成(甚至在这之前)就可以上线应用,之前在申请应用的时候提到过的功能开通,签约等,很大一部分都需要应用上线之后才可以做,所以这个操作可以尽早。最基础的支付功能基本上签约没什么难度,早点签约早点开通。

Appendix

链接

EOF

Published 2019/12/27

Some tech & personal blog posts