node支付宝支付及同步、异步通知、主动查询支付宝订单状态

一、 如何开通支付宝支付

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,就是手机网页。
  • 这是因为支付宝的网页支付有两种,电脑页面手机页面。虽然可以定义两个路由来分别处理,但我觉得直接放一起就好,代码会更简洁一些。
  • 所以我在这里做了个判断,不同的支付方式,methodproductCode这两个参数也不同。这两个参数,都我是在支付宝的文档里找到的,固定的值,大家按照我这里写的填就行。
  • 接着定义订单的相关信息,调用刚才定义的查询当前订单方法,拿到相关的订单信息,并组装到bizContent里。
  • 继续调用pageExecute,将各种参数都传递进去。
  • 注意,用的方式是GET,也就说要生成的是一个支付链接地址,而不是表单。
  • 里面还有两个URLreturnUrl 同步通知地址(当用户在支付宝完成支付操作后,支付宝会将用户浏览器重定向到这个指定的 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,也就是等待买家付款

所以大家注意了,这说明codemsg,都不能用来判断是否付款,只有用tradeStatus才行。

7.3.4. 付款了

  • 好,我们现在付款。
  • 一付完款,马上就关闭浏览器窗口,这样同步通知就不会生效了。
  • 因为是在本地测试,而不是服务器上,所以也收不到异步通知。

这样因为收不到通知,所以订单就不会更新状态。然后我们再主动查一下订单信息:
在这里插入图片描述

这次又有变化了:

  • 重点先看支付状态(tradeStatus),现在的值是TRADE_SUCCESS,这才是付款成功了。
  • 此外,返回的数据里还有需要的:流水号(tradeNo)支付时间(sendPayDate)

我们可以用tradeStatus来判断订单状态,用然后将流水号(tradeNo)支付时间(sendPayDate)记录在数据库中。

支付成功订单优化 增加排他锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值