支付宝 当面付(扫描支付) 对接逻辑

这两天给网站 博客下方添加了 打赏功能 使用的是 支付宝的 当面付功能 特此记录一下,觉得不错的可以在下方打赏 嘿嘿 ,下面先来看一下效果图。

image-20210426232134924

1.当面付产品介绍

支付宝当面付产品官网介绍

本篇主要介绍 扫码支付

当面付帮助商家在线下消费场景中实现快速收款,支持 条码支付扫码支付 两种付款方式。商家可通过以下两种任一方式进行收款,提升收银效率,实现资金实时到账

  • 条码支付:买家出示支付宝钱包中的条码、二维码,商家扫描用户条码即可完成 条码支付 收款。
  • 扫码支付:买家通过使用支付宝 扫一扫 功能,扫描商家收款二维码即可完成 扫码支付 付款。

2.扫码支付应用场景

适用于单件商品单独定价、无人值守自助售货机 打赏等商家。用户打开支付宝中的 扫一扫 功能,扫描商家展示的二维码进行支付。该模式适用于线下实体店支付、面对面支付、自助售货机等场景。

image-20210426232434270

3.准入条件 (商家想用该功能的条件)

需要有营业执照和门头照片,门头照片可以网上随便找一个上传上去, 营业执照需要和商家账户名相同 ,本人是没有的 ,所以大概 30天后 就无法使用了。。

  • 该能力对企业支付宝账户和个体工商户开放。

  • 商家的收银系统需要有红外扫描枪设备,或其他的扫码、展示码设备。

  • 签约申请提交资料:

    • 经营场所照片。

      • 有门头照的经营场所,需提供门头照。
      • 无门头照的经营场所,需提供内景照或场景照。
    • 同名的营业执照(即与支付宝账户认证名称一致)。

      • 您可以在支付宝 PC 端登录 商家中心,进入 产品签约管理,通过资质凭证补全来恢复产品的正常使用。

        账户类型 收款额度规则 入驻资质要求(页面需要填写) 使用时间
        个人账户 单笔收款 ≤ 500元,单日收款 ≤ 5000元,不区分借记或贷记渠道。 1、需按规范提交经营场景照片(如门头照、门店地址);2、提供本人同名营业执照。 30天。如提交资料均不通过,商家需要在合约生效后的30天内补全门头照等资料,否则会影响正常收款温馨提示:当使用时间到期(从合约生效开始计算 30天)系统会在到期前 15 天和 3 天发出代办通知,30 天到期,为了更好的产品使用体验,建议您及时补全资质)。
        个人账户 收款不受限额。 1、需按规范提交经营场景照片(如门头照、门店地址);2、提供本人同名营业执照。 若提交资料均通过,收款不受限。
        企业账户 收款不受限额。
    -   若签约时提供了同名营业执照,或者签约后补充了同名营业执照,商家收款无限额。

4.接入准备

需要获取 以下参数供 后续调用接口:

APPID :通过创建应用后 应用头像下发的 202100xxxxx 这个就是APPID

应用私钥: 通过密钥在线生成器 可以直接生成

应用公钥: 通过密钥在线生成器 可以直接生成

支付宝公钥: 这个要注意,是通过 上面的应用公钥 去换取的 ,在应用 开发信息 -》 接口加签方式 -》设置

4.1 创建应用

登录 支付宝开放平台,创建应用并提交审核,审核通过后会生成应用唯一标识 APPID,并且可以申请开通开放产品使用权限。通过 APPID 应用才能调用开放产品的接口能力。详情请参考 创建应用

image-20210426233500081

4.2 配置应用

应用创建完成后,系统会自动跳转到应用详情页面。您可以在 能力列表 中点击 添加能力 来添加 当面付 功能

image-20210426233407440

4.3 设置接口加签方式

在 应用 -》开发信息 -》 接口加签方式 -》设置

支付宝开放平台的应用管理体系,使用了公私钥的机制,商户可通过设置接口加签方式为自身应用配置 公私钥/公私钥证书 来保障商户应用和支付宝交互的安全性。密钥的获取方式请参见 生成密钥 可以选择Web在线生成方式

image-20210426233924123

公钥证书与公钥的区别请参见 普通公钥与公钥证书区别

填入 上面在线生成的 公钥字符 点击保存设置

image-20210426234027959

