Bootstrap

Node接入支付宝开放平台的沙箱实现支付功能

一些重要的支付宝沙箱文档及网址

  • 支付宝开放平台:https://open.alipay.com/
  • 简单介绍:https://www.yuque.com/chenqiu/alipay-node-sdk/guide
  • 支付宝开放平台API:https://open.alipay.com/api
  • 支付宝开放平台文档:https://opendocs.alipay.com/home

支付流程介绍

图片来自支付宝开放平台。
在这里插入图片描述

上面的流程图,已经大致描述了发起支付的过程,这里笔者用自己的语言,详细地转化一下:

下单。这里的下单,在项目中往往表现为用户确认了订单、然后选择支付方式、点击立即支付,即当用户点击“立即支付”时才产生的动作。 这一步要发送一个请求到我们的服务器,假如说系统中立即支付的API路径是/pay,参数一般就是订单包含的订单项,如商品id、购买数量、优惠券id等。

服务器端处理用户的下单请求。在这一步,是业务比较多的一步,也是关键步骤。我们根据客户端传来的商品id、数量、优惠券等参数,计算出每一个商品的单价、订单的总金额等(金额是不应该由客户端直接传的,应该由服务器端计算,更安全)。再生成一个订单号。然后调用一个接口,把这些参数(如商品名称、支付金额、订单号等)打包传给支付宝,也就是对应上图中的1.1。

依然是处理用户的下单请求,只是我嫌段落太长,换一段。在图中的1.1,我们调用了支付宝的接口。其实图中还少了一步1.2,那就是支付宝会返回一段html文档,就是大家经常看到的带二维码的PC端支付页面。请注意,到这一步,对客户端来说依然是在访问/pay接口,因为我们还没有对请求作出响应,只是一直在和支付宝交互罢了。所以,我们把支付宝返回的html文档作为响应信息,返回给客户端,客户端直接加载这一段html文档,用户就看到了一个支付页面。

到了这里,对应上图中的2、3、4、5步,和我们的服务器就没关系了,完全是用户在进行支付的过程。

到了第6步,用户支付完成,支付宝会跳转到一个我们指定的网页,这个网页一般是我们自定义的页面,比如在网页上显示“您已成功购买xxx商品,感谢您的支持”等。注意,此处的returnUrl只是跳转到我们自定义的友好页面,告诉用户支付成功了,只做展示用途,并非真正的支付回调。

到了第7步,支付宝会调用我们指定的回调API接口,将一些参数传给我们,如交易流水号、订单号,我们可以根据这些信息查询交易是否真的成功了(第8步),从而执行后续的业务,比如将订单状态变为已支付、给用户增加积分、扣减优惠券……等。

沙箱环境简介与配置

支付宝有一个供开发者测试使用的沙箱环境,会提供一个沙箱版的支付宝app、一个商家账户、一个买家账户。有了这个,可以让我们跳过商家入驻、企业资质审核等过程,开箱即用,降低了学习成本。

现在,让我们简单的配置沙箱环境。

1、进入到支付宝支付官网,用你自己的支付宝扫码登录,再点击进入管理中心。
在这里插入图片描述2、点击进入沙箱应用。
在这里插入图片描述
此时就到了沙箱应用的控制台,进入之后要创建一个应用。

3、生成安全密钥
此部分的官方文档为:https://opendocs.alipay.com/common/02kipl。
进入该网址下载支付宝官方的密钥生成工具应用。

进入应用后,根据开发语言选择密钥格式和密钥长度。注意此处要选择RSA2。

然后点击生成密钥,工具会自动生成商户应用公钥(public_key)和应用私钥(private_key)。
在这里插入图片描述
这里有几个密钥要注意一下:

  • 应用公钥(public key):需提供给支付宝账号管理者上传到支付宝开放平台。
  • 应用私钥(private key):由开发者自己保存,需填写到代码中供签名时使用。
  • 支付宝公钥:支付宝提供的公钥,用于接入服务。

还有要注意的事情为:

  • 生成的私钥需妥善保管,避免遗失,不要泄露。
  • 密钥和应用(APPID)一一对应,即开发者需要为名下的每个应用分别设置密钥,且不同应用的密钥不能混用。

