上一篇 SpringBoot + Vue 结合支付宝支付(2)-- 项目搭建
项目 demo 地址:https://gitee.com/manster1231
首先我们将我们的 阿里支付 配置文件引入到项目中 resources 目录下,然后我们为其创建配置类
package com.manster.pay.config;
import com.alipay.api.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import javax.annotation.Resource;
/**
* @Author manster
* @Date 2022/6/4
**/
@Configuration
@PropertySource("classpath:alipay-sandbox.properties")
public class AlipayClientConfig {
}
首先我们先对其进行配置的测试,新建一个测试类进行测试
package com.manster.pay;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import javax.annotation.Resource;
/**
* @Author manster
* @Date 2022/6/4
**/
@SpringBootTest
@Slf4j
public class AlipayTests {
@Resource
private Environment config;
@Test
public void testAlipayConfig(){
log.info(config.getProperty("alipay.app-id"));
}
}
然后我们导入 alipay 的 jar 包
<dependency>
<groupId>com.alipay.sdkgroupId>
<artifactId>alipay-sdk-javaartifactId>
<version>4.27.1.ALLversion>
dependency>
最后我们使用支付宝 SDK 签名进行验签,我们根据文档对其进行配置
package com.manster.pay.config;
import com.alipay.api.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import javax.annotation.Resource;
/**
* @Author manster
* @Date 2022/6/4
**/
@Configuration
@PropertySource("classpath:alipay-sandbox.properties")
public class AlipayClientConfig {
@Resource
private Environment config;
@Bean
public AlipayClient alipayClient() throws AlipayApiException {
AlipayConfig alipayConfig = new AlipayConfig();
//设置网关地址
alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
//设置应用ID
alipayConfig.setAppId(config.getProperty("alipay.app-id"));
//设置应用私钥
alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
//设置请求格式,固定值json
alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
//设置字符集
alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
//设置支付宝公钥
alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
//设置签名类型
alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
//构造client
AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
return alipayClient;
}
}
沙箱接入注意事项
- 电脑网站支付支持沙箱接入;在沙箱调通接口后,必须在线上进行测试与验收,所有返回码及业务逻辑以线上为准。
- 电脑网站支付只支持余额支付,不支持银行卡、余额宝等其他支付方式。
- 支付时,请使用沙箱买家账号支付。
- 如果扫二维码付款时,请使用沙箱支付宝客户端扫码付款。
Alipay API https://opendocs.alipay.com/open/028r8t?scene=22
电脑网站支付的支付接口 alipay.trade.page.pay(统一收单下单并支付页面接口)调用时序图如下:
调用流程如下:
注意:
package com.manster.pay.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import sun.net.spi.nameservice.dns.DNSNameServiceDescriptor;
/**
* @Author manster
* @Date 2022/6/5
**/
@AllArgsConstructor
@Getter
public enum OrderStatus {
/**
* 未支付
*/
NOTPAY("未支付"),
/**
* 支付成功
*/
SUCCESS("支付成功"),
/**
* 已关闭
*/
CLOSED("超时已关闭"),
/**
* 已取消
*/
CANCEL("用户已取消"),
/**
* 退款中
*/
REFUND_PROCESSING("退款中"),
/**
* 已退款
*/
REFUND_SUCCESS("已退款"),
/**
* 退款异常
*/
REFUND_ABNORMAL("退款异常");
private final String type;
}
package com.manster.pay.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @Author manster
* @Date 2022/6/5
**/
@AllArgsConstructor
@Getter
public enum PayType {
/**
* 微信
*/
WXPAY("微信"),
/**
* 支付宝
*/
ALIPAY("支付宝");
private final String type;
}
https://opendocs.alipay.com/open/028r8t?scene=22
前端点击下单
然后会调用后端请求统一收单下单并支付页面接口
支付宝接口返回表单
后端将表单返回给前端
前端直接执行表单提交到支付宝
支付宝就会展示支付页面(扫码,或者登陆)
我们先整理一下前端的思路
首先我们编写前端页面代码
课程列表
选择支付方式
确认支付(支付宝和微信V3)
确认支付(微信V2)
使用微信扫码支付
其中 aliPay.js 中的接口
// axios 发送ajax请求
import request from '@/utils/request'
export default{
//发起支付请求
tradePagePay(productId) {
return request({
url: '/api/ali-pay/trade/page/pay/' + productId,
method: 'post'
})
}
}
然后我们编写支付接口
package com.manster.pay.controller;
import com.manster.pay.service.AliPayService;
import com.manster.pay.util.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* @Author manster
* @Date 2022/6/5
**/
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
@ApiOperation("统一收单下单并支付页面接口")
@PostMapping("/trade/page/pay/{productId}")
public R tradePagePay(@PathVariable("productId") Long productId){
log.info("统一收单下单并支付页面接口调用");
//请求支付页面接口返回表单
String formStr = aliPayService.tradeCreate(productId);
//将form表单脚本返回前端,前端自动提交跳转支付页面
return R.ok().data("formStr", formStr);
}
}
我们对订单进行创建,并携带支付后的跳转路径
package com.manster.pay.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.manster.pay.entity.OrderInfo;
import com.manster.pay.service.AliPayService;
import com.manster.pay.service.OrderInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
/**
* @Author manster
* @Date 2022/6/5
**/
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private Environment config;
@Transactional(rollbackFor = Exception.class)
@Override
public String tradeCreate(Long productId) {
try {
//创建订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());
//调用支付宝接口
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
//配置需要的公共请求参数
//设置支付成功消息异步通知接口
request.setNotifyUrl(config.getProperty("alipay.notify-url"));
//设置支付完成的返回页面
request.setReturnUrl(config.getProperty("alipay.return-url"));
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderInfo.getOrderNo());
BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal(100));
bizContent.put("total_amount", total);
bizContent.put("subject", orderInfo.getTitle());
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toString());
//发送请求,调用支付宝
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if(response.isSuccess()){
log.info("调用成功 ===>" + response.getBody());
return response.getBody();
} else {
log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());
throw new RuntimeException("创建支付交易失败");
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("创建支付交易失败");
}
}
}
我们需要在支付宝端完成支付的情况下,将订单的状态修改为已支付,所以我们需要异步通知(在成功支付后支付宝向我们发送请求),注意:此处我们需要使用到内网穿透
我们需要在创建支付宝请求对象时就将我们的异步通知返回接口写好
对于 PC 网站支付的交易,在用户支付完成之后,支付宝会根据 API 中商户传入的 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商户系统。
//设置支付成功消息异步通知接口
request.setNotifyUrl(config.getProperty("alipay.notify-url"));
然后我们根据支付宝官方的建议编写对应的接口来修改订单状态(根据我们的封装我们只需要进行第五步的校验即可)
某商户设置的通知地址为 https://商家网站通知地址,对应接收到通知的示例如下:
https: //商家网站通知地址?voucher_detail_list=[{"amount":"0.20","merchantContribute":"0.00","name":"5折券","otherContribute":"0.20","type":"ALIPAY_DISCOUNT_VOUCHER","voucherId":"2016101200073002586200003BQ4"}]&fund_bill_list=[{"amount":"0.80","fundChannel":"ALIPAYACCOUNT"},{"amount":"0.20","fundChannel":"MDISCOUNT"}]&subject=PC网站支付交易&trade_no=2016101221001004580200203978&gmt_create=2016-10-12 21:36:12¬ify_type=trade_status_sync&total_amount=1.00&out_trade_no=mobile_rdm862016-10-12213600&invoice_amount=0.80&seller_id=2088201909970555¬ify_time=2016-10-12 21:41:23&trade_status=TRADE_SUCCESS&gmt_payment=2016-10-12 21:37:19&receipt_amount=0.80&passback_params=passback_params123&buyer_id=2088102114562585&app_id=2016092101248425¬ify_id=7676a2e1e4e737cff30015c4b7b55e3kh6& sign_type=RSA2&buyer_pay_amount=0.80&sign=***&point_amount=0.00
第一步: 在通知返回参数列表中,除去 sign、sign_type 两个参数外,凡是通知返回回来的参数皆是待验签的参数。
第二步: 将剩下参数进行 url_decode,然后进行字典排序,组成字符串,得到待签名字符串:
app_id=2016092101248425&buyer_id=2088102114562585&buyer_pay_amount=0.80&fund_bill_list=[{"amount":"0.80","fundChannel":"ALIPAYACCOUNT"},{"amount":"0.20","fundChannel":"MDISCOUNT"}]&gmt_create=2016-10-12 21:36:12&gmt_payment=2016-10-12 21:37:19&invoice_amount=0.80¬ify_id=7676a2e1e4e737cff30015c4b7b55e3kh6¬ify_time=2016-10-12 21:41:23¬ify_type=trade_status_sync&out_trade_no=mobile_rdm862016-10-12213600&passback_params=passback_params123&point_amount=0.00&receipt_amount=0.80&seller_id=2088201909970555&subject=PC网站支付交易&total_amount=1.00&trade_no=2016101221001004580200203978&trade_status=TRADE_SUCCESS&voucher_detail_list=[{"amount":"0.20","merchantContribute":"0.00","name":"5折券","otherContribute":"0.20","type":"ALIPAY_DISCOUNT_VOUCHER","voucherId":"2016101200073002586200003BQ4"}]
第三步: 将签名参数(sign)使用 base64 解码为字节码串。
第四步: 使用 RSA 的验签方法,通过签名字符串、签名参数(经过 base64 解码)及支付宝公钥验证签名。
第五步:需要严格按照如下描述校验通知数据的正确性:
- 商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号;
- 判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额);
- 校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方(有的时候,一个商户可能有多个 seller_id/seller_email);
- 验证 app_id 是否为该商户本身。
上述 1、2、3、4 有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。 在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。
注意:
- 状态 TRADE_SUCCESS 的通知触发条件是商户签约的产品支持退款功能的前提下,买家付款成功;
- 交易状态 TRADE_FINISHED 的通知触发条件是商户签约的产品不支持退款功能的前提下,买家付款成功;或者,商户签约的产品支持退款功能的前提下,交易已经成功并且已经超过可退款期限。
@ApiOperation("支付通知")
@PostMapping("/trade/notify")
public String tradeNotify(@RequestParam Map<String, String> params) {
log.info("========支付通知=========");
String result = "failure";
try {
//异步通知验签
boolean signVerified = AlipaySignature.rsaCheckV1(
params,
config.getProperty("alipay.alipay-public-key"),
AlipayConstants.CHARSET_UTF8,
AlipayConstants.SIGN_TYPE_RSA2); //调用SDK验证签名
if(!signVerified) {
//验签失败则记录异常日志,并在response中返回failure.
log.error("支付成功异步验签失败");
return result;
}
//验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,
log.info("支付成功异步验签失败");
//1.商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号
String outTradeNo = params.get("out_trade_no");
OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
if(order == null){
log.error("订单不存在");
return result;
}
//2.判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
String totalAmount = params.get("total_amount");
int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal(100)).intValue();
int totalFeeInt = order.getTotalFee().intValue();
if(totalAmountInt != totalFeeInt){
log.error("金额校验失败");
return result;
}
//3.校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的
// 操作方(有的时候,一个商户可能有多个 seller_id/seller_email)
String sellerId = params.get("seller_id");
String sellerIdProperty = config.getProperty("alipay.seller-id");
if(!sellerId.equals(sellerIdProperty)){
log.error("商家pid校验失败");
return result;
}
//4.验证 app_id 是否为该商户本身。
String appId = params.get("app_id");
String appIdProperty = config.getProperty("alipay.app-id");
if(!appId.equals(appIdProperty)){
log.error("app-id校验失败");
return result;
}
//在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS支付宝才会认定为买家付款成功。
String tradeStatus = params.get("trade_status");
if(!"TRADE_SUCCESS".equals(tradeStatus)){
log.error("支付未成功");
return result;
}
//处理业务。修改订单状态,记录支付日志
aliPayService.processOrder(params);
//校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
//向支付宝反馈,否则会不断发送通知(25小时内8次),对此我们处理重复通知
result = "success";
} catch (AlipayApiException e) {
e.printStackTrace();
}
return result;
}
其中我们的业务处理 processOrder() 进行方法的封装
private final ReentrantLock lock = new ReentrantLock();
/**
* 处理订单
* @param params
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, String> params) {
//获取订单号
String orderNo = params.get("out_trade_no");
//避免同时多个线程进来,都检测到是NOTPAY状态后,都要执行记录日志
if(lock.tryLock()){
try{
//处理重复通知
//接口幂等性:无论接口调用多少次,以下只执行一次
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfoForAliPay(params);
}finally {
//主动释放锁
lock.unlock();
}
}
}
记录支付日志
package com.manster.pay.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.manster.pay.entity.PaymentInfo;
import com.manster.pay.enums.PayType;
import com.manster.pay.mapper.PaymentInfoMapper;
import com.manster.pay.service.PaymentInfoService;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
/**
* @Author manster
* @Date 2022/6/4
**/
@Service
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
/**
* 支付宝记录支付日志
* @param params
*/
@Override
public void createPaymentInfoForAliPay(Map<String, String> params) {
//获取订单号
String orderNo = params.get("out_trade_no");
//支付系统交易编号
String transactionId = params.get("trade_no");
//交易状态
String tradeStatus = params.get("trade_status");
//交易金额
String totalAmount = params.get("total_amount");
int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal(100)).intValue();
PaymentInfo paymentInfo = new PaymentInfo();
paymentInfo.setOrderNo(orderNo);
paymentInfo.setPaymentType(PayType.ALIPAY.getType());
paymentInfo.setTransactionId(transactionId);
paymentInfo.setTradeType("电脑网站支付");
paymentInfo.setTradeState(tradeStatus);
paymentInfo.setPayerTotal(totalAmountInt);
Gson gson = new Gson();
String json = gson.toJson(params, HashMap.class);
paymentInfo.setContent(json);
baseMapper.insert(paymentInfo);
}
}
https://opendocs.alipay.com/open/028wob
通常交易关闭是通过 alipay.trade.page.pay 中的超时时间来控制,支付宝也提供给商户 alipay.trade.close(统一收单交易关闭接口)。若用户一直未支付,商户可以调用该接口关闭指定交易;成功关闭交易后该交易不可支付。
交易关闭接口的调用时序图 alipay.trade.close(统一收单交易关闭接口)如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xxWtfhhB-1654512260847)(http://mdn.alipayobjects.com/afts/img/A*HkUJSLhOKjYAAAAAAAAAAAAAAa8wAA/original?bz=openpt_doc&t=MdzERc-hiSLTtwmo1N82cAAAAABkMK8AAAAA)]
此过程中可能会产生 “交易不存在” 的错误,这是因为在沙箱支付时,只有我们使用用户名密码登录成功或者手机扫码成功之后,支付宝才会创建这个订单,我们只是到了支付页面没有进行操作,在支付宝方该订单就是不存在的
首先我们创建取消订单的接口
@ApiOperation("用户取消订单")
@PostMapping("/trade/close/{orderNo}")
public R cancel(@PathVariable String orderNo) {
log.info("取消订单");
aliPayService.cancelOrder(orderNo);
return R.ok().setMessage("订单已取消");
}
然后我们实现取消订单
/**
* 取消订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) {
//调用支付宝提供的统一收单关闭
this.closeOrder(orderNo);
//更新用户订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
}
/**
* 用支付宝提供的统一收单关闭
* @param orderNo 订单号
*/
private void closeOrder(String orderNo) {
try {
log.info("关单接口调用,订单号==> {}", orderNo);
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeCloseResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功 ===>" + response.getBody());
} else {
log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("关单接口调用失败");
}
}
https://opendocs.alipay.com/open/028woa
可能在网络通信的过程中,支付宝方已经完成了交易,但是返回信息给我们的时候出现了问题导致结果没有通知过来,此时我们就需要进行查单操作了。
编写接口
@ApiOperation("查询订单")
@GetMapping("/trade/query/{orderNo}")
public R queryOrder(@PathVariable String orderNo){
log.info("查询订单");
String result = aliPayService.queryOrder(orderNo);
return R.ok().setMessage("查询成功").data("result", result);
}
实现查单
/**
* 查询订单
* @param orderNo 订单号
* @return 返回订单查询结果
*/
@Override
public String queryOrder(String orderNo) {
try {
log.info("查询订单接口 ==> {}", orderNo);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功 ===>" + response.getBody());
return response.getBody();
} else {
log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());
return null;
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("查单接口调用失败");
}
}
在进行了订单的相关操作以后,很可能在一定情况下网络出现问题,例如:
首先我们需要开启定时任务
@EnableScheduling
然后我们编写对应的定时任务
package com.manster.pay.task;
import com.manster.pay.entity.OrderInfo;
import com.manster.pay.enums.PayType;
import com.manster.pay.service.AliPayService;
import com.manster.pay.service.OrderInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* @Author manster
* @Date 2022/6/6
**/
@Slf4j
@Component
public class AliPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AliPayService aliPayService;
/**
* 从第0秒开始每隔30秒查询一次,查询创建超过5分钟并且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() {
log.info("========执行订单定时查询========");
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());
for (OrderInfo orderInfo : orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单==> {}", orderNo);
//核实订单状态,调用支付宝查单接口
aliPayService.checkOrderStatus(orderNo);
}
}
}
我们需要一个枚举类来判断支付宝方的订单情况
package com.manster.pay.enums.alipay;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @Author manster
* @Date 2022/6/6
**/
@AllArgsConstructor
@Getter
public enum AliPayTradeState {
/**
* 交易创建,等待买家付款
*/
NOTPAY("WAIT_BUYER_PAY"),
/**
* 未付款交易超时关闭,或支付完成后全额退款
*/
CLOSED("TRADE_CLOSED"),
/**
* 交易支付成功
*/
SUCCESS("TRADE_SUCCESS");
private final String type;
}
最后我们实现根据远程支付宝端的订单状态修改本地订单状态
/**
* 根据订单号调用支付宝支付查单接口,核实订单状态
* 订单未创建,更新商户端订单状态
* 订单未支付,调用关单接口。更新商户端订单状态
* 订单已支付,更新商户端订单状态,记录支付日志
* @param orderNo
*/
@Override
public void checkOrderStatus(String orderNo) {
log.warn("根据订单号核实订单状态 ===> {}", orderNo);
String result = this.queryOrder(orderNo);
//订单未创建
if(result == null){
log.warn("核实订单未创建 ===> {}", orderNo);
//更新本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
}
//解析查询订单返回的结果
Gson gson = new Gson();
HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(result, HashMap.class);
LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");
String tradeStatus = (String) alipayTradeQueryResponse.get("trade_status");
//订单未支付
if(AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){
log.warn("核实订单未支付 ===> {}", orderNo);
//订单未支付进行关单
this.closeOrder(orderNo);
//更新商户端订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
}
//订单已支付
if(AliPayTradeState.SUCCESS.getType().equals(tradeStatus)){
log.warn("核实订单已支付 ===> {}", orderNo);
//更新商户端订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfoForAliPay(alipayTradeQueryResponse);
}
}
https://opendocs.alipay.com/open/028sm9
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,支付宝将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退回到买家账号上。
商户可调用 alipay.trade.refund(统一收单交易退款查询接口)接口进行退款,支付宝同步返回退款参数。调用时序图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03LiU1Hu-1654512260848)(http://mdn.alipayobjects.com/afts/img/A*uhTNTY136OMAAAAAAAAAAAAAAa8wAA/original?bz=openpt_doc&t=v6t9V3bhEVJ0nzGYXCoudQAAAABkMK8AAAAA)]
若退款接口由于网络等原因返回异常,商户可调用退款查询接口 alipay.trade.fastpay.refund.query(统一收单交易退款查询接口)查询指定交易的退款信息。
支付宝退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。
注意
首先我们编写退款接口
@ApiOperation("申请退款")
@PostMapping("/trade/refund/{orderNo}/{reason}")
public R refunds(@PathVariable String orderNo, @PathVariable String reason){
log.info("申请退款");
aliPayService.refund(orderNo, reason);
return R.ok();
}
然后我们实现退款业务
/**
* 退款
* @param orderNo
* @param reason
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) {
try {
//创建退款单
RefundInfo refundInfo = refundInfoService.createRefundByOrderNoForAliPay(orderNo, reason);
//调用退款接口
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
//组装业务对象
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);//订单编号
BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
bizContent.put("refund_amount", refund);//退款金额
bizContent.put("refund_reason", reason);//退款原因
request.setBizContent(bizContent.toString());
AlipayTradeRefundResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功 ===>" + response.getBody());
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
//更新退款单
refundInfoService.updateRefundForAliPay(
refundInfo.getRefundNo(),
response.getBody(),
AliPayTradeState.REFUND_SUCCESS.getType());
} else {
log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
//更新退款单
refundInfoService.updateRefundForAliPay(
refundInfo.getRefundNo(),
response.getBody(),
AliPayTradeState.REFUND_ERROR.getType());
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("退款接口调用失败");
}
}
其中创建退款单和修改退款单方法为:
/**
* 创建退款单
* @param orderNo 订单号
* @param reason 原因
* @return
*/
@Override
public RefundInfo createRefundByOrderNoForAliPay(String orderNo, String reason) {
//获取订单信息
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
//生成退款订单
RefundInfo refundInfo = new RefundInfo();
refundInfo.setOrderNo(orderNo);
refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款订单号
refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额
refundInfo.setRefund(orderInfo.getTotalFee());//退款金额
refundInfo.setReason(reason);
baseMapper.insert(refundInfo);
return refundInfo;
}
/**
* 修改退款单状态
* @param refundNo 退款单
* @param content 退款响应
* @param refundStatus 退款状态
*/
@Override
public void updateRefundForAliPay(String refundNo, String content, String refundStatus) {
//根据退款单编号进行退款
QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no", refundNo);
//设置要修改的字段
RefundInfo refundInfo = new RefundInfo();
refundInfo.setRefundStatus(refundStatus);
refundInfo.setContentReturn(content);
//更新退款单
baseMapper.update(refundInfo, queryWrapper);
}
退款状态的枚举类为
/**
* 退款成功
*/
REFUND_SUCCESS("REFUND_SUCCESS"),
/**
* 退款失败
*/
REFUND_ERROR("REFUND_ERROR"),
https://opendocs.alipay.com/open/028sma
编写退款接口
@ApiOperation("查询退款")
@PostMapping("/trade/fastpay/refund/{orderNo}")
public R queryRefund(@PathVariable String orderNo){
log.info("申请退款");
String result = aliPayService.queryRefund(orderNo);
return R.ok().setMessage("查询成功").data("result", result);
}
实现退款业务
/**
* 查询退款
* @param orderNo
* @return
*/
@Override
public String queryRefund(String orderNo) {
try {
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
bizContent.put("out_request_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功 ===>" + response.getBody());
return response.getBody();
} else {
log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());
return null;//订单不存在
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("查退单接口调用失败");
}
}
https://opendocs.alipay.com/open/028woc
点击选择日期后,点击不同类型的按钮即可发送请求,后端会返回账单所在的下载地址,此时我们直接创建一个超链接元素,并为其赋值链接地址和下载文件名称,并进行点击操作,账单即可进行下载。
微信账单申请
下载交易账单
下载资金账单
支付宝账单申请
下载交易账单
下载资金账单
bill.js
import request from '@/utils/request'
export default{
downloadBillWxPay(billDate, type) {
return request({
url: '/api/wx-pay/downloadbill/' + billDate + '/' + type,
method: 'get'
})
},
downloadBillAliPay(billDate, type) {
return request({
url: '/api/ali-pay/bill/downloadurl/query/' + billDate + '/' + type,
method: 'get'
})
},
}
在点击请求后我们进行接口的编写
@ApiOperation("获取账单url")
@GetMapping("/bill/downloadurl/query/{billDate}/{type}")
public R queryTradeBill(@PathVariable String billDate, @PathVariable String type){
log.info("获取账单url");
String downloadUrl = aliPayService.queryBill(billDate, type);
return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
}
/**
* 根据日期和类型获取账单url
* @param billDate 日期
* @param type 类型
* @return 账单url
*/
@Override
public String queryBill(String billDate, String type) {
try {
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("bill_type", type);
bizContent.put("bill_date", billDate);
request.setBizContent(bizContent.toString());
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功 ===>" + response.getBody());
Gson gson = new Gson();
HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(response.getBody(), HashMap.class);
LinkedTreeMap billDownloadurlQueryResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
String billDownloadUrl = (String) billDownloadurlQueryResponse.get("bill_download_url");
return billDownloadUrl;
} else {
log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("申请账单失败");
}
return null;
}