支付宝开发工具介绍
工具介绍
- 电脑网站支付 - 支付宝文档中心 (alipay.com)
- 调用流程
- 沙箱环境接入指南 - 支付宝文档中心 (alipay.com)
- 沙箱环境就是测试环境,提供一个测试用的应用,包含 APPID、密钥、支付账号、支付工具等
- 按照指南接入即可
- 每周日中午12点至每周一中午12点沙箱环境进行维护,期间可能出现不可用
alipay-sdk
alipay-sdk 是支付宝 NodeJS 版 的 服务端 SDK (通用版),用于实现本例的支付功能。
- 网页&移动应用 - 开发工具包下载 - 概览 - 支付宝文档中心 (alipay.com)
- alipay-sdk 使用文档 · 语雀 (yuque.com)
- 模块源码中也包含开发者友好的使用说明注释
配置沙箱环境应用的密钥
沙箱环境默认有一个用于测试的应用,代码编写时使用它的 APPID。
首次使用该应用还是需要配置密钥,生成密钥参考:生成密钥并上传
本例使用 “Web 在线加密” 生成密钥,这是网页版工具不需要下载。
- 密钥长度(即签名算法)选择默认推荐的 RSA2
- 密钥格式选择
PKCS2(非JAVA适用)
生成密钥会生成应用私钥和应用公钥两个字符串,记得将它们保存到本地。
设置沙箱环境应用的密钥时:
- 密钥的加签方式是“公钥”
- 将刚才生成的应用公钥配置进去,保存设置后会生成支付宝公钥,记得将其保存到本地
保存到本地的目的:
由于实例化 SDK 客户端时需要指定应用的私钥信息,请务必注意不要将私钥信息配置在源码中(比如配置为常量或储存在配置文件的某个字段中等),因为私钥的保密等级往往比源码高得多,将会增加私钥泄露的风险。推荐将私钥信息储存在专用的私钥文件中,将私钥文件通过安全的流程分发到服务器的安全储存区域上,仅供自己的应用运行时读取。
保存到项目 /pem
目录下的文件:
- app_public_key.txt - 应用公钥
- app_private_key.txt - 应用私钥
- alipay_pubilic_key.txt - 支付宝公钥
使用 OpenSLL 工具生成的密钥是 pem 格式的,使用“Web 在线加密”工具生成的是字符串或 txt 文本格式的密钥,所以本例全部以 .txt
文件保存。
支付流程
本例支付流程没有指南中那么严谨:
- 系统在支付订单前并不向数据库保存订单数据
- 用户在商户订单页面点击支付按钮
- 客户端向服务器发送请求获取到PC端支付宝收银台地址后跳转
- 用户在收银台地址登录并完成支付后,进行两个操作:
- 同步跳转回商户网站的支付成功页面
- 异步向商户发送通知(请求商户提供的 api)
- 商户接收到异步通知,对请求进行验签,确认来源安全后创建订单保存到数据库
创建 SDK 实例并配置 api
配置 SDK 时部分字段需配置为沙箱环境指南中指定的值。
验签:指的是交易成功后反向发送请求时验证请求来源,防止伪造请求。
// controllers\alipay.js
const AlipaySdk = require('alipay-sdk').default
const fs = require('fs')
const path = require('path')
// APPID
const appId = '2021000118602014'
// alipay-sdk 配置选项
const alipaySdkConfig = {
// 应用id
appId,
// 应用私钥字符串
privateKey: fs.readFileSync(path.join(__dirname, '../pem/app_private_key.txt'), 'ascii'),
// 签名算法
signType: 'RSA2',
// 支付宝公钥(需要对返回值做验签时候必填)
alipayPublicKey: fs.readFileSync(path.join(__dirname, '../pem/alipay_public_key.txt'), 'ascii'),
// 网关地址
gateway: 'https://openapi.alipaydev.com/gateway.do',
// 网关超时时间
timeout: 5000,
// 是否将网关返回的下划线 key 转换为驼峰写法
camelcase: true
}
// 获取支付地址
const getPayUrl = async (req, res) => {
// 获取订单信息
// totalAmount 订单总金额
// subject 订单标题
// body 订单描述
// products 产品列表
// address 收货地址
// userId 用户id
const { totalAmount, subject, body, products, address, userId } = req.body
// 创建 SDK 实例
const alipaySdk = new AlipaySdk(alipaySdkConfig)
res.send('OK')
}
module.exports = {
getPayUrl
}
执行请求接口使用
接口介绍
/**
* 执行请求
* @param {string} method 调用的 api 名称
* @param {object} params [可选] api 请求参数,包含公共请求参数和业务参数
* @param {object} params.bizContent 业务请求参数
* @param {Boolean} option [可选] 选项
* @param {Boolean} option.validateSign [可选] 是否对返回值验签
* @param {object} option.formData [可选] 页面类接口请求参数
* @param {object} option.log [可选] 日志记录对象,对象需包含两个记录日志的方法{info, error}
* @return {Promise} 请求执行结果
*/
exec(method: string, params?: IRequestParams, option?: IRequestOption): Promise<AlipaySdkCommonResult | string>;
请求参数
关于请求参数 params,需要参考接口文档,例如本例使用的电脑网站支付接口:alipay.trade.page.pay(统一收单下单并支付页面接口),该接口用于返回 PC 端的支付宝收银台地址
传递请求参数的时候可以使用驼峰写法。
文档中的请求参数包含两个部分:
- 公共参数-公共请求参数
- 规定在
params
对象中传递的参数 - 默认会使用 SDK 配置对象中的参数,也可以重新指定
- 规定在
- 请求参数(
biz_content
)-业务参数- 规定在
params.biz_content
对象中传递的参数 - 只有当公共请求参数列表中包含
biz_content
时,才需要传递这个对象
- 规定在
页面类接口
页面类接口默认返回的数据为 html 代码片段(如 alipay.trade.page.pay 接口返回的是一个 Form 表单)。
这类接口不能直接使用 exec()
方法的第二个形参 params
传递请求参数,而需要通过 exec()
方法的第三个形参 option.formData
传递,否则会报错
alipay-sdk 提供了一个 AlipayForm
的类,从 alipay-sdk/lib/form
引入,它实例化的对象是包含请求参数等信息的对象,通过调用实例的方法,向对象中添加请求参数,这个对象就用来传递给 exec()
方法的 option.formData
。
实例可以通过 setMethod()
修改请求方式:
post
(默认值):会返回一个 Form 表单的 HTML 代码片段get
:会返回一个支付宝收银台页面地址
const AlipaySdk = require('alipay-sdk').default
const AlipayFormData = require('alipay-sdk/lib/form').default
// SDK 配置
const alipaySdkConfig = {...}
// 创建 SDK 实例
const alipaySdk = new AlipaySdk(alipaySdkConfig)
// 创建 formData 实例
const formData = new AlipayFormData()
// AliPayForm { fields: [], files: [], method: 'post'}
console.log(formData)
// 设置 method 默认 post
// post、get 的区别在于 post 会返回 form 表单,get 返回 url
formData.setMethod('get')
// 添加请求参数
formData.addField(<fieldName>, <fieldValue>)
// 添加上传文件
formData.addFile(<fielName>, <filename>, <filePath>)
// 执行请求
const result = await alipaySdk.exec(
'alipay.trade.page.pay',
// 页面类接口的请求参数在 formData 中定义
// params 中的参数就无效了
{},
{
// 通过 formData 设置请求参
formData
}
)
// result 为 API 文档中“响应参数”对应的结果
console.log(result)
获取支付地址接口设计
基本信息
Path: /alipay
Method: POST
请求参数
Body
参数名称 | 是否必传 | 类型 | 说明 |
---|---|---|---|
totalAmount | 是 | Number | 订单总金额,支付宝SDK要求的必传参数 |
subject | 是 | String | 订单标题,支付宝SDK要求的必传参数 |
body | 否 | String | 订单描述,支付宝SDK要求的可选参数 |
returnUrl | 是 | Number | 客户端回跳地址,支付宝SDK要求的可选参数 |
notifyUrl | 是 | String | 服务端通知api,支付宝SDK要求的可选参数 |
address | 否 | String | 收货地址 |
products | 是 | Array | 产品列表 [{product: <productId>, count: <购买数量>}] |
userId | 是 | String | 用户id |
返回数据
字段名称 | 说明 |
---|---|
url | 支付宝收银台地址 |
执行请求
编写代码
// controllers\alipay.js
const AlipaySdk = require('alipay-sdk').default
const AlipayFormData = require('alipay-sdk/lib/form').default
const fs = require('fs')
const path = require('path')
const orderid = require('order-id')(process.env.ORDER_SECRET)
// APPID
const appId = '2021000118602014'
// alipay-sdk 配置选项
const alipaySdkConfig = {
// 应用id
appId,
// 应用私钥字符串
privateKey: fs.readFileSync(path.join(__dirname, '../pem/app_private_key.txt'), 'ascii'),
// 签名算法
signType: 'RSA2',
// 支付宝公钥(需要对返回值做验签时候必填)
alipayPublicKey: fs.readFileSync(path.join(__dirname, '../pem/alipay_public_key.txt'), 'ascii'),
// 网关地址
gateway: 'https://openapi.alipaydev.com/gateway.do',
// 网关超时时间
timeout: 5000,
// 是否将网关返回的下划线 key 转换为驼峰写法
camelcase: true
}
// 获取支付地址
const getPayUrl = async (req, res) => {
// 获取订单信息
// totalAmount 订单总金额
// subject 订单标题
// body 订单描述
// products 产品列表
// address 收货地址
// userId 用户id
const { totalAmount, subject, body, products, address, userId } = req.body
// 创建 SDK 实例
const alipaySdk = new AlipaySdk(alipaySdkConfig)
// 创建 formData 实例
const formData = new AlipayFormData()
// 设置 method 为 `get`
// 获取支付宝收银台地址用于跳转
formData.setMethod('get')
// appId signType 等必选参数可以从 SDK 中获取 不需要重复添加
// formData.addField('appId', appId)
// 添加业务参数
formData.addField('bizContent', {
// [必选] 商户自己管理订单号: 64个字符内, 可包含数字, 字母, 下划线.
// order-id 模块基于时间戳生成唯一订单 ID
outTradeNo: orderid.generate(),
// [必选] 销售产品码,与支付宝签约的产品码名称
// 目前电脑支付场景下仅支持FAST_INSTANT_TRADE_PAY
productCode: 'FAST_INSTANT_TRADE_PAY',
// [必选] 订单总金额, 精确到小数点后两位
totalAmount,
// [必选] 订单标题
subject,
// [可选] 订单描述
// alipay.trade.page.pay 业务参数中没有 `body` 可参考其它支付接口(坑)
body,
// 公用回传参数,如果请求时传递了该参数,则返回给商户时会回传该参数
// 支付宝会在异步通知时将该参数原样返回
// 该参数会拼接到 URL 上返回,所以要进行 UrlEncode 编码并注意长度限制
passback_params: JSON.stringify({ products, address, userId })
})
// 执行请求
try {
const url = await alipaySdk.exec(
// 电脑网站支付的支付接口(统一收单下单并支付页面接口)
'alipay.trade.page.pay',
{},
{ formData }
)
res.json({ url })
} catch (e) {
res.status(400).json(e)
}
}
module.exports = {
getPayUrl
}
// routes\alipay.js
const express = require('express')
const { getPayUrl } = require('../controllers/alipay')
const router = express.Router()
// 获取支付地址
router.post('/alipay', getPayUrl)
module.exports = router
测试
可使用 vscode 扩展 Live Server 启动 web 服务默认 5500 端口。
在运行的目录下新建一个 index.html 页面,用于发送请求:
axios
.post('http://localhost/api/alipay', {
totalAmount: 10,
subject: 'subject 订单标题',
body: 'body 订单描述'
})
.then(({ data }) => {
// 跳转到收银台页面
window.open(data.url)
})
如果页面
支付存在钓鱼风险
的错误,无法正常显示,可以参考:支付宝存在防钓鱼风险!一般情况可能是需要清除 Cookies。
使用沙箱环境提供的买家账户进行支付,支付完成:
同步通知
通过添加 returnUrl
请求参数,可以指定支付完成后回跳的页面地址。
跳转操作是在客户端页面进行的,所以可以使用本地运行的 web 页面。
首先在 Live Server 运行的服务目录下新建一个 paysuccess.html 文件。
添加 returnUrl
参数:
// 获取订单信息
// returnUrl 支付成功同步回跳地址
const { returnUrl, totalAmount, subject, body, products, address, userId } = req.body
// 支付宝在用户支付成功后同步跳回的地址
formData.addField('returnUrl', returnUrl)
添加 axios 请求参数:
axios
.post('http://localhost/api/alipay', {
totalAmount: 10,
subject: 'subject 订单标题',
body: 'body 订单描述',
returnUrl: 'http://127.0.0.1:5500/paysuccess.html'
})
.then(({ data }) => {
// 跳转到收银台页面
window.open(data.url)
})
这样在支付完成后,会在上面的收银台页面停留大约5秒左右,首先跳转到下面这样的中转页面:
接着就会跳转到 returnUrl
指定的页面,并且携带一些参数,地址就像:
http://127.0.0.1:5500/paysuccess.html?charset=utf-8&out_trade_no=1360-553681-2474&method=alipay.trade.page.pay.return&total_amount=10&sign=XPyeNxkGx0KIsAcXFlHo7O8usOuPfkNaMc9NvTKqk9IVI6BLDe7zH%2FcmkwN7wQvizGgA0xeXpoy07RGK%2BjZFBiz39p60dAoWw0MfPnq06VYXnpyV8jqV0UQ8VAsyazOGqX289NRXJcB41jXXAYE%2BHVaG2nwnlJjpaNXXl0LWjAktItugSjFMcXdH5SUakIaJ57%2Fe5JLA4925hX0dgfS7ub7JltZXhQdhO%2FoT4ZTcMx%2B%2FZQrm0rB4Lka53UtdmZ1xtdNPzHhtiNfqO5NFWtcewmBzbyBzbsrRMXKim3YzZezHXZ4E%2BRDUG4NMpjoOt%2Fto7%2FTQZJfEycExHgQivIRsWA%3D%3D&trade_no=2021081222001424690501677676&auth_app_id=2021000118602014&version=1.0&app_id=2021000118602014&sign_type=RSA2&seller_id=2088621956272172×tamp=2021-08-12+21%3A50%3A17
有时候中转页面会像下图一样展示,并不会跳转到 returnUrl
指定的页面:
这是由于沙箱环境不稳定导致的意外情况,**支付宝技术支持(页面右边的咨询对话框)**通过订单号查询,表示参数传递的没问题,并建议使用正式环境测试。
如果不想切换到正式环境测试,可以等一阵再测。
检查参数是否正确传递,请参考 同步页面不跳转
异步通知
通过添加 notifyUrl
请求参数,可以指定支付完成后用于异步请求的 api 地址,这个 api 是商户用于接收支付成功通知后进行下一步处理。
支付宝通过 POST 请求的形式将支付结果作为参数(JSON格式)通知到商户系统。
异步是支付宝服务器发送请求,所以 notifyUrl
必须是公网可以访问的地址,可使用本机 IP 或将应用部署到服务器。
返回参数示例
{
out_trade_no: '商户订单 ID',
trade_no: '支付宝交易 ID'
subject: '订单标题',
body: '订单描述',
passback_params: '公用回传参数字符串',
total_amount: '0.01',
version: '1.0',
charset: 'utf-8',
app_id: '',
auth_app_id: '',
sign: '',
sign_type: 'RSA2',
buyer_id: '',
seller_id: '',
notify_id: '',
notify_type: 'trade_status_sync',
notify_time: '2021-08-19 16:31:32',
fund_bill_list: '[{"amount":"0.01","fundChannel":""}]',
trade_status: 'TRADE_SUCCESS',
invoice_amount: '0.01',
receipt_amount: '0.01',
buyer_pay_amount: '0.01',
point_amount: '0.00',
gmt_create: '2021-08-19 16:31:22',
gmt_payment: '2021-08-19 16:31:31',
}
设置 notifyUrl
添加 notifyUrl
参数:
// 获取订单信息
// notifyUrl 支付成功异步通知地址
const { returnUrl, notifyUrl, totalAmount, subject, body, products, address, userId } = req.body
// 支付宝在用户支付成功后异步请求的地址
formData.addField('notifyUrl', notifyUrl)
添加 axios 请求参数:
axios
.post('http://localhost/api/alipay', {
totalAmount: 10,
subject: 'subject 订单标题',
body: 'body 订单描述',
returnUrl: 'http://127.0.0.1:5500/paysuccess.html',
notifyUrl: 'http://ecormmerce-end.com/api/alipayNotifyUrl'
})
.then(({ data }) => {
// 跳转到收银台页面
window.open(data.url)
})
定义 api
api 的主要内容有两个:
- 验签,验证请求来源是否安全
- 修改数据库信息,本例就是在订单支付成功后将订单信息创建并保存在数据库
// controllers\alipay.js
...
// 支付成功回调
const alipayNotifyUrl = async (req, res) => {
// 1. 验签
// 创建 SDK 实例
const alipaySdk = new AlipaySdk(alipaySdkConfig)
try {
// result 为布尔值,表示是否验签通过
const result = await alipaySdk.checkNotifySign(req.body)
// 获取回传参数
const passback = JSON.parse(req.body.passback_params)
// 创建订单
console.log('创建订单')
} catch (e) {
console.error('验签异常:', e)
res.status(400).json(e)
}
}
module.exports = {
getPayUrl,
alipayNotifyUrl
}
// routes\alipay.js
const express = require('express')
const { getPayUrl, alipayNotifyUrl } = require('../controllers/alipay')
const router = express.Router()
// 获取支付地址
router.post('/alipay', getPayUrl)
// 支付成功异步通知
router.post('/alipayNotifyUrl', alipayNotifyUrl)
module.exports = router
PM2 启动应用
如果将应用部署到服务器,可以使用 pm2 启动应用,方便管理进程:PM2 - Quick Start (keymetrics.io)
注意要在服务器安装 MongoDB 和创建数据库。
配置 pm2 进程
在根目录创建 ecosystem.config.js 配置文件:
// ecosystem.config.js
module.exports = {
apps: [
{
// 启动应用的脚本文件
// 类似 `node app.js`
script: 'app.js',
// 进程名称
name: 'ecommerce—end',
// 启动命令参数
// 预加载 dotenv/config 模块
// 类似 `node app.js -r dotenv/config`
node_args: '-r dotenv/config',
// 默认使用的环境变量配置
env: {
NODE_ENV: 'development'
},
// 使用 `--env` 命令指定使用的环境变量配置
// `pm2 start ecosystem.config.js --env production`
env_production: {
NODE_ENV: 'production'
}
}
]
}
添加脚本:
// package.json
"scripts": {
"start": "cross-env NODE_ENV=development nodemon -r dotenv/config app.js",
"start:pm2": "pm2 start ecosystem.config.js --env production",
"stop:pm2": "pm2 stop ecosystem.config.js"
},
配置服务器数据库信息
在服务器上连接的数据库信息可能与本地不一样,通过配置文件管理数据库连接信息:
为 development 和 production 环境创建配置文件:
// config\development.json5
{
// 应用相关
app: {
// 服务启动监听端口
port: 80
},
// 本地数据库连接信息
dbConfig: {
// 连接地址
host: 'localhost',
// 数据库名称
name: 'ecommerce',
// 用户名(默认为空)
user: '',
// 密码(默认为空)
pass: '',
// 端口(默认27017)
port: '27017'
}
}
生产环境修改为自己服务器上的数据库信息:
// config\production.json5
{
// 应用相关
app: {
// 服务启动监听端口
port: 5000
},
// 本地数据库连接信息
dbConfig: {
// 连接地址
host: 'localhost',
// 数据库名称
name: 'ecommerce',
// 用户名(默认为空)
user: '',
// 密码(默认为空)
pass: '',
// 端口(默认27017)
port: '27017'
}
}
.json5
是 node-config 模块支持的配置文件格式,内部依赖了可以解析 JSON5 的模块,不需要额外配置。本例使用 JSON5 的目的是可以编写注释。
删除已经没用的 custom-environment-variables.json
文件,和 .env
中数据库信息环境变量:
# .env
# JWT 密钥
JWT_SECRET=test
JWT 密钥根据个人喜好可以为每个环境配置不同的值。
修改代码中使用 APP_PORT 端口的地方:
// app.js
+ const config = require('config')
- const APP_PORT = process.env.APP_PORT || 80
+ const { port: APP_PORT = 80 } = config.get('app')
服务器启动
本例服务器使用的 Ubuntu 操作系统:
- 将应用进行 zip 打包
- 将 zip 文件上传到服务器(
scp
命令) - 解压缩 zip 包(
unzip
命令) - 安装依赖
npm install
- 启动应用
npm run start:pm2
可以在日志中查看 console.log
打印的内容,查看日志 pm2 logs