支付宝 当面付(扫描支付) 对接逻辑
这两天给网站 博客下方添加了 打赏功能 使用的是 支付宝的 当面付功能 特此记录一下,觉得不错的可以在下方打赏 嘿嘿 ,下面先来看一下效果图。
1.当面付产品介绍
本篇主要介绍 扫码支付
当面付帮助商家在线下消费场景中实现快速收款,支持 条码支付 和 扫码支付 两种付款方式。商家可通过以下两种任一方式进行收款,提升收银效率,实现资金实时到账
- 条码支付:买家出示支付宝钱包中的条码、二维码,商家扫描用户条码即可完成 条码支付 收款。
扫码支付
:买家通过使用支付宝 扫一扫 功能,扫描商家收款二维码即可完成 扫码支付 付款。
2.扫码支付应用场景
适用于单件商品单独定价、无人值守
、自助售货机 打赏
等商家。用户打开支付宝中的 扫一扫 功能,扫描商家展示的二维码进行支付。该模式适用于线下实体店支付、面对面支付、自助售货机等场景。
3.准入条件 (商家想用该功能的条件)
需要有营业执照和门头照片,门头照片可以网上随便找一个上传上去, 营业执照需要和商家账户名相同 ,本人是没有的 ,所以大概 30天后 就无法使用了。。
-
该能力对企业支付宝账户和个体工商户开放。
-
商家的收银系统需要有红外扫描枪设备,或其他的扫码、展示码设备。
-
签约申请提交资料:
-
经营场所照片。
- 有门头照的经营场所,需提供门头照。
- 无门头照的经营场所,需提供内景照或场景照。
-
同名的营业执照(即与支付宝账户认证名称一致)。
-
您可以在支付宝 PC 端登录 商家中心,进入 产品签约管理,通过资质凭证补全来恢复产品的正常使用。
账户类型 收款额度规则 入驻资质要求(页面需要填写) 使用时间 个人账户 单笔收款 ≤ 500元,单日收款 ≤ 5000元,不区分借记或贷记渠道。 1、需按规范提交经营场景照片(如门头照、门店地址);2、提供本人同名营业执照。 30天。如提交资料均不通过,商家需要在合约生效后的30天内补全门头照等资料,否则会影响正常收款
。温馨提示:当使用时间到期(从合约生效开始计算 30天)系统会在到期前 15 天和 3 天发出代办通知,30 天到期,为了更好的产品使用体验,建议您及时补全资质)。个人账户 收款不受限额。 1、需按规范提交经营场景照片(如门头照、门店地址);2、提供本人同名营业执照。 若提交资料均通过,收款不受限。 企业账户 收款不受限额。 无 无
-
-
- 若签约时提供了同名营业执照,或者签约后补充了同名营业执照,商家收款无限额。
4.接入准备
需要获取 以下参数供 后续调用接口:
APPID :通过创建应用后 应用头像下发的 202100xxxxx 这个就是APPID
应用私钥: 通过密钥在线生成器 可以直接生成
应用公钥: 通过密钥在线生成器 可以直接生成
支付宝公钥: 这个要注意,是通过 上面的应用公钥 去换取的 ,在应用 开发信息 -》 接口加签方式 -》设置
4.1 创建应用
登录 支付宝开放平台,创建应用并提交审核,审核通过后会生成应用唯一标识 APPID,并且可以申请开通开放产品使用权限。通过 APPID 应用才能调用开放产品的接口能力。详情请参考 创建应用。
4.2 配置应用
应用创建完成后,系统会自动跳转到应用详情页面。您可以在 能力列表 中点击 添加能力 来添加 当面付 功能
4.3 设置接口加签方式
在 应用 -》开发信息 -》 接口加签方式 -》设置
支付宝开放平台的应用管理体系,使用了公私钥的机制,商户可通过设置接口加签方式为自身应用配置 公私钥/公私钥证书 来保障商户应用和支付宝交互的安全性。密钥的获取方式请参见 生成密钥 可以选择Web在线生成方式。
公钥证书与公钥的区别请参见 普通公钥与公钥证书区别。
填入 上面在线生成的 公钥字符 点击保存设置
通过应用公钥 换取 支付宝公钥 这个后面调用接口是有用的
4.4 上线应用
商户在添加功能和配置密钥后,即可将应用提交审核,预计会有一个工作日的审核时间,请耐心等待,详细步骤可参考 上线应用。
应用上线后,还需要完成应用签约才能在线上环境(生产环境)使用当面付功能。
4.5 签约 当面付
这个也很重要 必须签约后 才能使用
当面付 需要签约 上传 门头照片 和 营业执照 ,门头照片可以网上随便找一个无水印的, 营业执照可以暂时不传,不传大概可以使用30天
签约完成后 应用部分会多出一个 应用2.0签约xxxxx 自动生成的
当上面的接入准备都做完后 即可获得 以下几个数据
APPID :通过创建应用后 应用头像下发的 202100xxxxx 这个就是APPID
应用私钥: 通过密钥在线生成器 可以直接生成
应用公钥: 通过密钥在线生成器 可以直接生成
支付宝公钥: 这个要注意,是通过 上面的应用公钥 去换取的 ,在应用 开发信息 -》 接口加签方式 -》设置
5.扫码支付 逻辑
来看看 支付宝提供的 接入流程图
开发者需要确认自己的应用在审核通过后显示 已上线,同时完成当面付功能的签约后,才能顺利调用以下接口。否则会有缺少权限的报错。
调用流程
- 后台 调用 支付宝的 预下单请求(可以设置二维码过期时间) 获取二维码
- 后台将二维码 生成图片 返回给 前端
- 用户进行扫码 支付完成后 可以通过配置 支付成功异步调用接口,或者后台自主轮训查询接口 来判断是否支付完成
- 如果二维码过期或者 在指定时间内 用户未支付 调用 cancel 接口去 撤销关闭交易
当面付推荐使用 自主轮训查询,我这里使用的异步通知接口 为了尝试看看
- 商户系统调用 alipay.trade.precreate(统一收单线下交易预创建)接口,获得该订单的二维码串 qr_code,开发者需要利用二维码生成工具获得最终的订单二维码图片;
- 发起轮询获得支付结果:等待 5 秒后调用 alipay.trade.query(统一收单线下交易查询)接口,通过支付时传入的商户订单号(out_trade_no)查询支付结果(返回参数 TRADE_STATUS),如果仍然返回等待用户付款(WAIT_BUYER_PAY),则再次等待 5 秒后继续查询,直到返回确切的支付结果(成功 TRADE_SUCCESS 或 已撤销关闭 TRADE_CLOSED),或是超出轮询时间。在最后一次查询仍然返回等待用户付款的情况下,必须立即调用 alipay.trade.cancel(统一收单交易撤销接口)将这笔交易撤销,避免用户继续支付;
- 除了主动轮询,当订单支付成功时,商户也可以通过设置异步通知(notify_url)来获得支付宝服务端返回的支付结果,详见 扫码异步通知,注意一定要对异步通知验签,确保通知是支付宝发出的。
6. 服务端 SDK (新版) 简述
支付宝提供了 2种 SDK 一个是老版本 一个是新版本,我把两个都试了下,这里我选用 新版来介绍和使用,新版主要使用 Factory 类
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.总金额校验
等等。。
异步返回结果验签
-
在通知返回参数列表中,除去 sign、sign_type 两个参数外,凡是通知返回回来的参数皆是待验签的参数;
-
将剩下参数进行 url_decode, 然后进行字典排序,组成字符串,得到待签名字符串:
gmt_create=2015-06-11 22:33:46&gmt_payment=2015-06-11 22:33:59¬ify_id=42af7baacd1d3746cf7b56752b91edcj34¬ify_time=2015-06-11 22:34:03¬ify_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
- 将签名参数(sign)使用 base64 解码为字节码串;
- 使用 RSA/RSA2 的验签方法,通过签名字符串、签名参数(经过 base64 解码)及支付宝公钥验证签名;
- 需要严格按照如下描述校验通知数据的正确性:
- 商户需要验证该通知数据中的 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; }}
效果展示
前端代码就不展示了。。 拿出手机扫描即可 支付 。。
总结
本篇主要讲解了 如何从头到尾接入 支付宝的当面付功能, 从创建应用 应用配置 签约 获取密钥参数等 到 使用新版SDK 调用 获取二维码 以及 异步通知接口的 代码编写逻辑 多看文档就好。
本文由博客一文多发平台 OpenWrite 发布!