通过应用公钥 换取 支付宝公钥 这个后面调用接口是有用的

image-20210426234109273

4.4 上线应用

商户在添加功能和配置密钥后,即可将应用提交审核,预计会有一个工作日的审核时间,请耐心等待,详细步骤可参考 上线应用

应用上线后,还需要完成应用签约才能在线上环境(生产环境)使用当面付功能。

image-20210426235255354

4.5 签约 当面付

这个也很重要 必须签约后 才能使用

当面付 需要签约 上传 门头照片 和 营业执照 ,门头照片可以网上随便找一个无水印的, 营业执照可以暂时不传,不传大概可以使用30天

签约完成后 应用部分会多出一个 应用2.0签约xxxxx 自动生成的

image-20210426233330970

当上面的接入准备都做完后 即可获得 以下几个数据

APPID :通过创建应用后 应用头像下发的 202100xxxxx 这个就是APPID

应用私钥: 通过密钥在线生成器 可以直接生成

应用公钥: 通过密钥在线生成器 可以直接生成

支付宝公钥: 这个要注意,是通过 上面的应用公钥 去换取的 ,在应用 开发信息 -》 接口加签方式 -》设置

5.扫码支付 逻辑

当面付扫描支付逻辑

来看看 支付宝提供的 接入流程图

开发者需要确认自己的应用在审核通过后显示 已上线,同时完成当面付功能的签约后,才能顺利调用以下接口。否则会有缺少权限的报错。

image-20210426235207462

调用流程

image-20210426235331071

  1. 后台 调用 支付宝的 预下单请求(可以设置二维码过期时间) 获取二维码
  2. 后台将二维码 生成图片 返回给 前端
  3. 用户进行扫码 支付完成后 可以通过配置 支付成功异步调用接口,或者后台自主轮训查询接口 来判断是否支付完成
  4. 如果二维码过期或者 在指定时间内 用户未支付 调用 cancel 接口去 撤销关闭交易
  5. 当面付推荐使用 自主轮训查询,我这里使用的异步通知接口 为了尝试看看
  1. 商户系统调用 alipay.trade.precreate(统一收单线下交易预创建)接口,获得该订单的二维码串 qr_code,开发者需要利用二维码生成工具获得最终的订单二维码图片;
  2. 发起轮询获得支付结果:等待 5 秒后调用 alipay.trade.query(统一收单线下交易查询)接口,通过支付时传入的商户订单号(out_trade_no)查询支付结果(返回参数 TRADE_STATUS),如果仍然返回等待用户付款(WAIT_BUYER_PAY),则再次等待 5 秒后继续查询,直到返回确切的支付结果(成功 TRADE_SUCCESS 或 已撤销关闭 TRADE_CLOSED),或是超出轮询时间。在最后一次查询仍然返回等待用户付款的情况下,必须立即调用 alipay.trade.cancel(统一收单交易撤销接口)将这笔交易撤销,避免用户继续支付;
  3. 除了主动轮询,当订单支付成功时,商户也可以通过设置异步通知(notify_url)来获得支付宝服务端返回的支付结果,详见 扫码异步通知,注意一定要对异步通知验签,确保通知是支付宝发出的。

6. 服务端 SDK (新版) 简述

服务端的新版SDK 连接

支付宝提供了 2种 SDK 一个是老版本 一个是新版本,我把两个都试了下,这里我选用 新版来介绍和使用,新版主要使用 Factory 类

image-20210427000048152

POM 引入 新版sdk alipay-easysdk

       <!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk -->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-easysdk</artifactId>
            <version>2.1.2</version>
        </dependency>

调用示例

仔细看看 示例没啥难的,需要注意的是 alipayPublicKey = 支付宝公钥,不是应用公钥,就是上面拿应用公钥去换取的 支付宝公钥, 我这里使用的 非正书模式

