React+Redux+Ant Design+TypeScript 电子商务实战-服务端应用 05 支付宝SDK & pm2

支付宝开发工具介绍

工具介绍

alipay-sdk

alipay-sdk 是支付宝 NodeJS 版 的 服务端 SDK (通用版),用于实现本例的支付功能。

配置沙箱环境应用的密钥

沙箱环境默认有一个用于测试的应用,代码编写时使用它的 APPID。

首次使用该应用还是需要配置密钥,生成密钥参考:生成密钥并上传

本例使用 “Web 在线加密” 生成密钥,这是网页版工具不需要下载。

  • 密钥长度(即签名算法)选择默认推荐的 RSA2
  • 密钥格式选择 PKCS2(非JAVA适用)

生成密钥会生成应用私钥应用公钥两个字符串,记得将它们保存到本地。

设置沙箱环境应用的密钥时:

  • 密钥的加签方式是“公钥”
  • 将刚才生成的应用公钥配置进去,保存设置后会生成支付宝公钥,记得将其保存到本地

保存到本地的目的:

由于实例化 SDK 客户端时需要指定应用的私钥信息,请务必注意不要将私钥信息配置在源码中(比如配置为常量或储存在配置文件的某个字段中等),因为私钥的保密等级往往比源码高得多,将会增加私钥泄露的风险。推荐将私钥信息储存在专用的私钥文件中,将私钥文件通过安全的流程分发到服务器的安全储存区域上,仅供自己的应用运行时读取。

来源:Alipay SDK 集成说明

保存到项目 /pem 目录下的文件:

  • app_public_key.txt - 应用公钥
  • app_private_key.txt - 应用私钥
  • alipay_pubilic_key.txt - 支付宝公钥

使用 OpenSLL 工具生成的密钥是 pem 格式的,使用“Web 在线加密”工具生成的是字符串或 txt 文本格式的密钥,所以本例全部以 .txt 文件保存。

支付流程

本例支付流程没有指南中那么严谨:

  1. 系统在支付订单前并不向数据库保存订单数据
  2. 用户在商户订单页面点击支付按钮
  3. 客户端向服务器发送请求获取到PC端支付宝收银台地址后跳转
  4. 用户在收银台地址登录并完成支付后,进行两个操作:
    1. 同步跳转回商户网站的支付成功页面
    2. 异步向商户发送通知(请求商户提供的 api)
  5. 商户接收到异步通知,对请求进行验签,确认来源安全后创建订单保存到数据库

创建 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:会返回一个支付宝收银台页面地址

官方指南:pageExecute()方法如何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

参数名称是否必传类型说明
totalAmountNumber订单总金额,支付宝SDK要求的必传参数
subjectString订单标题,支付宝SDK要求的必传参数
bodyString订单描述,支付宝SDK要求的可选参数
returnUrlNumber客户端回跳地址,支付宝SDK要求的可选参数
notifyUrlString服务端通知api,支付宝SDK要求的可选参数
addressString收货地址
productsArray产品列表 [{product: <productId>, count: <购买数量>}]
userIdString用户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&timestamp=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 的主要内容有两个:

  1. 验签,验证请求来源是否安全
  2. 修改数据库信息,本例就是在订单支付成功后将订单信息创建并保存在数据库
// 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 操作系统:

  1. 将应用进行 zip 打包
  2. 将 zip 文件上传到服务器(scp 命令)
  3. 解压缩 zip 包(unzip 命令)
  4. 安装依赖 npm install
  5. 启动应用 npm run start:pm2

可以在日志中查看 console.log 打印的内容,查看日志 pm2 logs

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值