目录
1.准备工作
2.idea配置文件准备
3.后端代码编写
接口1:支付订单
接口2:查询订单
接口3:订单退款
接口4:查询退款结果
接口5:获取总账单
接口6:取消订单
接口7:回调接口
定时任务:主动查询订单
4.前端代码编写
5.验证
支付订单
查询订单
订单退款
查询退款
取消订单
1.准备工作
进入沙箱控制台获取自己的买家卖家id、网关等配置信息
快速接入 - 支付宝文档中心 (alipay.com)
将沙箱工具下载本地,方便支付和调试,支付宝沙箱版
2.idea配置文件准备
properties设置自己沙箱的信息:8个值
#alipay 沙箱环境
#自己的appid 沙箱应用里面
alipay.app-id=xxx
#商户pid 沙箱账号里面
alipay.seller-id=xxx
#支付宝公钥 沙箱应用里面 公钥模式-》查看
alipay.alipay-public-key=xxx
#应用私钥 沙箱应用里面
alipay.merchant-private-key=xxx
#支付宝网关地址 沙箱应用里面
alipay.gateway-url= xxx
#接口内容加密方式 沙箱应用里面-》接口内容加密方式
alipay.content-key=xxx
#支付回调返回地址 如果自己有页面就写,没的话就返回百度
alipay.return-url = https://www.baidu.com
#支付回调公网地址+接口 这个需要通过ngrok穿透,让自己本地的项目映射到公网 这里不多讲解
alipay.notify-url = xxx
3.后端代码编写
controller层:接口总共7个接口,其中有一个是支付宝回调接口,就是你支付完成后,支付宝调用你提供的接口给你传回调消息,所以需要你这个接口是能够在公网访问的接口,因此对于本地环境来说需要内网穿透。
接口1:支付订单
根据前端选择的商品id/编号,传入后台后,调用AlipayTradePagePayModel进行参数设置,最后通过AlipayTradePagePayRequest,设置1支付成功后的回调地址我写的百度2回调在哪个接口暴漏公网的接口,然后执行pageExecute完成签名执行请求
实现代码如下:
impl层代码
public String createPay(Long orderId) {
//1.请求
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
//2.设置数据
AlipayTradePagePayModel bizModel = new AlipayTradePagePayModel();
// 商品编号
bizModel.setOutTradeNo(orderId.toString());
//单位是元
bizModel.setTotalAmount(String.valueOf(0.01));
// 订单标题
bizModel.setSubject("测试商品");
//默认的
bizModel.setProductCode("FAST_INSTANT_TRADE_PAY");
//3.绑定
request.setBizModel(bizModel);
// 支付成功后返回哪里
request.setReturnUrl(returnUrl);
// 结果回调地址
request.setNotifyUrl(notifyUrl);
//用户支付后支付宝会以GET方法请求returnUrl,并且携带out_trade_no,trade_no,total_amount等参数.
AlipayTradePagePayResponse response = null;
try {
//完成签名并执行请求
response = alipayClient.pageExecute(request);
if (response.isSuccess()) {
log.debug("调用成功,参数为===>{}",JSON.toJSONString(response.getBody()));
return response.getBody();
} else {
log.error("调用失败");
log.error(response.getMsg());
return null;
}
} catch (AlipayApiException e) {
log.error("调用异常");
return null;
}
}
支付回调接口
/**
* 支付宝回调
* 发起支付后,商户进行的验证和保存记录等操作
* 这个接口地址是根据配置文件进行配置的
* @param params 支付宝返回的
* @return 返回给支付宝的 只有两种状态 success failure
*/
@PostMapping("/tradeNotify")
public String tradeNotify(@RequestParam Map<String, String> params) {
// 支付通知正在执行
// 通知参数===>
// {
// "gmt_create":"2023-11-01 09:59:50",
// "charset":"UTF-8",
// "gmt_payment":"2023-11-01 10:00:15",
// "notify_time":"2023-11-01 10:00:17",
// "subject":"测试商品",
// "sign":"f5/aoi4fNs+DZRuyqFr6uU1J6l6sImbZLZzJvYl76tDJFRW+gv3Ewk2DW6EemdD9zNt0QNpagfp3IS0CVDKnTrVly4aA/QehNQ9f6Ru9kNU9lqRhc/GRx2ikuQgYw7MUeoMLXNSL5xh9G09bVFBwl7iYa/I2fh8FgFQTyDgjUVjsFen7Kokt70DNi1KIWyuD7qLCMu7SRYP0NtNp6kA1AoRhx6zpu2MOCqRVlsMeQyYB5fbj0sWJcWogWBYcUuzTZrLE0X/lc7a8hMYw63IhBag47L9sbtxcZfOIq1Sd7/L20fmaPLl0PZllbILad+O6uIRXBRC5PvZa/t9IN2A2gw==",
// "buyer_id":"2088722019936375",
// "invoice_amount":"0.01",
// "version":"1.0",
// "notify_id":"2023110101222100016036370501117115",
// "fund_bill_list":"[{\"amount\":\"0.01\",\"fundChannel\":\"ALIPAYACCOUNT\"}]",
// "notify_type":"trade_status_sync",
// "out_trade_no":"486789",
// "total_amount":"0.01","
// trade_status":"TRADE_SUCCESS",
// "trade_no":"2023110122001436370501040720",
// "auth_app_id":"9021000131620971",
// "receipt_amount":"0.01",
// "point_amount":"0.00",
// "buyer_pay_amount":"0.01",
// "app_id":"9021000131620971",
// "sign_type":"RSA2",
// "seller_id":"2088721019958624"
// }
log.info("支付回调正在执行");
log.info("支付宝回调参数为===>{}", JSON.toJSONString(params));
//验签
boolean signVerified = false;
String result = "failure";
try {
// 入参
signVerified = AlipaySignature.rsaCheckV1(params, aliPayPublicKey, AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);
//验签成功
if (signVerified) {
log.info("验签成功");
// 1.商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号 数据库查询这条记录是否存在?在生成订单的时候创建订单
String out_trade_no = params.get("out_trade_no");
// 根据订单号查询这条记录,返回order,如果不存在返回result(failure)
// 2.商户需要验证该通知数据中的total_amount是否为该订单的实际金额(即商户订单创建时的金额)
String totalAmount = params.get("total_amount");
int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
// 将order中的价格拿出来跟这个作比较,若不同返回result(failure)
// 3. 校验通知中的seller_id(或者sell_email)是否为out_trade_no 这笔单据的对应的操作方法 需要保持与商户pid一致
String sellerId = params.get("seller_id");
if (!sellerId.equals(aliPaySellerId)){
log.error("商家pid校验失败");
return result;
}
// 4. 验证app_id 是否为商户本身
String appId = params.get("app_id");
if (!appId.equals(aliPayAppId)){
log.error("appId校验失败");
return result;
}
// 5. 验证交易通知是否为TRADE_SUCCESS,只有为TRADE_SUCCESS支付宝才会认定买家付款成功
String tradeStatus = params.get("trade_status");
if (!"TRADE_SUCCESS".equals(tradeStatus)){
log.error("支付未成功");
return result;
}
// 处理订单后续
log.info("5大校验验证通过,开始对回调函数进行处理");
aliPayService.processOrder(params);
result = "success";
return result;
} else {
log.error("验签失败");
return result;
}
} catch (AlipayApiException e) {
log.error("验签异常");
return result;
}
}
impl
@Override
public void processOrder(Map<String, String> params) {
String out_trade_no = params.get("out_trade_no");
//支付宝支付中的支付编号
String trade_no = params.get("trade_no");
//交易类型(扫码 登录等等)
String trade_status = params.get("trade_status");
//存放全部数据(json)以备不时之需
String s = JSON.toJSONString(params);
// 将订单状态查询出来,如果是未支付就继续执行下面操作 仅仅只有未支付才进行下面操作
// 如果已经支付就return
if (lock.tryLock()) {
try {
log.info("幂等性校验开始");
// if (!"未支付".equals("调用数据库查询")) {
// return;
// }
// 更新订单状态
// 记录支付日志
log.info("幂等性校验结束");
log.info("订单{}的支付日志已添加,状态已更改为已支付,支付宝记录编号为{}", out_trade_no, trade_no); //订单789454的支付记录添加成功,支付记录id为2023110122001436370501040721.
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
接口2:查询订单
前端传入商品id/编号,后端封装数据使用AlipayTradeQueryModel,用AlipayTradeQueryRequest发起请求。
实现代码如下:
@Override
public String queryPay(String orderNo) {
//请求
AlipayTradeQueryRequest request=new AlipayTradeQueryRequest();
//数据
AlipayTradeQueryModel bizModel=new AlipayTradeQueryModel();
bizModel.setOutTradeNo(orderNo);
request.setBizModel(bizModel);
try{
//完成签名并执行请求
AlipayTradeQueryResponse response=alipayClient.execute(request);
if(response.isSuccess()){
log.info("查询订单{}成功",orderNo);
return response.getBody();
}
else{
log.error("查询订单{}失败,响应数据是{}.",orderNo,response.getBody());
return null;
}
}
catch(AlipayApiException e){
log.error("查询订单{}异常",orderNo);
return null;
}
}
接口3:订单退款
传入两个参数,商品id/编号和退款原因
需要注意点是如果封装数据时传入了退款订单号(自己设置的),那么在后面查询订单退款信息时也需要传入这个退款订单号,如果这里没有设置,后面的退款订单号就是商品id/编号
实现代码如下:
@Override
public void refund(String orderNo, String reason) {
//请求
AlipayTradeRefundRequest request=new AlipayTradeRefundRequest();
//数据
AlipayTradeRefundModel bizModel=new AlipayTradeRefundModel();
//订单号
bizModel.setOutTradeNo(orderNo);
// 退款金额
bizModel.setRefundAmount("0.01");
//退款原因
bizModel.setRefundReason(reason);
request.setBizModel(bizModel);
log.info("签名入参===>{}",JSON.toJSONString(request));
try{
//完成签名并执行请求
AlipayTradeRefundResponse response=alipayClient.execute(request);
//成功则说明退款成功了
if(response.isSuccess()){
log.info("订单{}退款成功",orderNo);
}
else{
log.error("订单{}退款失败,错误原因===>{}",orderNo,response.getSubMsg());
throw new RuntimeException("订单退款失败");
}
}
catch(AlipayApiException e){
log.error("订单{}退款异常",orderNo);
throw new RuntimeException("订单退款异常");
}
}
接口4:查询退款结果
前端传入订单编号,后端进行查询,注意点就是刚才所说退款订单号问题,如果退款时没传退款订单号,这里就传订单号
实现代码如下:
@Override
public String queryRefund(String orderNo) {
AlipayTradeFastpayRefundQueryRequest request=new AlipayTradeFastpayRefundQueryRequest();
AlipayTradeFastpayRefundQueryModel bizModel=new AlipayTradeFastpayRefundQueryModel();
//订单号
bizModel.setOutTradeNo(orderNo);
// 退款订单号,如果退款时候没有传退款订单号,那么查询时就传订单号
bizModel.setOutRequestNo(orderNo);
//想要额外返回的数据(也就是文档中响应可选的数据)
ArrayList<String> extraResponseDatas=new ArrayList<>();
extraResponseDatas.add("refund_status");
bizModel.setQueryOptions(extraResponseDatas);
request.setBizModel(bizModel);
try{
//完成签名并执行请求
AlipayTradeFastpayRefundQueryResponse response=alipayClient.execute(request);
if(response.isSuccess()){
log.info("退款{}查询成功",orderNo);
return JSON.toJSONString(response.getBody());
}
else{
log.debug("退款{}查询失败,原因是==>{}",orderNo,response.getSubMsg());
return null;
}
}
catch(AlipayApiException e){
log.debug("退款{}查询异常",orderNo);
return null;
}
}
接口5:获取总账单
根据账单类型和日期获取账单url地址,这里出参是url,放在浏览器直接下载
实现代码如下:
@Override
public String queryBill(String billDate, String type) {
//请求
AlipayDataDataserviceBillDownloadurlQueryRequest request=new AlipayDataDataserviceBillDownloadurlQueryRequest();
//数据
AlipayDataDataserviceBillDownloadurlQueryModel bizModel=new AlipayDataDataserviceBillDownloadurlQueryModel();
bizModel.setBillType(type);
bizModel.setBillDate(billDate);
request.setBizModel(bizModel);
try{
//完成签名并执行请求
AlipayDataDataserviceBillDownloadurlQueryResponse response=alipayClient.execute(request);
if(response.isSuccess()){
log.info("获取账单下载url成功");
return response.getBillDownloadUrl();
}
else{
log.error("获取账单下载url失败,原因是===>{}",response.getSubMsg());
return null;
}
}
catch(AlipayApiException e){
log.error("获取账单下载url异常");
return null;
}
}
接口6:取消订单
传入商品id/编号,后端封装数据,完成签名后进行取消
实现代码如下:
private void closePay(String orderNo) {
log.info("关单接口的订单号,订单号===>{}",JSON.toJSONString(orderNo));
//请求
AlipayTradeCloseRequest request=new AlipayTradeCloseRequest();
//数据
AlipayTradeCloseModel bizModel=new AlipayTradeCloseModel();
bizModel.setOutTradeNo(orderNo);
request.setBizModel(bizModel);
try{
//完成签名并执行请求
AlipayTradeCloseResponse response=alipayClient.execute(request);
if(response.isSuccess()){
log.info("订单{}取消成功",orderNo);
}
else{
log.info("订单{}取消失败,原因==>{}",orderNo,response.getSubMsg());
throw new RuntimeException("关单接口调用失败");
}
}
catch(AlipayApiException e){
log.error("订单{}取消异常",orderNo);
throw new RuntimeException("关单接口异常");
}
}
接口7:回调接口
当发起支付后,支付宝会调该接口进行数据返回,需要验证签名和5个参数
@PostMapping("/tradeNotify")
public String tradeNotify(@RequestParam Map<String, String> params) {
// 支付通知正在执行
// 通知参数===>
// {
// "gmt_create":"2023-11-01 09:59:50",
// "charset":"UTF-8",
// "gmt_payment":"2023-11-01 10:00:15",
// "notify_time":"2023-11-01 10:00:17",
// "subject":"测试商品",
// "sign":"f5/aoi4fNs+DZRuyqFr6uU1J6l6sImbZLZzJvYl76tDJFRW+gv3Ewk2DW6EemdD9zNt0QNpagfp3IS0CVDKnTrVly4aA/QehNQ9f6Ru9kNU9lqRhc/GRx2ikuQgYw7MUeoMLXNSL5xh9G09bVFBwl7iYa/I2fh8FgFQTyDgjUVjsFen7Kokt70DNi1KIWyuD7qLCMu7SRYP0NtNp6kA1AoRhx6zpu2MOCqRVlsMeQyYB5fbj0sWJcWogWBYcUuzTZrLE0X/lc7a8hMYw63IhBag47L9sbtxcZfOIq1Sd7/L20fmaPLl0PZllbILad+O6uIRXBRC5PvZa/t9IN2A2gw==",
// "buyer_id":"2088722019936375",
// "invoice_amount":"0.01",
// "version":"1.0",
// "notify_id":"2023110101222100016036370501117115",
// "fund_bill_list":"[{\"amount\":\"0.01\",\"fundChannel\":\"ALIPAYACCOUNT\"}]",
// "notify_type":"trade_status_sync",
// "out_trade_no":"486789",
// "total_amount":"0.01","
// trade_status":"TRADE_SUCCESS",
// "trade_no":"2023110122001436370501040720",
// "auth_app_id":"9021000131620971",
// "receipt_amount":"0.01",
// "point_amount":"0.00",
// "buyer_pay_amount":"0.01",
// "app_id":"9021000131620971",
// "sign_type":"RSA2",
// "seller_id":"2088721019958624"
// }
log.info("支付回调正在执行");
log.info("支付宝回调参数为===>{}", JSON.toJSONString(params));
//验签
boolean signVerified = false;
String result = "failure";
try {
// 入参
signVerified = AlipaySignature.rsaCheckV1(params, aliPayPublicKey, AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);
//验签成功
if (signVerified) {
log.info("验签成功");
// 1.商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号 数据库查询这条记录是否存在?在生成订单的时候创建订单
String out_trade_no = params.get("out_trade_no");
// 根据订单号查询这条记录,返回order,如果不存在返回result(failure)
// 2.商户需要验证该通知数据中的total_amount是否为该订单的实际金额(即商户订单创建时的金额)
String totalAmount = params.get("total_amount");
int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
// 将order中的价格拿出来跟这个作比较,若不同返回result(failure)
// 3. 校验通知中的seller_id(或者sell_email)是否为out_trade_no 这笔单据的对应的操作方法 需要保持与商户pid一致
String sellerId = params.get("seller_id");
if (!sellerId.equals(aliPaySellerId)){
log.error("商家pid校验失败");
return result;
}
// 4. 验证app_id 是否为商户本身
String appId = params.get("app_id");
if (!appId.equals(aliPayAppId)){
log.error("appId校验失败");
return result;
}
// 5. 验证交易通知是否为TRADE_SUCCESS,只有为TRADE_SUCCESS支付宝才会认定买家付款成功
String tradeStatus = params.get("trade_status");
if (!"TRADE_SUCCESS".equals(tradeStatus)){
log.error("支付未成功");
return result;
}
// 处理订单后续
log.info("5大校验验证通过,开始对回调函数进行处理");
// 进行自己的业务操作
aliPayService.processOrder(params);
result = "success";
return result;
} else {
log.error("验签失败");
return result;
}
} catch (AlipayApiException e) {
log.error("验签异常");
return result;
}
}
定时任务:主动查询订单
除了回调这个被动查询外,我们也可以通过定时任务来进行定时查询表中未支付的订单和支付宝中的状态,来保证一致性。
定时把表中未支付的订单拿出来,进行查询
/**
* 从第0秒开始每隔30s执行一次,查询创建超过5分钟且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm(){
log.info("orderConfirm被执行...");
// 1.查询本地未支付的记录,这里写死了
String orderNo = "9956851223";
// 2.核实订单状态,调用支付宝查单接口
aliPayService.checkPayStatus(orderNo);
}
/**
* 查询支付宝订单状态
* @param orderNo
*/
@Override
public void checkPayStatus(String orderNo) {
log.info("根据订单号核实订单状态=====>{}",JSON.toJSONString(orderNo));
// 查询支付宝这个订单的状态
String result = this.queryPay(orderNo);
// 1.如果未创建
if (result == null){
log.info("订单未创建===>{}",JSON.toJSONString(orderNo));
// 更新订单状态
return;
}
System.out.println("result"+result);
// 获取订单状态
String alipayTradeQueryResponse1 = JSON.parseObject(result).get("alipay_trade_query_response").toString();
String tradeStatus = JSON.parseObject(alipayTradeQueryResponse1).get("trade_status").toString();
// 2.订单状态如果是未支付
if (AliPayTradeState.NOTYPE.getType().equals(tradeStatus)){
log.info("订单未支付===>{}",JSON.toJSONString(orderNo));
// 调用关单接口
this.closePay(orderNo);
// 更新订单状态
}
// 3.订单状态如果是已支付
if (AliPayTradeState.SUCCESS.getType().equals(tradeStatus)){
log.info("订单已支付===>{}",JSON.toJSONString(orderNo));
// 更新商户订单状态
// 记录支付日志
}
// 4. 订单状态如果是已关闭
if (AliPayTradeState.CLOSED.getType().equals(tradeStatus)){
log.info("订单已关闭===>{}",JSON.toJSONString(orderNo));
// 更新商户订单状态
// 记录支付日志
}
}
public enum AliPayTradeState {
/**
* 支付成功
*/
SUCCESS("TRADE_SUCCESS"),
/**
* 未支付
*/
NOTYPE("WAIT_BUYER_PAY"),
/**
* 已关闭
*/
CLOSED("TRADE_CLOSED");
private String type;
}
4.前端代码编写
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<title>Login</title>
<script src="js/jquery-1.8.3.min.js"></script>
</head>
<script>
function zhifu(){
$.ajax({
url:"http://localhost:8080/pay/createPay/995685122334",
type:"post",
success: function(data) {
// console.log(data)
document.write(data.body)
}
})
}
</script>
<script>
function guanbi(){
$.ajax({
url:"http://localhost:8080/pay/cancelPay/995685122334",
type:"post",
success: function(data) {
alert(data.msg)
}
})
}
</script>
<script>
function tuikuan(){
$.ajax({
url:"http://localhost:8080/pay/refunds/995685122334/不想要了",
type:"post",
success: function(data) {
alert(data.msg)
}
})
}
</script>
<script>
function chaxuntuikuan(){
$.ajax({
url:"http://localhost:8080/pay/queryRefund/995685122334",
type:"post",
success: function(data) {
console.log(data)
}
})
}
</script>
<script>
function chaxun(){
$.ajax({
url:"http://localhost:8080/pay/query/995685122334",
type:"get",
success: function(data) {
console.log(data)
}
})
}
</script>
<script>
function duizhangliushui(){
$.ajax({
url:"http://localhost:8080/pay/downloadurl/query/2023-10-31/trade",
type:"get",
success: function(data) {
console.log(data.body)
// alert(data.body)
}
})
}
</script>
<body>
<div style="margin: 0 auto; width: 600px; height: 600px; text-align: center; margin-top: 300px; display: flex;">
<div style="width:50px; height:50px;left: auto;">
<button onclick="zhifu()">支付</button>
</div>
<div style="width:100px; height:50px;left: auto;">
<button onclick="chaxun()">查询订单</button>
</div>
<div style="width:50px; height: 50px;left: auto;">
<button onclick="tuikuan()">退款</button>
</div>
<div style="width:100px; height: 50px;left: auto;">
<button onclick="guanbi()">取消订单</button>
</div>
<div style="width:100px; height: 50px;left: auto;">
<button onclick="chaxuntuikuan()">查询退款</button>
</div>
<div style="width:100px; height: 50px;left: auto;">
<button onclick="duizhangliushui()">对账流水</button>
</div>
</div>
</body>
</html>
5.验证
支付订单
点击支付按钮
手机沙箱版支付宝扫码支付。
然后前端页面就跳转到百度了,因为设置回调跳转到百度
查询订单
订单退款
查询退款
取消订单
重新下一单,但是扫码后不付款