import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.factory.Factory.Payment;
import com.alipay.easysdk.kernel.Config;
import com.alipay.easysdk.kernel.util.ResponseChecker;
import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        // 1. 设置参数(全局只需设置一次)
        Factory.setOptions(getOptions());
        try {
            // 2. 发起API调用(以创建当面付收款二维码为例)
            AlipayTradePrecreateResponse response = Payment.FaceToFace()
                    .preCreate("Apple iPhone11 128G", "2234567890", "5799.00");
            // 3. 处理响应或异常
            if (ResponseChecker.success(response)) {
                System.out.println("调用成功");
            } else {
                System.err.println("调用失败,原因:" + response.msg + "," + response.subMsg);
            }
        } catch (Exception e) {
            System.err.println("调用遭遇异常,原因:" + e.getMessage());
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    private static Config getOptions() {
        Config config = new Config();
        config.protocol = "https";
        config.gatewayHost = "openapi.alipay.com";
        config.signType = "RSA2";

        config.appId = "<-- 请填写您的AppId,例如:2019091767145019 -->";

        // 为避免私钥随源码泄露,推荐从文件中读取私钥字符串而不是写入源码中
        config.merchantPrivateKey = "<-- 请填写您的应用私钥,例如:MIIEvQIBADANB ... ... -->";

        //注:证书文件路径支持设置为文件系统中的路径或CLASS_PATH中的路径,优先从文件系统中加载,加载失败后会继续尝试从CLASS_PATH中加载
        config.merchantCertPath = "<-- 请填写您的应用公钥证书文件路径,例如:/foo/appCertPublicKey_2019051064521003.crt -->";
        config.alipayCertPath = "<-- 请填写您的支付宝公钥证书文件路径,例如:/foo/alipayCertPublicKey_RSA2.crt -->";
        config.alipayRootCertPath = "<-- 请填写您的支付宝根证书文件路径,例如:/foo/alipayRootCert.crt -->";

        //注:如果采用非证书模式,则无需赋值上面的三个证书路径,改为赋值如下的支付宝公钥字符串即可
        // config.alipayPublicKey = "<-- 请填写您的支付宝公钥,例如:MIIBIjANBg... -->";

        //可设置异步通知接收服务地址(可选)
        config.notifyUrl = "<-- 请填写您的支付类接口异步通知接收服务地址,例如:https://www.test.com/callback -->";

        //可设置AES密钥,调用AES加解密相关接口时需要(可选)
        config.encryptKey = "<-- 请填写您的AES密钥,例如:aa4BtZ4tspm2wnXLb1ThQA== -->";

        return config;
    }
}

7.异步通知 (仅用于扫码支付)

异步通知文档连接

POST 方式 ,异步通知地址 notify_url

​ //可设置异步通知接收服务地址(可选) ​ config.notifyUrl = “<– 请填写您的支付类接口异步通知接收服务地址,例如:https://www.test.com/callback –>”;

异步通知是指当收银台调用 预下单 请求 API 生成二维码展示给用户后,用户通过手机扫描二维码进行支付,支付宝会将该笔订单的变更信息,沿着商户调用预下单请求时所传入的异步通知地址 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商户系统

主要使用下面的 通知参数来接收数据

7.1 异步通知参数