4、配置安全密钥
用上述的工具生成密钥之后,要把应用公钥上传到支付宝开发者平台,如图点击切换为自定义密钥。
在这里插入图片描述
在弹出的窗口,把刚才生成的应用公钥复制粘贴进去,点击确定,就可以看到这个界面了,注意这里的支付宝公钥:
在这里插入图片描述

在这里插入图片描述
5、下载支付宝沙箱APP及账号管理
“支付宝沙箱APP”可以在官网找到,直接下载即可。
在沙箱控制台当中,点击“沙箱账号”,就可以看到当前应用对应的沙箱账号,用来登录我们的沙箱版支付宝APP进行测试。
在这里插入图片描述
上面的是商家的账号,下面是买家的测试账号。

6、学会看文档
在支付宝开放平台,点击文档,即可查看文档。如果要查看API就点击API,如果要查看技术的总体介绍,就点击文档。
在这里插入图片描述比如点击API,然后选择场景:
在这里插入图片描述比如说选择电脑网站支付,就可以查看到这些API列表。点击后面的查看文档就可以查看到这些API详细的参数。
在这里插入图片描述

其实不同场景的API是差不多的,例如“下单并支付页面”的接口,电脑网站支付的API接口路径为“alipay.trade.page.pay”,而手机端的则为“alipay.trade.wap.pay”。
在这里插入图片描述

7、开始写代码

下载依赖

npm install alipay-sdk

封装SDK
appId在上面进入沙箱控制台的时候就可以看到了。
注意下面的支付宝公钥和应用私钥,都是步骤二里的。

const AlipaySdk = require('alipay-sdk').default; // 引入 SDK
const alipaySdk =  new  AlipaySdk({
  appId: '此处要修改', // 开放平台上创建应用时生成的 appId
  signType: 'RSA2', // 签名算法,默认 RSA2
  gateway: 'https://openapi.alipaydev.com/gateway.do', // 支付宝网关地址 ,沙箱环境下使用时需要修改 正式线上的时候换成 https://openapi.alipay.com/gateway.do
  alipayPublicKey: '此处要修改', // 支付宝公钥,需要对结果验签时候必填
  privateKey: '此处要修改', // 应用私钥字符串
});
module.exports = alipaySdk;

//正式环境只要把上述换成正式的就可以了

创建提交交易信息的表单

注意这里要编写的参数是在上面的API文档里可以查看到。

const AlipayFormData = require("alipay-sdk/lib/form").default;
const formData = new AlipayFormData();
formData.setMethod('get');
formData.addField('bizContent', {
	outTradeNo: '15693801273225', // 商户订单号,64个字符以内、可包含字母、数字、下划线,且不能重复
	productCode: 'FAST_INSTANT_TRADE_PAY', // 销售产品码,与支付宝签约的产品码名称,仅支持FAST_INSTANT_TRADE_PAY
	totalAmount: '0.01', // 订单总金额,单位为元,精确到小数点后两位
	subject: '商品', // 订单标题
	body: '商品详情', // 订单描述
});
// 表示异步通知回调,
formData.addField('notifyUrl', 'https://www.baidu.com');
// 付款成功的跳转页面
formData.addField('returnUrl', 'https://www.bilibili.com');

这里的notifyUrl和returnUrl要做一个区分:

  • notifyUrl:消息回调的接口,当付款成功,支付宝会发送成功信息到我们的项目当中,告诉我们付款成功了,即我们项目自己定义的接口,需要公网能够访问到的地址,即**项目要上线。**如果是自己本地的测试项目,可以配置内网穿透。
  • returnUrl:付款成功之后,页面会自动跳转的嘛,returnUrl就表示这个跳转的地址。

发送交易请求:调用SDK的exec()方法

该方法有三个参数:

const result = await alipaySdk.exec(method, params, options);
  • method:String 调用的 Api,比如 alipay.system.oauth.token
  • params:Api 的请求参数(包含“公共请求参数”和“业务参数”)
  • options:可以传入我们的formData
  • 更详细请查看:https://www.yuque.com/chenqiu/alipay-node-sdk/exec

具体实例:

// 付款API:https://opendocs.alipay.com/apis/api_1/alipay.trade.pay?scene=32
async function test() {
	const result = await alipaySdk.exec( // result 为可以跳转到支付链接的 url
		'alipay.trade.page.pay', // 统一收单下单并支付页面接口
		{}, // api 请求的参数(包含“公共请求参数”和“业务参数”)
		{
			formData: formData
		},
	);

	console.log(result)
}
test()

到这里,我们查看一下这个result是什么?
在这里插入图片描述
把这个复制到浏览器里,咱们就来到了支付界面啦,这里就可以登录我们的沙箱买家测试账号进行付款了:
在这里插入图片描述

代码跳转的话代码如下:

        goPay() {
            let data = {
                orderId: 't454545212121' //随机生成唯一的就行了这个 自己找吧
            }
            var instance = this.$axios.create({headers: {'content-type': 'application/x-www-form-urlencoded'}});
             // 代理到  http://localhost:3000/api/pcpay
            instance.post(`http://localhost:3000/api/pcpay`, this.$qs.stringify(data)).then(res =>{
                this.data=res;
                window.open(res.data.result)
            });

        }

付款完毕之后,就会跳转到returnUrl的网址喽。

有没有发现method就是API文档里的接口名称?没错!
在这里插入图片描述
咱们上面调用了“alipay.trade.page.pay”这个接口来下单与付款,下面我们再来看看如何用“alipay.trade.query”查看这个订单的状态。

async function check() {
	let outTradeNo = "15693801273225";
	const formData = new AlipayFormData();
	formData.setMethod('get');
	formData.addField('bizContent', {
		outTradeNo
	});
	// 通过该接口主动查询订单状态
	
	// 查询订单状态:https://opendocs.alipay.com/apis/api_1/alipay.trade.query?scene=23
	 let result = await alipaySdk.exec(
		'alipay.trade.query', {}, {
			formData: formData
		},
	);
console.log(result)

}

check()

这里的result还是一个网址,我们需要axios请求这个网址,才能查看订单状态:

/**
 * 添加购物车提交订单支付宝支付后查询订单状态是否成功 */
router.post('/api/member/queryOrderAlipay', (req, res) => {
  let orderId=req.body.orderId
  const formData = new AlipayFormData();
  formData.setMethod('get');
  formData.addField('bizContent', {
    orderId
  });
  // 通过该接口主动查询订单状态
  const result = alipaySdk.exec(
    'alipay.trade.query',
    {},
    { formData: formData },
  );
  axios({
    method: 'GET',
    url: result
  })
    .then(data => {
      let r = data.data.alipay_trade_query_response;
      if(r.code === '10000') { // 接口调用成功
        switch(r.trade_status) {
          case 'WAIT_BUYER_PAY':
            res.send(
              {
                "success": true,
                "message": "success",
                "code": 200,
                "timestamp": (new Date()).getTime(),
                "result": {
                  "status":0,
                  "massage":'交易创建,等待买家付款'
                }
              }
            )
            break;
          case 'TRADE_CLOSED':
            res.send(
              {
                "success": true,
                "message": "success",
                "code": 200,
                "timestamp": (new Date()).getTime(),
                "result": {
                  "status":1,
                  "massage":'未付款交易超时关闭,或支付完成后全额退款'
                }
              }
            )
            break;
          case 'TRADE_SUCCESS':
            res.send(
              {
                "success": true,
                "message": "success",
                "code": 200,
                "timestamp": (new Date()).getTime(),
                "result": {
                  "status":2,
                  "massage":'交易支付成功'
                }
              }
            )
            break;
          case 'TRADE_FINISHED':
            res.send(
              {
                "success": true,
                "message": "success",
                "code": 200,
                "timestamp": (new Date()).getTime(),
                "result": {
                  "status":3,
                  "massage":'交易结束,不可退款'
                }
              }
            )
            break;
        }
      } else if(r.code === '40004') {
        res.send('交易不存在');
      }
    })
    .catch(err => {
      res.json({
        msg: '查询失败',
        err
      });
    });

})

状态码可以通过上面API文档查看:
在这里插入图片描述

参考链接

  • https://blog.csdn.net/rapguys/article/details/115763799
  • https://blog.51cto.com/lllomh/3037337
;