一、 如何开通支付宝支付
1.1、 支付宝认证
先要说明一点,个人和企业,都可以开通支付宝支付。但两者都需要经过认证后,才能在项目中集成支付宝支付功能。要求是:
- 个人用户:要通过个体工商户验证,需要个体工商户营业执照。
- 企业用户:要通过企业实名认证,需要企业营业执照。
也就说,无论你是用个人身份,还是企业身份,都得有对应的营业执照。所以,如果没有注册个体工商户,没有注册企业,项目里是无法集成支付宝支付的。
1.2、创建应用
认证通过后,现在就可以去创建应用了,打开支付宝开放平台链接。在登录后,按照表单要求,先创建一个应用。
- 各种信息,大家根据需要自己填写就好。
- 应用类型,选择网页应用。
- 这里要注意,用来集成支付的站点,必须是
通过 ICP 备案
的!
完成后,点击控制台
,切换到网页/移动应用里,可以看到刚才创建的应用了。
1.3、应用的appId
- 注意看,在应用名字下面的那一串数字叫做
appId
,每个人的都不同。 - 再打开 Node 项目的
.env
文件,环境变量里加上:
ALIPAY_APPID=
- 将刚才复制的appId,粘贴进去。
1.4、 配置接口加签方式
接着,点击右侧的详情
,这里能看到自己应用的一些信息,点击左侧的开发设置
。
里需要将自己的密钥,或者证书传进来,才能使用支付宝的相关接口。密钥和证书,两种方式的区别是:
- 使用密钥方式,除了不能支出资金,其他全都适用。
- 所以如果你的项目要实现转账出去,或者发红包等支出操作,就要选择证书了。
我们的项目,并不涉及到支出操作,仅仅是为了收款。所以我们这里就简单点,选择密钥方式。
1.5、 密钥工具
按照网站的说明,我们下载密钥工具。完成后,大家直接安装就好。
1.6、 应用公钥与支付宝公钥
现在就会生成一对应用公钥
和应用私钥
,先复制一下上面的应用公钥。
回到刚才支付宝的网站
里,点击下一步
,粘贴到里面,再点击确认上传。
支付宝会生成一个支付宝公钥
给你。大家不要搞混了。我们上传的是应用公钥
,而支付宝给我们的是支付宝公钥
。
这个支付宝公钥
,才是在 Node 项目里,调用支付宝接口所需要的。点击下复制
打开项目的.env
文件,增加
ALIPAY_PUBLIC_KEY=
并把刚才复制的支付宝公钥
粘贴进来。
1.7、应用私钥
密钥工具
里,还生成了个应用私钥
。但它这里生成的是PKCS8
格式,这种格式是给Java
用的。我们在Node
项目里,或者在其他语言里使用,需要转换为PKCS1
格式。复制一下应用私钥
然后点击格式转换
,将刚复制的应用私钥
粘贴进去,点击转换
,就得到了转好的格式了。
我们将转换好的应用私钥
复制一下,打开.env
,增加:
ALIPAY_APP_PRIVATE_KEY=
1.8、 回顾各种密钥
至此,各种密钥就都配置完成了。流程有点繁琐,回顾一下刚才的操作,这里一共有三个密钥,应用公钥、应用私钥和支付宝公钥
,其中:
应用公钥、应用私钥
都是用密钥工具生成的。- 然后将
应用公钥上传到支付宝的网站
里,它会换给我们支付宝公钥
,要将这个值保存在环境变量
中。 应用私钥
,需要将格式转换一下后,也保存在环境变量
中。- 这样环境变量中,目前一共是三个值:
appId、支付宝公钥、转换了格式的应用私钥
。大家一定要仔细些,不要搞错了。
1.9、 上线应用
1.10、 开通产品
最后一步,需要开通产品
,访问产品中心网址。
二、 实现支付宝支付
现在基础的准备工作就完成了,我们终于可以开始开发了。站点中集成支付宝,需要使用:Alipay SDK。
2.1. 安装 alipay-sdk
npm i alipay-sdk
2.2、 初始化 SDK
继续看文档,它这里提供了初始化 SDK 的方法,可以使用公钥
或者证书
。我们这用的是公钥
,那就看第一种方式:普通公钥模式
。
它这里是将密钥保存为文件了,然后用 Node 直接读取文件。而我们是将密钥放在环境变量了,所以不用这么麻烦,直接调用就行。新建utils/alipay.js
文件,参考它这里的写法,将参数值,全都改为环境变量中读取。
const { AlipaySdk } = require('alipay-sdk');
const alipaySdk = new AlipaySdk({
appId: process.env.ALIPAY_APPID,
privateKey: process.env.ALIPAY_APP_PRIVATE_KEY,
alipayPublicKey: process.env.ALIPAY_PUBLIC_KEY
});
module.exports = alipaySdk;
2.3、 查询订单
新建一个路由文件:routes/alipay.js
,在这里,我们要查询订单,和实现支付功能
const express = require('express')
const router = express.Router()
const { User, Order } = require('../models');
const { success, failure } = require('../utils/responses');
const { NotFound, BadRequest } = require('http-errors');
const alipaySdk = require('../utils/alipay');
const userAuth = require('../middlewares/user-auth');
const moment = require('moment');
const logger = require('../utils/logger');
/**
* 公共方法:查询当前订单
* @param req
* @returns {Promise<*>}
*/
async function getOrder(req) {
const { outTradeNo } = req.body;
if (!outTradeNo) {
throw new BadRequest('订单号不能为空。');
}
const order = await Order.findOne({
where: {
outTradeNo: outTradeNo,
userId: req.userId
}
});
// 用户只能查看自己的订单
if (!order) {
throw new NotFound(`订单号: ${outTradeNo} 的订单未找到。`)
}
if (order.status > 0) {
throw new BadRequest('订单已经支付或失效,无法付款。')
}
return order;
}
module.exports = router;
- 在查询的时候,还要指明
userId是当前登录用户的id
,因为用户只能对自己订单进行支付。 - 继续要判断下,如果
订单的状态大于1
,就表示订单已经支付,或者取消了,是不能支付的。 - 只有
状态是0
的订单,才是未付款的,能正常支付
的订单。
2.4、 实现支付代码
我们要做的是网站支付,找到 pageExecute 这个地方,这就是网站里集成支付的方法。但这里的文档写的十分简陋,我们先一起简单看一下。
- 里面的bizContent,很显然,这是订单相关的一些参数。
- 下面有两处都调用了pageExecute,第一个是用了POST,它是生成了一个HTML格式的Form表单。用户可以通过这个表单,直接付款。
- 第二个的代码,是将POST改为了GET。用这种方式,可以生成了一个支付链接。用户访问这个链接地址,就可以跳转到支付宝网站上付款了。
生成表单的方式,我觉得用户体验不太好。我们选择第二种方法,使用GET
参数,跳转到支付宝网站去付款。继续添加一个路由,实现支付
/**
* 支付宝支付
* POST /alipay/pay/page 电脑页面
* POST /alipay/pay/wap 手机页面
*/
router.post('/pay/:platform', userAuth, async function (req, res, next) {
try {
// 判断是电脑页面,还是手机页面
const isPC = req.params.platform === 'page';
const method = isPC ? 'alipay.trade.page.pay' : 'alipay.trade.wap.pay';
const productCode = isPC ? 'FAST_INSTANT_TRADE_PAY' : 'QUICK_WAP_WAY';
// 支付订单信息
const order = await getOrder(req);
const { outTradeNo, totalAmount, subject } = order;
const bizContent = {
product_code: productCode,
out_trade_no: outTradeNo,
subject: subject,
total_amount: totalAmount
};
// 支付页面接口,返回 HTML 代码片段
const url = alipaySdk.pageExecute(method, 'GET', {
bizContent,
returnUrl: 'http://localhost:3000/alipay/finish', // 当支付完成后,支付宝跳转地址
notify_url: 'https://api.xw.cn/alipay/notify', // 异步通知接口地址
});
success(res, '支付地址生成成功。', { url });
} catch (error) {
failure(res, error);
}
});
这里的代码有点多,大家不用慌,我们一起来看一看:
- 首先要注意,路由上加上了
userAuth
中间件。也就说这个路由需要登录后才能访问。但是我们没有放在app.js
中,这是因为这个路由文件里,将来还会有其他路由不需要认证就能访问。 - 再来看路由地址里,有个
:platform
参数。如果请求的是/page
,那就是电脑网页。如果是/wap
,就是手机网页。 - 这是因为支付宝的网页支付有两种,
电脑页面
与手机页面
。虽然可以定义两个路由来分别处理,但我觉得直接放一起就好,代码会更简洁一些。 - 所以我在这里做了个判断,不同的支付方式,
method
和productCode
这两个参数也不同。这两个参数,都我是在支付宝的文档里找到的,固定的值,大家按照我这里写的填就行。 - 接着定义订单的相关信息,调用刚才定义的查询当前订单方法,拿到相关的订单信息,并组装到
bizContent
里。 - 继续调用
pageExecute
,将各种参数都传递进去。 - 注意,用的方式是
GET
,也就说要生成的是一个支付链接地址,而不是表单。 - 里面还有两个
URL
,returnUrl
同步通知地址(当用户在支付宝完成支付操作后,支付宝会将用户浏览器重定向到这个指定的 returnUrl。这个过程是同步的,即用户完成支付后,页面会立即跳转到该地址。它主要用于向用户展示支付结果,比如告知用户支付成功或失败,并引导用户进行后续操作,如查看订单详情、返回首页等
。)notify_url
异步通知地址(支付宝会在支付状态发生变化时(如支付成功、支付关闭等),向这个 notify_url 发起异步的 HTTP POST 请求。无论用户浏览器是否关闭、网络是否正常,支付宝都会尽力将支付状态的变更信息发送到该地址。服务器端接收到这个通知后,可以根据通知内容进行订单状态更新、库存调整等关键业务操作。
)
完成后,将生成的支付链接返回出去。
支付宝支付完整代码routes/alipay.js
const express = require('express')
const router = express.Router()
const { User, Order } = require('../models');
const { success, failure } = require('../utils/responses');
const { NotFound, BadRequest } = require('http-errors');
const alipaySdk = require('../utils/alipay');
const userAuth = require('../middlewares/user-auth');
const moment = require('moment');
const logger = require('../utils/logger');
/**
* 支付宝支付
* POST /alipay/pay/page 电脑页面
* POST /alipay/pay/wap 手机页面
*/
router.post('/pay/:platform', userAuth, async function (req, res, next) {
try {
// 判断是电脑页面,还是手机页面
const isPC = req.params.platform === 'page';
const method = isPC ? 'alipay.trade.page.pay' : 'alipay.trade.wap.pay';
const productCode = isPC ? 'FAST_INSTANT_TRADE_PAY' : 'QUICK_WAP_WAY';
// 支付订单信息
const order = await getOrder(req);
const { outTradeNo, totalAmount, subject } = order;
const bizContent = {
product_code: productCode,
out_trade_no: outTradeNo,
subject: subject,
total_amount: totalAmount
};
// 支付页面接口,返回 HTML 代码片段
const url = alipaySdk.pageExecute(method, 'GET', {
bizContent,
returnUrl: 'http://localhost:3000/alipay/finish', // 当支付完成后,支付宝跳转地址
notify_url: 'https://api.xw.cn/alipay/notify', // 异步通知接口地址
});
success(res, '支付地址生成成功。', { url });
} catch (error) {
failure(res, error);
}
});
/**
* 公共方法:查询当前订单
* @param req
* @returns {Promise<*>}
*/
async function getOrder(req) {
const { outTradeNo } = req.body;
if (!outTradeNo) {
throw new BadRequest('订单号不能为空。');
}
const order = await Order.findOne({
where: {
outTradeNo: outTradeNo,
userId: req.userId
}
});
// 用户只能查看自己的订单
if (!order) {
throw new NotFound(`订单号: ${outTradeNo} 的订单未找到。`)
}
if (order.status > 0) {
throw new BadRequest('订单已经支付或失效,无法付款。')
}
return order;
}
module.exports = router;
2.5、 添加路由 app.js
const alipayRouter = require('./routes/alipay');
app.use('/alipay', alipayRouter);
2.6、测试支付
三、 总结一下
- 在站点中集成支付宝,首先需要去开通。相关的流程比较繁琐,大家可以按照课程中的步骤,一点点去做好。
应用公钥
是传到支付宝网站上的。换来的支付宝公钥
和转换了格式的-应用私钥
,要保存到项目的环境变量里。- 支付宝支付,分为
电脑网页
和手机网页
支付。两者显示的效果是不同的。在手机真机上支付,可以直接打开支付宝 App 进行付款。
四、沙箱支付
登录自己的支付宝后,访问:沙箱支付
-
获取 APPID:
- -
获取密钥
-
在项目代码的
.env
环境变量文件中,填入刚获取的APPID
和密钥
。
4.2. 增加沙箱网关地址
支付宝沙箱测试的网关地址,与正式接口不同,需要增加配置。打开utils/alipay.js
中,增加沙箱的网关地址
const alipaySdk = new AlipaySdk({
appId: process.env.ALIPAY_APPID,
privateKey: process.env.ALIPAY_APP_PRIVATE_KEY,
alipayPublicKey: process.env.ALIPAY_PUBLIC_KEY,
// 增加沙箱网关地址
gateway: 'https://openapi-sandbox.dl.alipaydev.com/gateway.do',
});
4.3. 发起支付
发起支付后 得到沙箱支付链接
需要先获取沙箱的账号,访问https://open.alipay.com/develop/sandbox/account
访问刚才生成的支付地址,用这个账号密码登录
,然后尝试支付。
五、同步通知
5.1.routes/alipay.js
补充同步同步逻辑
支付代码里,有一个returnUrl
这里写的链接地址,就是当支付成功后,支付宝跳转回来的地址。我们这里写的是http://localhost:3000/alipay/finish
。
/**
* 支付宝支付成功后,跳转页面
* GET /alipay/finish
*/
router.get('/finish', async function (req, res) {
try {
const alipayData = req.query;
res.json({ alipayData });
} catch (error) {
failure(res, error);
}
});
- 通过req.query,获取到支付宝传过来的参数。
- 然后直接用res.json,打印出来看一看,到底有些什么东西。
5.2. 支付宝传过来的参数
- 订单号 (out_trade_no)
- 金额 (total_amount)
- 签名 (sign)
- 流水号 (trade_no)
- 时间戳 (timestamp)
5.3. 支付宝验签
继续看支付宝 SDK 的文档,这里有一个通知验签。
可以看到,可以通过调用了checkNotifySign
方法,来检查支付信息。咱们依葫芦画瓢,修改下代码
router.get('/finish', async function (req, res) {
try {
const alipayData = req.query;
const verify = alipaySdk.checkNotifySign(alipayData);
res.json({ verify });
} catch (error) {
failure(res, error)
}
});
- 也照样子调用了
checkNotifySign
。 - 并将验签的结果,返回出去。
刷新一下浏览器,可以看到提示true
,也就说明验签成功了,这就表示支付是没有问题的。
但如果修改 URL 里的任何一个参数值,例如我们将地址复制出来,并在签名参数这里,加上了111
用这个地址再次访问,就会提示false
了
5.4.更新订单与用户状态
验签成功的意思,就表示这个订单成功支付了,下一步就是要更新订单状态了。我们先简单的做个判断:
/**
* 支付宝支付成功后,跳转页面
* GET /alipay/finish
*/
router.get('/finish', async function (req, res) {
try {
const alipayData = req.query;
const verify = alipaySdk.checkNotifySign(alipayData);
// 验签成功,更新订单与会员信息
if (verify) {
const {out_trade_no, trade_no, timestamp} = alipayData;
await paidSuccess(out_trade_no, trade_no, timestamp);
// res.redirect(`https://你的前台域名/orders/${alipayData.out_trade_no}`);
res.send('支付成功');
} else {
throw new BadRequest('支付验签失败。');
}
} catch (error) {
failure(res, error)
}
});
/**
* 支付成功后,更新订单状态和会员信息
* @param outTradeNo
* @param tradeNo
* @param paidAt
* @returns {Promise<void>}
*/
async function paidSuccess(outTradeNo, tradeNo, paidAt) {
// 查询当前订单
const order = await Order.findOne({where: {outTradeNo: outTradeNo}});
// 对于状态已更新的订单,直接返回。防止用户重复请求,重复增加大会员有效期
if (order.status > 0) {
return;
}
// 更新订单状态
await order.update({
tradeNo: tradeNo, // 流水号
status: 1, // 订单状态:已支付
paymentMethod: 0, // 支付方式:支付宝
paidAt: paidAt, // 支付时间
})
// 查询订单对应的用户
const user = await User.findByPk(order.userId);
// 将用户组设置为大会员。可防止管理员创建订单,并将用户组修改为大会员
if (user.role === 0) {
user.role = 1;
}
// 使用moment.js,增加大会员有效期
user.membershipExpiredAt = moment(user.membershipExpiredAt || new Date())
.add(order.membershipMonths, 'months')
.toDate();
// 保存用户信息
await user.save();
}
5.5. 通知地址,增加到环境变量
5.5.1 开发环境
打开.env
,增加
ALIPAY_RETURN_URL=http://接口电脑IP地址:3000/alipay/finish
5.5.1 生产环境
ALIPAY_RETURN_URL=https://你的前台域名/alipay/finish
5.6. 路由中使用环境变量
{
returnUrl: process.env.ALIPAY_RETURN_URL, // 当支付完成后,支付宝跳转地址
}
六、异步通知
- 如果用户不小心关闭了浏览器,怎么更新订单状态
在支付成功后,是有一个跳转过程的,要等待几秒钟,才能跳转到指定的回调地址。再通过 URL 里的各种参数来验签,去更新的订单和用户状态。
但用户不一定会等待跳转,非常有可能,在还没有跳转的时候,因为不小心,或者某些原因,直接关闭了浏览器。如果是手机支付,也有可能支付完成后,还没等跳转,就退出了支付宝
App。这种情况其实是很常见的,那毫无疑问,因为没有跳转过来,没有请求到finish路由,订单和用户信息就不会更新。
那碰到这种情况了,总不能因为用户自己不小心关闭了浏览器,我们网站就不承认收到钱了吧?碰到这种情况,有个办法是使用异步通知
。在生成支付链接的代码里,还有一个notify_url
。
const url = alipaySdk.pageExecute(method, 'GET', {
bizContent,
returnUrl: process.env.ALIPAY_RETURN_URL, // 当支付完成后,支付宝跳转地址
notify_url: 'https://api.xw.cn/alipay/notify', // 异步通知通知地址
});
我们在这里要设定一个接口地址,当支付成功后,除了会正常跳转通知你支付成功外。支付宝还会往你指定的notify_url
里,主动发送POST
请求。
你在这个接口里,也会收到支付宝发过来的各种数据。我们同样的可以进行验签,验签通过后,更新订单与会员信息。
但是要注意啊,由于这个接口是通过支付宝的官方服务器,来主动来请求的
。所以你不能设置成localhost
,或者局域网的IP地址
。因为这种地址,在外部网络,在支付宝服务器上,是访问不到的。所以这里提供的地址,必须是部署在服务器上的在线接口地址。
让我们先写点基础代码,然后部署上去测一下。打开routes/alipay.js
,增加一个路由
/**
* 支付宝异步通知
* POST /alipay/notify
*/
router.post('/notify', async function (req, res) {
try {
const alipayData = req.body;
const verify = alipaySdk.checkNotifySign(alipayData);
// 如果验签成功,更新订单与会员信息
if (verify) {
const {out_trade_no, trade_no, gmt_payment} = alipayData;
await paidSuccess(out_trade_no, trade_no, gmt_payment);
res.send('success');
} else {
logger.warn('支付宝验签失败:', alipayData);
res.send('fail');
}
} catch (error) {
failure(res, error)
}
});
- 请求方式是POST。
- 这里有点小技巧,因为是支付宝服务器主动发出的请求,所以我们没法在浏览器,或者终端里调试数据。
- 那我们想要看到支付宝到底传了什么东西过来,可以通过日志功能来记录。
- 只要支付宝请求了这个接口,日志功能,就会将发过来的数据,存入数据库的日志表中。
6.1.增加环境变量.env
# 改为你自己的
ALIPAY_NOTIFY_URL=https://api.xw.cn/alipay/notify
routes/alipay.js
const url = alipaySdk.pageExecute(method, 'GET', {
bizContent,
returnUrl: process.env.ALIPAY_RETURN_URL, // 当支付完成后,支付宝跳转地址
notify_url: process.env.ALIPAY_NOTIFY_URL, // 异步通知接口地址
});
6.2. routes/alipay.js
完整代码 包含同步异步
const express = require('express')
const router = express.Router()
const {User, Order} = require('../models');
const {success, failure} = require('../utils/responses');
const {NotFound, BadRequest} = require('http-errors');
const alipaySdk = require('../utils/alipay');
const userAuth = require('../middlewares/user-auth');
const moment = require('moment');
const logger = require('../utils/logger');
/**
* 支付宝支付
* POST /alipay/pay/page 电脑页面
* POST /alipay/pay/wap 手机页面
*/
router.post('/pay/:platform', userAuth, async function (req, res, next) {
try {
// 判断是电脑页面,还是手机页面
const isPC = req.params.platform === 'page';
const method = isPC ? 'alipay.trade.page.pay' : 'alipay.trade.wap.pay';
const productCode = isPC ? 'FAST_INSTANT_TRADE_PAY' : 'QUICK_WAP_WAY';
// 支付订单信息
const order = await getOrder(req);
const {outTradeNo, totalAmount, subject} = order;
const bizContent = {
product_code: productCode,
out_trade_no: outTradeNo,
subject: subject,
total_amount: totalAmount
};
// 支付页面接口,返回 HTML 代码片段
const url = alipaySdk.pageExecute(method, 'GET', {
bizContent,
returnUrl: process.env.ALIPAY_RETURN_URL, // 当支付完成后,支付宝跳转地址
notify_url: process.env.ALIPAY_NOTIFY_URL, // 异步通知接口地址
});
success(res, '支付地址生成成功。', {url});
} catch (error) {
failure(res, error);
}
});
/**
* 支付宝支付成功后,跳转页面
* GET /alipay/finish
*/
router.get('/finish', async function (req, res) {
try {
const alipayData = req.query;
const verify = alipaySdk.checkNotifySign(alipayData);
// 验签成功,更新订单与会员信息
if (verify) {
const {out_trade_no, trade_no, timestamp} = alipayData;
await paidSuccess(out_trade_no, trade_no, timestamp);
// res.redirect(`https://你的前台域名/orders/${alipayData.out_trade_no}`);
res.send('支付成功');
} else {
throw new BadRequest('支付验签失败。');
}
} catch (error) {
failure(res, error)
}
});
/**
* 支付宝异步通知
* POST /alipay/notify
*/
router.post('/notify', async function (req, res) {
try {
const alipayData = req.body;
const verify = alipaySdk.checkNotifySign(alipayData);
// 如果验签成功,更新订单与会员信息
if (verify) {
const {out_trade_no, trade_no, gmt_payment} = alipayData;
await paidSuccess(out_trade_no, trade_no, gmt_payment);
res.send('success');
} else {
logger.warn('支付宝验签失败:', alipayData);
res.send('fail');
}
} catch (error) {
failure(res, error)
}
});
/**
* 支付成功后,更新订单状态和会员信息
* @param outTradeNo
* @param tradeNo
* @param paidAt
* @returns {Promise<void>}
*/
async function paidSuccess(outTradeNo, tradeNo, paidAt) {
// 查询当前订单
const order = await Order.findOne({where: {outTradeNo: outTradeNo}});
// 对于状态已更新的订单,直接返回。防止用户重复请求,重复增加大会员有效期
if (order.status > 0) {
return;
}
// 更新订单状态
await order.update({
tradeNo: tradeNo, // 流水号
status: 1, // 订单状态:已支付
paymentMethod: 0, // 支付方式:支付宝
paidAt: paidAt, // 支付时间
})
// 查询订单对应的用户
const user = await User.findByPk(order.userId);
// 将用户组设置为大会员。可防止管理员创建订单,并将用户组修改为大会员
if (user.role === 0) {
user.role = 1;
}
// 使用moment.js,增加大会员有效期
user.membershipExpiredAt = moment(user.membershipExpiredAt || new Date())
.add(order.membershipMonths, 'months')
.toDate();
// 保存用户信息
await user.save();
}
/**
* 公共方法:查询当前订单
* @param req
* @returns {Promise<*>}
*/
async function getOrder(req) {
const {outTradeNo} = req.body;
if (!outTradeNo) {
throw new BadRequest('订单号不能为空。');
}
const order = await Order.findOne({
where: {
outTradeNo: outTradeNo,
userId: req.userId
}
});
// 用户只能查看自己的订单
if (!order) {
throw new NotFound(`订单号: ${outTradeNo} 的订单未找到。`)
}
if (order.status > 0) {
throw new BadRequest('订单已支付或取消。')
}
return order;
}
module.exports = router;
七、主动查询订单状态
7.1.为啥需要主动查询订单状态
大家想一下,如果在某些极端情况下,支付成功后,同步、异步,全都未收到通知,这就导致订单状态就一直得不到更新。比方说:
- 支付成功了,但还没跳转,刚好用户又不小心关闭了浏览器,这样同步就收不到通知了。
- 还有,如果支付宝的服务,或者自己的代码、数据库出错了,这样异步也收不到通知了。
支付宝出错的情况也不是没有,近期就出现过故障:
当然这种情况并不常见,最大可能是我们自己服务器坏了,或者自己写的程序或数据库出现了问题。
真碰到这些情况了,也没关系。这前端这边
,可以做一个按钮
。让用户点击后
,主动查询支付宝的订单状态
,这样就不怕收不到通知了。
7.2. 实现主动查询订单状态
查看官方文档,这里用到的方法是:alipay.trade.query
,切换到Node.js
后,直接给出了例子,看着也很简单,调用下方法,传订单号过去就行了。
/**
* 主动查询支付宝订单状态
* POST /alipay/query
*/
router.post('/query', userAuth, async function (req, res) {
try {
// 查询订单
const order = await getOrder(req);
const result = await alipaySdk.exec('alipay.trade.query', {
bizContent: {
out_trade_no: order.outTradeNo,
},
});
success(res, '查询成功。', { result });
} catch (error) {
failure(res, error);
}
});
7.3.测试
7.3.1. 只生成支付链接
-
Apifox 打开,切换到开发环境中。
-
创建一个新订单。
-
复制订单号后,调用电脑页面支付接口。
-
但是先
不要访问
,也不要支付
。 -
我们直接,用这个订单号,去调用刚做的
主动查询支付宝订单状态接口
-
订单的
msg
,提示的是交易失败(Business Failed)
-
code
,是40004
。
7.3.2. 只扫码(登录账号密码),但不付款(不输入支付密码)
- 复制下支付地址,直接用
浏览器访问
。 - 注意了,现在用手机扫码,但是
不要付款
。 - 再次调用接口,
查询下订单状态
。
code
,是10000
。msg
,是Success
,但其实我们并没有付款。- 所以继续看,下面还多了个
tradeStatus
。看来这个才是真正的订单状态
,写的是WAIT_BUYER_PAY
,也就是等待买家付款
。
所以大家注意了,这说明code
和msg
,都不能用来判断是否付款,只有用tradeStatus
才行。
7.3.4. 付款了
- 好,我们现在付款。
- 一付完款,马上就关闭浏览器窗口,这样同步通知就不会生效了。
- 因为是在本地测试,而不是服务器上,所以也收不到异步通知。
这样因为收不到通知,所以订单就不会更新状态。然后我们再主动查一下订单信息:
这次又有变化了:
- 重点先看
支付状态(tradeStatus)
,现在的值是TRADE_SUCCESS
,这才是付款成功了。 - 此外,返回的数据里还有需要的:
流水号(tradeNo)
和支付时间(sendPayDate)
。
我们可以用tradeStatus
来判断订单状态,用然后将流水号(tradeNo)
和支付时间(sendPayDate)
记录在数据库中。