参数 类型 必填 描述
notify_time Date 通知时间。通知的发送时间。格式为yyyy-MM-dd HH:mm:ss。示例值:2011-12-27 06:30:30
notify_type String(64) 通知类型。示例值:trade_status_sync
notify_id String(128) 通知校验 ID。示例值:ac05099524730693a8b330c5ecf72da9786
sign_type String(10) 签名类型。商户生成签名字符串所使用的签名算法类型,目前支持 RSA2 和 RSA,推荐使用 RSA2(如果开发者手动验签,不使用 SDK 验签,可以不传此参数)。示例值:RSA2
sign String(256) 签名。请参考异步返回结果的验签(如果开发者手动验签,不使用 SDK 验签,可以不传此参数)。示例值:601510b7970e52cc63db0f44997cf70e
trade_no String(64) 支付宝交易号。支付宝交易凭证号。示例值:2013112011001004330000121536
app_id String(32) 开发者的 app_id。支付宝分配给开发者的应用 APPID。示例值:2014072300007148
out_trade_no String(64) 商户订单号。原支付请求的商户订单号。示例值:6823789339978248
out_biz_no String(64) 商户业务号。商户业务 ID,主要是退款通知中返回退款申请的流水号。示例值:HZRF001
buyer_id String(16) 买家支付宝用户号。买家支付宝账号对应的支付宝唯一用户号。示例值:2088102122524333
buyer_logon_id String(100) 买家支付宝账号。示例值:15901825620
seller_id String(30) 卖家支付宝用户号。示例值:2088101106499364
seller_email String(100) 卖家支付宝账号。示例值:zhuzhanghu@alitest.com
trade_status String(32) 交易状态。交易目前所处的状态。示例值:TRADE_CLOSED
total_amount Number(9,2) 订单金额。本次交易支付的订单金额,单位为人民币(元)。示例值:20
receipt_amount Number(9,2) 实收金额。商户在交易中实际收到的款项,单位为人民币(元)。示例值:15
invoice_amount Number(9,2) 开票金额。用户在交易中支付的可开发票的金额。示例值:10.00
buyer_pay_amount Number(9,2) 付款金额。用户在交易中支付的金额。示例值:13.88
point_amount Number(9,2) 集分宝金额。使用集分宝支付的金额。示例值:12.00
refund_fee Number(9,2) 总退款金额。退款通知中,返回总退款金额,单位为元,支持两位小数。示例值:2.58
send_back_fee Number(9,2) 实际退款金额。商户实际退款给用户的金额,单位为元,支持两位小数。示例值:2.08
subject String(256) 订单标题。商品的标题/交易标题/订单标题/订单关键字等,是请求时对应的参数,原样通知回来。示例值:当面付交易
body String(400) 商品描述。该订单的备注、描述、明细等。对应请求时的 body 参数,原样通知回来。示例值:当面付交易内容
gmt_create Date 交易创建时间。该笔交易创建的时间。格式为 yyyy-MM-dd HH:mm:ss。示例值:2015-04-27 15:45:57
gmt_payment Date 交易付款时间。该笔交易的买家付款时间。格式为 yyyy-MM-dd HH:mm:ss。示例值:2015-04-27 15:45:57
gmt_refund Date 交易退款时间。该笔交易的退款时间。格式为 yyyy-MM-dd HH:mm:ss.S。示例值:2015-04-28 15:45:57.320
gmt_close Date 交易结束时间。该笔交易结束时间。格式为 yyyy-MM-dd HH:mm:ss。示例值:2015-04-29 15:45:57
fund_bill_list String(512) 支付金额信息。支付成功的各个渠道金额信息,详见下表 资金明细信息说明示例值:[{“amount”:”15.00″,”fundChannel”:”ALIPAYACCOUNT”}]
voucher_detail_list String 优惠券信息。本交易支付时所使用的所有优惠券信息,详见下表 优惠券信息说明示例值:[{“amount”:“0.20”,“merchantContribute”:“0.00”,“name”:“一键创建券模板的券名称”,“otherContribute”:“0.20”,“type”:“ALIPAY_BIZ_VOUCHER”,“memo”:“学生卡8折优惠”}]

8. 集成博客系统 打赏功能 案例代码

使用的是 新版SDK Factory

8.1 提供获取二维码功能

    @RequestMapping("/qrCode")
    @CrossOrigin
    public void getPayQr(String money, HttpServletRequest request, HttpServletResponse response) {
        aliPayService.getPayQr(money, new ServletWebRequest(request, response));
    }

主要就是调用 预付款接口 Factory.Payment.FaceToFace().preCreate() 接口 获取到二维码的 code ,根据google 的 zxing core 生成二维码图片

通过ImageIO.write 把二维码图片写到 outputstream 中

ImageIO.write(qrBufferedImage, aliPayTradeQrConfig.getQrSuffix(), request.getResponse().getOutputStream());



