1.沙箱准备
1.1 去支付宝开放平台找到沙箱测试,appid以及秘钥都是需要的,密钥生成工具支付宝有可以去下载 密钥生成工具.
2.依赖引入
<!--支付宝支付-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>3.4.49.ALL</version>
</dependency>
3.yml属性配置
### 支付宝支付
alipay:
config:
#支付宝预请求接口
url: https://openapi.alipaydev.com/gateway.do
#商家appid
appId: 2016121231231
#商家私钥
privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDAOnh1VIED11yqUKVTmvmITyybMDcSwAG0nN3NM/zkUSvUvO3BooqYxta8CFzlZNv3xv4VLJJfM5uuPMJTZS1xEf7vWhuAfzrTOR6Qg4aQr/hGZq1ROGJW/fqm6vIxZ4TVFpTb9UMtyW8bQsItm1/LyVEGYPVIqx5qM8UPZPkMpUg0/IpI8LP0Lb0JQ9eBCY3p4BCYRkwp5Te5xCIjoc4bqH/c+BD0lz2f1oOLrsq3HQJUyFI/MDUAW1WSmsUeKmQUZsUlEwVupNQ5SM1CAsXA1Ca4T+cdH8j5rUIAeaakMqVFy7hTzIqRliFCbS6xYlgVq5yXgWUuGKNqL++YN/bxAgMBAAECggEAGUlXwS9tjUDOOsVDCj1vp8OqfXuZMwk48DZWYwD8ulgHKQP1TqyACNrXMoLzHWXoHa76i3SCiK10R+KGIyQVAsyWkDRvZ6VfXUD3CRfHJEf5iiCMiUOv8TkCW+PQKBgD/DeGXr9C36jO4H82ohVm6mFiMExPLp+f2kxfIkhrcHUwmgQc2KnISXoNZn1Hg5Nv0LpXtNBKen6dbt+rCtI4BBm5ETg+1QMlHFaAVgiqVtyQn1qG8wIN8JS6V7Mapr01N+Q/5XHZXNRtvsHo0/DEyInRbL2HgHzaC5ifBTVM+TpzV8nEue4f2yHE8hkaEWgNCaJpJVC52k7LeEJp+tzPsnCVL655dvKie6qtyBsB+6XpG+E7QfidAoGAUuoekdJdI6AIBJKC4miks2tpCd7JF2cZfw+UkNIy0sbWpclXjx5Qqnc0EBR/gNYyGZC0OYivbL8dL1ZG4w7OS9r8XSdQtGLd58Yhr1Zn3bD6YXTPBRDApa29d5tr8hyRXnXogQ+DQVRaoQd8oIstea0rvJWXbPEN/+U3yODWoe0=
#支付宝公钥
pubKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkVl/RJ5vsQg9/UkRFDNgRFmEDnLIkpy/LzMv12jF3aBp9esAR809iGiqpogyMuCgs143cZIitBOiAYn70E3swptQi0uG2+IqhLcPqBpsVGf6xXJ6G69GAxHx/rhHFEwIDAQAB
charset: UTF-8
#签名类型
signType: RSA2
#异步回调内部接口
notifyUrl: https://www.baidu.com/
#同步回调
returnUrl: https://www.baidu.com/
format: json
回调接口都是要外网能访问的域名接口,不能是ip直接访问,这个可以在测试的时候用穿透工具ngrok做穿透来实现;
4.配置类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
*@author liuxingying
*@description 支付宝配置文件
*@date 2020/9/24
*/
@Data
@Component
@ConfigurationProperties(prefix = "alipay.config")
public class AlipayAppConfig {
private String url;
private String appId;
private String privateKey;
private String pubKey;
private String charset;
private String signType;
// 异步回调地址
private String notifyUrl;
private String returnUrl;
// 返回格式
private String format;
}
5.接口实现
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import com.xx.core.exception.BusinessException;
import com.xx.ebc.config.AlipayAppConfig;
import com.xx.ebc.service.AlipayService;
import com.xx.ebc.utils.FormatUtils;
import com.xx.sequence.id.Sid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/**
*@author liuxingying
*@description 支付宝app支付实现类
*@date 2020/9/24
*/
@Service
@Slf4j
public class AliPayServiceImpl implements AlipayService {
@Resource
private AlipayAppConfig alipayAppConfig;
@Resource
private Sid sid;
private AlipayClient alipayClient;
/**
* 执行所有方法前都进行init初始化 读取配置文件的信息封装进对象
* @params []
* @return void
*/
@PostConstruct
public void init() {
alipayClient = new DefaultAlipayClient(alipayAppConfig.getUrl(),
alipayAppConfig.getAppId(),
alipayAppConfig.getPrivateKey(),
alipayAppConfig.getFormat(),
alipayAppConfig.getCharset(),
alipayAppConfig.getPubKey(),
alipayAppConfig.getSignType());
}
/** @description 请求支付宝接口
* @params [orderNo, totalFee]
* @return java.lang.String
* @author lxy
* @date 2020/9/25 14:22
**/
@Override
public String tradeAppPay(String orderNo, Integer totalFee) {
log.info("支付宝app支付");
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
//SDK已经封装掉了公共参数,这里只需要传入业务参数。以下方法为sdk的model入参方式(model和biz_content同时存在的情况下取biz_content)
//根据不同的需要new不同的对象 app支付的对象是AlipayTradeAppPayModel 对应更改就可以 这里是wap网页支付
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setBody("支付宝付款");
model.setSubject("开通VIP会员");
model.setOutTradeNo(orderNo);
model.setTotalAmount(FormatUtils.fenToYuan(totalFee));
request.setBizModel(model);
//设置异步回调内部服务的接口 处理业务
request.setNotifyUrl(alipayAppConfig.getNotifyUrl());
//同步回调跳转到需要的前端页面及接口
request.setReturnUrl(alipayAppConfig.getReturnUrl());
//对应的根据不同的支付修改对象 wap app
AlipayTradeWapPayResponse response;
try {
//app支付的接口是sdkExecute()
//response = this.alipayClient.sdkExecute(request);
//wap支付的接口是pageExecute()
response = this.alipayClient.pageExecute(request);
} catch (AlipayApiException e) {
log.error("支付宝支付失败{}", e.getErrMsg());
throw new BusinessException(e.getErrMsg());
}
if (response.isSuccess()) {
log.info("调用成功" + response.getBody());
return response.getBody();
} else {
log.error("调用失败" + response.getBody());
return null;
}
}
/** @description 支付宝退款
* @params [orderNo, refundFee]
* @return java.lang.Boolean
* @author lxy
* @date 2020/9/25 14:28
**/
@Override
public Boolean tradeRefund(String orderNo, Integer refundFee) {
log.info("支付宝退款 {}");
String outRequestNo = orderNo + sid.next();
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
model.setOutTradeNo(orderNo);
model.setRefundAmount(FormatUtils.fenToYuan(refundFee));
model.setRefundReason("正常退款");
model.setOutRequestNo(outRequestNo);
refundRequest.setBizModel(model);
AlipayTradeRefundResponse response;
try {
response = alipayClient.execute(refundRequest);
} catch (AlipayApiException e) {
log.error("支付宝退款失败{}", e.getLocalizedMessage());
return false;
}
if (response != null && "10000".equals(response.getCode())) {
log.info("调用成功" + response);
return true;
} else {
log.error("调用失败" + response);
return false;
}
}
}
- 关于钱的转换工具类如果有需要也可以加入使用
import java.math.BigDecimal;
/**
*@author liuxingying
*@description
*@date 2020/9/24
*/
public class FormatUtils {
/**
* 1. 钱的元到分
* 2. 佣金比例的百分之到万分之
*
* @param yuan
* @return
*/
public static int yuan2Fen(BigDecimal yuan) {
if (yuan == null){
return 0;
}
return yuan.multiply(BigDecimal.valueOf(100)).intValue();
}
public static Integer yuanToFen(String yuan) {
return (new BigDecimal(yuan)).setScale(2, 4).multiply(new BigDecimal(100)).intValue();
}
public static int yuan2Fen(Double yuan) {
if (yuan == null) {
return 0;
}
BigDecimal bigDecimal = new BigDecimal(String.valueOf(yuan));
return yuan2Fen(bigDecimal);
}
public static int yuan2Fen(int yuan) {
if (yuan <= 0) {
return 0;
}
return yuan * 100;
}
/**
* 1. 钱的元到分
* 2. 佣金比例的万分之到百分之
*
* @param fen
* @return
*/
public static BigDecimal fen2Yuan(Integer fen) {
if (fen == null) {
return BigDecimal.valueOf(0);
}
return BigDecimal.valueOf(fen).divide(BigDecimal.valueOf(100)).setScale(2, BigDecimal.ROUND_FLOOR);
}
public static Integer fen2YuanInt(Integer fen) {
if (fen == null) {
return 0;
}
return fen / 100;
}
public static String fenToYuan(Integer fen) {
BigDecimal bigDecimal = fen2Yuan(fen);
bigDecimal = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP);
return bigDecimal.toString();
}
/**
* 返回 (金额 * 百分比)(分)
*
* @param amount 金额 分
* @param rate 百分比
* @return
*/
public static Integer amountToRate(Integer amount, Integer rate) {
if ((amount == null) || (rate == null)) {
return null;
}
BigDecimal multiply = BigDecimal.valueOf(amount).multiply(BigDecimal.valueOf(rate).divide(BigDecimal.valueOf(100)));
return multiply.intValue();
}
/**
* 返回 (金额 * 万分比)(分)
*
* @param amount 金额 分
* @param rate 万分比
* @return
*/
public static Integer amountToRateV2(Integer amount, Integer rate) {
if ((amount == null) || (rate == null)) {
return null;
}
BigDecimal multiply = BigDecimal.valueOf(amount).multiply(BigDecimal.valueOf(rate).divide(BigDecimal.valueOf(10000)));
return multiply.intValue();
}
}
- 在回调接口处理业务的时候需要用到返回的参数,用对象接
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
*@author liuxingying
*@description 支付宝参数回调对象
*@date 2020/9/24
*/
@Data
public class AlipayNotifyParam {
private String appId;
private String tradeNo; // 支付宝交易凭证号
private String outTradeNo; // 原支付请求的商户订单号
private String outBizNo; // 商户业务ID,主要是退款通知中返回退款申请的流水号
private String buyerId; // 买家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字
private String buyerLogonId; // 买家支付宝账号
private String sellerId; // 卖家支付宝用户号
private String sellerEmail; // 卖家支付宝账号
private String tradeStatus; // 交易目前所处的状态,见交易状态说明
private BigDecimal totalAmount; // 本次交易支付的订单金额
private BigDecimal receiptAmount; // 商家在交易中实际收到的款项
private BigDecimal buyerPayAmount; // 用户在交易中支付的金额
private BigDecimal refundFee; // 退款通知中,返回总退款金额,单位为元,支持两位小数
private String subject; // 商品的标题/交易标题/订单标题/订单关键字等
private String body; // 该订单的备注、描述、明细等。对应请求时的body参数,原样通知回来
private Date gmtCreate; // 该笔交易创建的时间。格式为yyyy-MM-dd HH:mm:ss
private Date gmtPayment; // 该笔交易的买家付款时间。格式为yyyy-MM-dd HH:mm:ss
private Date gmtRefund; // 该笔交易的退款时间。格式为yyyy-MM-dd HH:mm:ss.S
private Date gmtClose; // 该笔交易结束时间。格式为yyyy-MM-dd HH:mm:ss
private String fundBillList; // 支付成功的各个渠道金额信息,array
private String passbackParams; // 公共回传参数,如果请求时传递了该参数,则返回给商户时会在异步通知时将该参数原样返回。
}
- 回调的接口总处理业务需要的一些解析返回参数以及验证签名等,选择使用方法即可
import cn.hutool.core.thread.ThreadUtil;
import com.alibaba.fastjson.JSON;
import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import com.x.core.domain.Result;
import com.x.ebc.config.AlipayAppConfig;
import com.x.ebc.pojo.dto.PayMealDTO;
import com.x.ebc.pojo.vo.AlipayNotifyParam;
import com.x.ebc.service.AlipayService;
import com.x.ebc.service.WxPayService;
import com.x.ebc.utils.FormatUtils;
import com.x.sequence.id.Sid;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
*@author liuxingying
*@description 微信支付/支付宝支付 前端控制器
*@date 2020/9/24
*/
@Slf4j
@RestController
public class PayController {
//private ExecutorService executorService = Executors.newFixedThreadPool(20);
@Resource
private AlipayAppConfig alipayAppConfig;
@Resource
private AlipayService alipayService;
@Resource
private WxPayService wxPayService;
@Resource
private Sid sid;
@GetMapping("/aliPay")
public String callback() {
String orderId = sid.next();
String s = alipayService.tradeAppPay(orderId, 1000);
return s;
}
/** @description 支付宝回调接口
* @params [request]
* @return java.lang.String
* @author lxy
* @date 2020/9/24 18:45
**/
@RequestMapping(value = "/aliPayCallback", method = RequestMethod.POST)
public String callback(HttpServletRequest request) {
// 将异步通知中收到的待验证所有参数都存放到map中
Map<String, String> params = convertRequestParamsToMap(request);
String paramsJson = JSON.toJSONString(params);
log.info("支付宝回调,{}", paramsJson);
try {
// 调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayAppConfig.getPubKey(),
alipayAppConfig.getCharset(), alipayAppConfig.getSignType());
log.info("signVerified={}", signVerified);
if (signVerified) {
log.info("支付宝回调签名认证成功");
// 按照支付结果异步通知中的描述,对支付结果中的业务内容进行1\2\3\4二次校验,校验成功后在response中返回success,校验失败返回failure
check(params);
// 另起线程处理业务
log.info("另起线程处理业务");
ThreadUtil.execAsync(()->{
AlipayNotifyParam param = buildAlipayNotifyParam(params);
String tradeStatus = param.getTradeStatus();
// 支付成功
if (tradeStatus.equals("TRADE_SUCCESS")) {
log.info("支付成功");
//TODO 用户处理自己逻辑
} else {
log.info("支付失败");
//TODO 用户处理自己逻辑
}
});
// 如果签名验证正确,立即返回success,后续业务另起线程单独处理
return "success";
} else {
log.info("支付宝回调签名认证失败, paramsJson:{}", paramsJson);
return "failure";
}
} catch (AlipayApiException e) {
log.info("支付宝回调发生异常{}", e.getLocalizedMessage());
return "failure";
}
}
/** @description 将request中的参数转换成Map
* @params [request]
* @return java.util.Map<java.lang.String,java.lang.String>
* @author lxy
* @date 2020/9/25 10:14
**/
private Map<String, String> convertRequestParamsToMap(HttpServletRequest request) {
Map<String, String> retMap = new HashMap<>(16);
//1.从支付宝回调的request域中取值
Map<String, String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = iter.next();
String[] values = requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
}
// 乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");
retMap.put(name, valueStr);
}
return retMap;
}
private AlipayNotifyParam buildAlipayNotifyParam(Map<String, String> params) {
String json = JSON.toJSONString(params);
return JSON.parseObject(json, AlipayNotifyParam.class);
}
private void check(Map<String, String> params) throws AlipayApiException {
String outTradeNo = params.get("out_trade_no");
// 1、商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
OrderInfo order = orderService.selectOrderByOrderNo(outTradeNo);
if (order == null) {
throw new AlipayApiException("out_trade_no错误");
}
// 2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),
Integer totalAmount = FormatUtils.yuan2Fen(new BigDecimal(params.get("total_amount")));
log.info("totalAmount{}", totalAmount);
if (!totalAmount.equals(order.getSnapshotTotalFee())) {
throw new AlipayApiException("error total_amount");
}
// 3、验证app_id是否为该商户本身。
if (!params.get("app_id").equals(alipayAppConfig.getAppId())) {
throw new AlipayApiException("app_id不一致");
}
}
}
总结
- 支付宝支付的开放平台中,有一个沙箱模式可以供我们测试使用,相比较微信来说要方便很多,而且java代码集成的复杂度以及上手容易度来说也更比微信来的快的多
- 总得来说这些代码可以直接使用 然后修改商户信息后直接可以引用实现