@Override
    public void getPayQr(String money, ServletWebRequest request) {
        log.info("【----------------------start getPayQr--------------------------】");
        long currentTimes = System.currentTimeMillis();
        try {
            // 2. 发起API调用(以创建当面付收款二维码为例)
            String outTradeNo = "" + currentTimes
                    + (long) (Math.random() * 10000000L);
            Map<String, Object> paramMap = new HashMap<>();
            //二维码10分钟后 失效
            paramMap.put("timeout_express", aliPayTradeQrConfig.getQrExpireMinute() + "m");
            AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace()
                    .batchOptional(paramMap)
                    .preCreate(subject, outTradeNo, money);
            // 3. 处理响应或异常
            if (ResponseChecker.success(response)) {
                log.info("预付款 preCreate 调用成功 业务订单号outTradeNo : {}", outTradeNo);
                // 需要修改为运行机器上的路径
                BufferedImage qrBufferedImage = ZxingUtils.getQRBufferedImage(response.getQrCode(), aliPayTradeQrConfig.getQrWidth(), aliPayTradeQrConfig.getQrHeight());
                if (qrBufferedImage != null) {
                    //保存业务订单信息
                    taskExecutor.execute(() -> {
                        long expireTime = currentTimes + (aliPayTradeQrConfig.getQrExpireMinute() * 60 * 1000);
                        AliPayTradeQrDelayContext.DELAY_QUEUE.put(AliPayTradeQrDelayContext.buildAliPayTradeQrDelayInfo(outTradeNo,
                                expireTime));
                        payOrderService.buildPayOrderInfo(subject, outTradeNo, money, expireTime);
                    });
                    //打印图片 将文件流放入response中
                    if (request.getResponse() != null) {
                        ImageIO.write(qrBufferedImage, aliPayTradeQrConfig.getQrSuffix(), request.getResponse().getOutputStream());
                    }
                }
            } else {
                log.error("调用失败,原因:{}", response.msg + "," + response.subMsg);
            }
        } catch (Exception e) {
            log.error("调用遭遇异常,原因:{}", e.getMessage());
            throw new RuntimeException(e.getMessage(), e);
        }
        log.info("【----------------------end getPayQr--------------------------】");
    }

com.google.zxing

<!-- https://mvnrepository.com/artifact/com.google.zxing/core -->
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.3.0</version>
</dependency>

ZxingUtils 生成图片二维码 工具类

/**
 * @author johnny
 * @create 2021-04-25 9:29 下午
 **/
public class ZxingUtils {

    private static Log log = LogFactory.getLog(ZxingUtils.class);

    private static final int BLACK = 0xFF000000;
    private static final int WHITE = 0xFFFFFFFF;

    private static BufferedImage toBufferedImage(BitMatrix matrix) {
        int width = matrix.getWidth();
        int height = matrix.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE);
            }
        }
        return image;
    }

    private static void writeToFile(BitMatrix matrix, String format, File file) throws IOException {
        BufferedImage image = toBufferedImage(matrix);
        if (!ImageIO.write(image, format, file)) {
            throw new IOException("Could not write an image of format " + format + " to " + file);
        }
    }

    /**
     * 将内容contents生成长宽均为width的图片,图片路径由imgPath指定
     */
    public static File getQRCodeImge(String contents, int width, String imgPath) {
        return getQRCodeImge(contents, width, width, imgPath);
    }

    /**
     * 将内容contents生成长为width,宽为width的图片,图片路径由imgPath指定
     */
    public static File getQRCodeImge(String contents, int width, int height, String imgPath) {
        try {
            Map<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
            hints.put(EncodeHintType.CHARACTER_SET, "UTF8");

            BitMatrix bitMatrix = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, width, height, hints);

            File imageFile = new File(imgPath);
            writeToFile(bitMatrix, "png", imageFile);

            return imageFile;

        } catch (Exception e) {
            log.error("create QR code error!", e);
            return null;
        }
    }

    public static BufferedImage getQRBufferedImage(String contents, int width, int height) {
        try {
            Map<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
            hints.put(EncodeHintType.CHARACTER_SET, "UTF8");

            BitMatrix bitMatrix = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, width, height, hints);

            BufferedImage image = toBufferedImage(bitMatrix);

            return image;
        } catch (Exception e) {
            log.error("create QR code error!", e);
            return null;
        }
    }
}

8.2 提供异步通知接口

支付宝 异步通知 文档中 明确说了,需要对异步通知接口收到的消息进行 以下的校验

1.订单校验

2.签名参数的校验

3.总金额校验

等等。。

异步返回结果验签

  1. 在通知返回参数列表中,除去 sign、sign_type 两个参数外,凡是通知返回回来的参数皆是待验签的参数;

  2. 将剩下参数进行 url_decode, 然后进行字典排序,组成字符串,得到待签名字符串:

    gmt_create=2015-06-11 22:33:46&gmt_payment=2015-06-11 22:33:59&notify_id=42af7baacd1d3746cf7b56752b91edcj34&notify_time=2015-06-11 22:34:03&notify_type=trade_status_sync&out_trade_no=21repl2ac2eOutTradeNo322&seller_email=testyufabu07@alipay.com&seller_id=2088211521646673&subject=FACE_TO_FACE_PAYMENT_PRECREATE中文&trade_no=2015061121001004400068549373&trade_status=TRADE_SUCCESS
  1. 将签名参数(sign)使用 base64 解码为字节码串;
  2. 使用 RSA/RSA2 的验签方法,通过签名字符串、签名参数(经过 base64 解码)及支付宝公钥验证签名;
  3. 需要严格按照如下描述校验通知数据的正确性:
    • 商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号。
    • 判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)。
    • 校验通知中的 seller_id(或者seller_email) 是否为 out_trade_no 这笔单据的对应的操作方(有的时候,一个商户可能有多个 seller_id/seller_email)。

上述有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。

新版SDK 校验方法

signVerified = Factory.Payment.Common().verifyNotify(params);

  /**
     * https://opendocs.alipay.com/open/194/103296
     *
     * @param request
     * @return
     */
    @RequestMapping("/callback")
    @CrossOrigin
    public String callback(HttpServletRequest request) {
        // 将异步通知中收到的待验证所有参数都存放到map中
        Map<String, String> params = convertRequestParamsToMap(request);
        String paramsJson = gson.toJson(params);
        log.info("支付宝回调,{}", paramsJson);
        try {
            // 支付宝配置 老版本这样做 ,新版本使用  Factory.setOptions(AliPayConfigUtils.getOptions());
            //AlipayConfig alipayConfig = new AlipayConfig();
            boolean signVerified = false;
            // 调用老的 SDK验证签名
//            boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayConfig.getAlipay_public_key(),
//                    alipayConfig.getCharset(), alipayConfig.getSigntype());
            try {
              // 调用老的 SDK验证签名
                signVerified = Factory.Payment.Common().verifyNotify(params);
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (signVerified) {
                log.info("支付宝回调签名认证成功");
                AlipayNotifyParam alipayNotifyParam = buildAlipayNotifyParam(params);
                // 按照支付结果异步通知中的描述,对支付结果中的业务内容进行1\2\3\4二次校验,校验成功后在response中返回success,校验失败返回failure
                aliPayService.checkNotifyParam(alipayNotifyParam);
                // 另起线程处理业务
                taskExecutor.execute(() -> {
                    String tradeStatus = alipayNotifyParam.getTradeStatus();
                    // 支付成功
                    if (AliPayTradeStatusEnum.TRADE_SUCCESS.name().equals(tradeStatus)
                            || AliPayTradeStatusEnum.TRADE_FINISHED.name().equals(tradeStatus)) {
                        try {
                            // 处理支付成功逻辑
                            aliPayService.processSuccess(alipayNotifyParam);
                            log.info("【支付 回调成功: 】");
                        } catch (Exception e) {
                            log.error("支付宝回调业务处理报错,params:" + paramsJson, e);
                        }
                    } else if (AliPayTradeStatusEnum.TRADE_CLOSED.name().equals(tradeStatus)) {
                        log.error("支付宝回调业务,支付宝交易状态:{}, params:{}", tradeStatus, paramsJson);
                        aliPayService.processSuccess(alipayNotifyParam);
                    } else {
                        log.error("没有处理支付宝回调业务,支付宝交易状态:{},params:{}", tradeStatus, paramsJson);
                    }
                });
                // 如果签名验证正确,立即返回success,后续业务另起线程单独处理
                // 业务处理失败,可查看日志进行补偿,跟支付宝已经没多大关系。
                return "success";
            } else {
                log.info("支付宝回调签名认证失败,signVerified=false, paramsJson:{}", paramsJson);
                return "failure";
            }
        } catch (AlipayApiException e) {
            log.error("支付宝回调签名认证失败,paramsJson:{},errorMsg:{}", paramsJson, e.getMessage());
            return "failure";
        }
    }

校验方法

    @Override    public void checkNotifyParam(AlipayNotifyParam alipayNotifyParam) throws AlipayApiException {        String outTradeNo = alipayNotifyParam.getOutTradeNo();        // 1、商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,        log.info("【receive outTradeNo : {}】", outTradeNo);        PayOrderInfo payOrderInfo = payOrderService.findByOutTradeNoAndAppId(outTradeNo, AliPayConfigUtils.getOptions().appId);        if (payOrderInfo == null) {            throw new AlipayApiException("out_trade_no错误");        }//        // 2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),        long total_amount = alipayNotifyParam.getTotalAmount().multiply(new BigDecimal(100)).longValue();        if (total_amount != payOrderInfo.getQrCodeAmount().multiply(new BigDecimal(100)).longValue()) {            throw new AlipayApiException("error total_amount");        }        // 3、校验通知中的seller_id(或者seller_email)是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email),//        // 第三步可根据实际情况省略//        // 4、验证app_id是否为该商户本身。        if (!alipayNotifyParam.getAppId().equals(AliPayConfigUtils.getOptions().appId)) {            throw new AlipayApiException("app_id不一致");        }    }

8.3 延迟队列去监听 过期交易 发送alipay.trade.cancel 撤销交易

延迟队列去调用 cancel 撤销接口 延迟队列的使用就不介绍了

内部涉及了 延迟队列的使用 ,逻辑就是 获取到二维码图片后 把订单任务放入延迟队列中,由另外一个线程从延迟队列中获取数据 去判断该条订单记录是否支付完成,如果未支付完成 则调用 cancel 订单撤销接口

long expireTime = currentTimes + (aliPayTradeQrConfig.getQrExpireMinute() * 60 * 1000);                        AliPayTradeQrDelayContext.DELAY_QUEUE.put(AliPayTradeQrDelayContext.buildAliPayTradeQrDelayInfo(outTradeNo,                                expireTime));
/** * @author johnny * @create 2021-04-26 8:53 下午 **/@Service@Slf4jpublic class AliPayTradeQrDelayContext {    public static final DelayQueue<AliPayTradeQrDelayInfo> DELAY_QUEUE = new DelayQueue<>();    @Autowired    private ThreadPoolTaskExecutor taskExecutor;    @Autowired    private AliPayService aliPayService;    @PostConstruct    public void init() {        //启动 延迟 二维码过期 监听线程        taskExecutor.execute(new AliPayTradeQrDelayConsumer(aliPayService));    }    /**     * 构造 AliPayTradeQrDelayInfo     */    public static AliPayTradeQrDelayInfo buildAliPayTradeQrDelayInfo(String outTradeNo, long expireTime) {        return new AliPayTradeQrDelayInfo(outTradeNo, expireTime);    }}
/** * 二维码延迟队列中的 info * * @author johnny * @create 2021-04-26 8:46 下午 **/@Datapublic class AliPayTradeQrDelayInfo implements Delayed {    private String outTradeNo;    private long expireTime;    public AliPayTradeQrDelayInfo(String outTradeNo, long expireTime) {        this.outTradeNo = outTradeNo;        this.expireTime = expireTime;    }    @Override    public long getDelay(TimeUnit unit) {        //判断expireTime是否大于当前系统时间,并将结果转换成MILLISECONDS        long diffTime = expireTime - System.currentTimeMillis();        return unit.convert(diffTime, TimeUnit.MILLISECONDS);    }    @Override    public int compareTo(Delayed o) {        //compareTo用在AliPayTradeQrDelayInfo的排序        return (int) (this.getExpireTime() - ((AliPayTradeQrDelayInfo) o).getExpireTime());    }}

队列监听者

/** * @author johnny * @create 2021-04-26 4:56 下午 **/@Slf4jpublic class AliPayTradeQrDelayConsumer implements Runnable {    private AliPayService aliPayService;    public AliPayTradeQrDelayConsumer(AliPayService aliPayService) {        this.aliPayService = aliPayService;    }    @Override    public void run() {        log.info("【监控过期二维码线程启动】");        while (true) {            try {                //阻塞 获取 DELAY_QUEUE 队列中的数据 判断是否订单完成 未完成则需要去 调用alipay.trade.canal 撤销交易                AliPayTradeQrDelayInfo aliPayTradeQrDelayInfo = AliPayTradeQrDelayContext.DELAY_QUEUE.take();                if (StringUtils.isNotEmpty(aliPayTradeQrDelayInfo.getOutTradeNo())) {                    //调用 撤销订单操作                    aliPayService.judgeCancel(aliPayTradeQrDelayInfo.getOutTradeNo());                }            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}    /**     * 如果是异步的回调,则安排定时任务和延迟队列 去 执行 过期的支付订单的  撤销交易     * 如果是主动轮训, 则轮训结束后 还没成功,则直接调用该方法  撤销交易          * Factory.Payment.Common().cancel     * @param outTradeNo     */    @Override    public void judgeCancel(String outTradeNo) {        log.info("【----------------------start judgeCancel--------------------------】");        PayOrderInfo payOrderInfo = payOrderService.findByOutTradeNoAndAppId(outTradeNo, AliPayConfigUtils.getOptions().appId);        if (payOrderInfo != null) {            if (!Arrays.asList(AliPayTradeStatusEnum.TRADE_SUCCESS.name(), AliPayTradeStatusEnum.TRADE_FINISHED.name())                    .contains(payOrderInfo.getTradeStatus())) {                //未完成的 表示已经 过期了                try {                    log.info("call cancel request outTradeNo : {}", outTradeNo);                    AlipayTradeCancelResponse cancelResponse = Factory.Payment.Common().cancel(outTradeNo);                    if (ResponseChecker.success(cancelResponse)) {                        //业务订单 状态给修改一下 撤销成功 或者 删除等操作                        log.info("call cancel request success : {}", outTradeNo);                        payOrderService.deletePayOrderByOutTradeNo(payOrderInfo.getOutTradeNo());                        // payOrderInfo.setTradeStatus(AliPayTradeStatusEnum.TRADE_CLOSED.name());                        //payOrderService.savePayOrderInfo(payOrderInfo);                    } else {                        log.error("【撤销交易 失败: msg: {} ,  subMsg {}】", cancelResponse.msg, cancelResponse.subMsg);                    }                } catch (Exception e) {                    e.printStackTrace();                }            } else {                log.info("【outTradeNo ready success finish 】");            }        }        log.info("【----------------------end  judgeCancel--------------------------】");    }

8.4 二维码 ConfigurationProperties 配置

@Data@ConfigurationProperties(prefix = "qr")public class AliPayTradeQrConfig {      //二维码过期分钟数    private int qrExpireMinute;      //二维码图片生成宽度    private int qrWidth;    //二维码图片生成高度    private int qrHeight;        //二维码图片生成后缀 PNG 。。    private String qrSuffix;}

8.5 AliPayConfigUtils 配置参数

接入准备工作 获取到的参数

APPID :通过创建应用后 应用头像下发的 202100xxxxx 这个就是APPID

应用私钥: 通过密钥在线生成器 可以直接生成

应用公钥: 通过密钥在线生成器 可以直接生成

支付宝公钥: 这个要注意,是通过 上面的应用公钥 去换取的 ,在应用 开发信息 -》 接口加签方式 -》设置

/** * @author johnny * @create 2021-04-26 1:03 上午 **/public class AliPayConfigUtils {    private static Config config = new Config();    public static void initConfig() {        config.protocol = "https";        config.gatewayHost = "openapi.alipay.com";        config.signType = "RSA2";        config.appId = "2021002141XXXXXXX";        // 为避免私钥随源码泄露,推荐从文件中读取私钥字符串而不是写入源码中        config.merchantPrivateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSXXXXXXX";        //注:证书文件路径支持设置为文件系统中的路径或CLASS_PATH中的路径,优先从文件系统中加载,加载失败后会继续尝试从CLASS_PATH中加载        //config.merchantCertPath = "<-- 请填写您的应用公钥证书文件路径,例如:/foo/appCertPublicKey_2019051064521003.crt -->";        //config.alipayCertPath = "<-- 请填写您的支付宝公钥证书文件路径,例如:/foo/alipayCertPublicKey_RSA2.crt -->";        //config.alipayRootCertPath = "<-- 请填写您的支付宝根证书文件路径,例如:/foo/alipayRootCert.crt -->";        //注:如果采用非证书模式,则无需赋值上面的三个证书路径,改为赋值如下的支付宝公钥字符串即可        config.alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKXXXXXXX;        //可设置异步通知接收服务地址(可选)        config.notifyUrl = "http://yanqqe.natappfree.cc/blogs/alipay/callback";        //可设置AES密钥,调用AES加解密相关接口时需要(可选)        config.encryptKey = "";    }    public static Config getOptions() {        return config;    }}

效果展示

前端代码就不展示了。。 拿出手机扫描即可 支付 。。

image-20210426232134924

总结

本篇主要讲解了 如何从头到尾接入 支付宝的当面付功能, 从创建应用 应用配置 签约 获取密钥参数等 到 使用新版SDK 调用 获取二维码 以及 异步通知接口的 代码编写逻辑 多看文档就好。

本文由博客一文多发平台 OpenWrite 发布!


版权声明:本文为qq_34285557原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_34285557/article/details/116520207