springboot整合支付宝沙箱支付
1.简介
支付宝开发平台地址:https://open.alipay.com/develop/sandbox/app
对于学生来说,目前网上确实没有比较统一而且质量好的支付教程。因为支付对个人开发者尤其是学生来说不太友好。因此,自己折腾两天,算是整理了一篇关于支付宝沙箱支付的文章。况且个人是不能申请支付(wx和alipay)都一样。幸亏有支付宝沙箱这个环境。其实跟正式环境差不多,就换下配置即可。
整体流程
2.配置说明
要记住这几个重要的配置
-
appId
这个是appId -
privateKey
商户私钥 -
publicKey
支付宝公钥, 即对应APPID下的支付宝公钥 -
notifyUrl
支付成功后异步回调地址(注意是必须是公网地址) -
returnUrl
#支付后回调地址 -
signType
签名类型 一般写 RSA2 -
charset
utf-8 -
format
json - #网关地址 在支付宝开发平台复制拷贝下来
-
gatewayUrl
: https://openapi.alipaydev.com/gateway.do -
logPath
: F:\ 日志路径
3.springboot整合支付宝沙箱支付代码
需要导入依赖
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.33.39.ALL</version>
</dependency>
3.1 配置类
package com.hjt.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "alipay")
@Data
/***
* @author: hjt
* @Date: 2020/11/13/19:19
* @Description: 支付宝配置类(读取配置文件)
*/
public class MyAliPayConfig {
/**
* APPID
*/
private String appId;
/**
* 商户私钥, 即PKCS8格式RSA2私钥
*/
private String privateKey;
/**
* 支付宝公钥
*/
private String publicKey;
/**
* 服务器异步通知页面路径,需http://格式的完整路径
* 踩坑:不能加?type=abc这类自定义参数
*/
private String notifyUrl;
/**
* 页面跳转同步通知页面路径,需http://格式的完整路径
* 踩坑:不能加?type=abc这类自定义参数
*/
private String returnUrl;
/**
* 签名方式
*/
private String signType;
/**
* 字符编码格式
*/
private String charset;
/***
* 参数编码格式 json
*/
private String format;
/**
* 支付宝网关
*/
private String gatewayUrl;
/**
* 日志打印地址
*/
private String logPath;
}
3.2 生成订单信息(以java为例,官网例子)
public void doPost (HttpServletRequest httpRequest,
HttpServletResponse httpResponse) throws ServletException, IOException {
AlipayClient alipayClient = new DefaultAlipayClient( "https://openapi.alipay.com/gateway.do" , APP_ID, APP_PRIVATE_KEY, FORMAT, CHARSET, ALIPAY_PUBLIC_KEY, SIGN_TYPE); //获得初始化的AlipayClient
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest(); //创建API对应的request
alipayRequest.setReturnUrl( "http://domain.com/CallBack/return_url.jsp" );
alipayRequest.setNotifyUrl( "http://domain.com/CallBack/notify_url.jsp" ); //在公共参数中设置回跳和通知地址
alipayRequest.setBizContent( "{" +
" \"out_trade_no\":\"20150320010101001\"," +
" \"product_code\":\"FAST_INSTANT_TRADE_PAY\"," +
" \"total_amount\":88.88," +
" \"subject\":\"Iphone6 16G\"," +
" \"body\":\"Iphone6 16G\"," +
" \"passback_params\":\"merchantBizType%3d3C%26merchantBizNo%3d2016010101111\"," +
" \"extend_params\":{" +
" \"sys_service_provider_id\":\"2088511833207846\"" +
" }" +
" }" ); //填充业务参数
String form= "" ;
try {
form = alipayClient.pageExecute(alipayRequest).getBody(); //调用SDK生成表单
} catch (AlipayApiException e) {
e.printStackTrace();
}
httpResponse.setContentType( "text/html;charset=" + CHARSET);
httpResponse.getWriter().write(form); //直接将完整的表单html输出到页面
httpResponse.getWriter().flush();
httpResponse.getWriter().close();
}
以下是我自己业务代码的例子
@Override
public void pay(PayInfoDto payInfoDTO, HttpServletResponse httpResponse) throws IOException {
/*查询订单id是否存在*/
Long orderId = payInfoDTO.getOrderId();
/*商品名称*/
String subject = "";
/*判断订单是否存在*/
R<Order> orderInfo = remoteOrderService.getOrderInfo(orderId);
if (orderInfo.getCode() != 200) {
throw new BaseException(PayException.ORDER_ERROR_PAY_ORDER);
}
Order order = orderInfo.getData();
//订单总金额
BigDecimal total = order.getTotal();
String proId = order.getProId();
/*商品名称以,分割*/
String[] split = proId.split(",");
for (int i = 0; i < split.length; i++) {
R<Product> productInfo = remoteOrderService.getProductInfo(Long.parseLong(split[i]));
if (productInfo.getCode() != 200) {
throw new BaseException(PayException.ORDER_ERROR_PAY_PRODUCT);
}
Product product = productInfo.getData();
subject = subject + product.getProTitle() + ",";
}
//进行支付宝支付
AlipayOrder alipayOrder = new AlipayOrder();
//商户订单号
alipayOrder.setOut_trade_no(String.valueOf(orderId));
//订单名称
alipayOrder.setSubject(subject);
alipayOrder.setDescription(subject);
//订单总金额
alipayOrder.setTotal_amount(total.toString());
/*进行支付*/
String payBody = alipayUtil.payByAlipay(alipayOrder);
log.info("-----------支付信息实体---------{}",payBody);
//并把消息推送到mq查询是否支付成功
if(StringUtils.isBlank(payBody)){
throw new BaseException(PayException.ORDER_ERROR_PAY_BODY);
}
/*把支付信息写到html网页中*/
httpResponse.setContentType("text/html;charset=" + CHARSET);
// 直接将完整的表单html输出到页面
httpResponse.getWriter().write(payBody);
httpResponse.getWriter().flush();
httpResponse.getWriter().close();
}
3.3支付成功后的异步通知
@Override
public String payNotifyUrl(HttpServletRequest request) throws AlipayApiException {
log.info("-----------开始进行支付后的异步通知回调-------");
/*接收参数*/
Map<String, String> params = this.exchangeParams(request);
String payContent = JSONUtil.toJsonStr(params);
log.info("---------接收的参数--- -----{}",params);
/*验证签名(支付宝公钥) 调用SDK验证签名*/
boolean signVerified = AlipaySignature.rsaCheckV1(params, aliPayConfig.getPublicKey(), aliPayConfig.getCharset(), aliPayConfig.getSignType());
if (signVerified){
/*收到支付宝异步通知,返回success,支付宝不再通知 否则会通知你三天三夜*/
log.info("-----验签成功-----");
/*应该马上返回"success",另起线程执行自己的业务逻辑*/
ExecutorService executor = ExecutorBuilder.create()
.setCorePoolSize(5)
.setMaxPoolSize(10)
.setWorkQueue(new LinkedBlockingQueue<>(100))
.build();
executor.execute(new Runnable(){
@Override
public void run() {
//TODO 幂等性问题后续也要考虑
/*支付状态*/
String trade_status = params.get("trade_status");
/*订单id*/
String out_trade_no = params.get("out_trade_no");
/*支付成功*/
if (PayConstant.TRADE_FINISHED.equals(trade_status) || PayConstant.TRADE_SUCCESS.equals(trade_status)) {
Long orderId = Long.valueOf(out_trade_no);
/*把对于的订单id改为已支付状态*/
R<Order> order = remoteOrderService.updateOrderById(orderId);
if(order.getCode()!=PayConstant.CODE){
throw new BaseException(PayException.ORDER_ERROR_PAY_ORDER);
}
/*存对于的支付信息*/
PayInfo payInfo = null;
payInfo = PayInfo.builder()
.orderIds(out_trade_no)
.callbackContent(payContent)
.callbackTime(LocalDateTime.now())
.tradeStatus(trade_status)
.tradeNo(params.get("trade_no"))
.buyerId(params.get("buyer_id"))
.totalAmount(params.get("total_amount"))
.version(params.get("version"))
.sellerId(params.get("seller_id"))
.receiptAmount(params.get("receipt_amount"))
.gmtCreate(params.get("gmt_create"))
.gmtPayment(params.get("gmt_payment"))
.fundBillList(params.get("fund_bill_list"))
.build();
payInfoMapper.insert(payInfo);
log.info("-----支付信息插入成功-------");
}
/* 支付失败 */
else{
log.error("-------支付失败, 订单id:------{}",out_trade_no);
throw new BaseException(PayException.ORDER_ERROR_PAY_BODY);
}
}
});
return "success";
// TODO 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,校验成功后在response中返回success并继续商家自身业务处理,校验失败返回failure
} else {
log.info("-----验签失败-----");
return "failure";
// TODO 验签失败则记录异常日志,并在response中返回failure.
}
}
注意!!!异步通知的地址必须是公网地址,这里我采用的是frp内网穿透到本地的地址
需要注意的是,异步通知必须要严格进行验签。
运行效果图:
先生成订单
然后再浏览器直接调用接口
http://localhost:4401/pay/api/v1/pay-info/pay?Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.YWRtaW4.VaJOloHfQLjacnm6-__pSaeNZ1JbLAdlgeJT3JEptos&orderId=769532559209283584
会直接跳转到支付支付界面
账号密码都是可以在你沙箱账号看得到
支付成功后可见已经回调到我们异步通知自定义的接口了
即我们在这配置的路径
notifyUrl
支付成功后异步回调地址(注意是必须是公网地址)
3.4退款操作
参数 | 类型 | 是否必填 | 最大长度 | 描述 | 示例值 |
---|---|---|---|---|---|
trade_no | String | 特殊可选 | 64 | 支付宝交易号。 和商户订单号不能同时为空 | 2021081722001419121412730660 |
out_trade_no | String | 特殊可选 | 64 | 商户订单号。 订单支付时传入的商户订单号,和支付宝交易号不能同时为空。 trade_no,out_trade_no如果同时存在优先取trade_no | 2014112611001004680073956707 |
out_request_no | String | 必选 | 64 | 退款请求号。 请求退款接口时,传入的退款请求号,如果在退款请求时未传入,则该值为创建交易时的商户订单号。 | HZ01RF001 |
query_options | String[] | 可选 | 1024 | 查询选项,商户通过上送该参数来定制同步需要额外返回的信息字段,数组格式。枚举支持: refund_detail_item_list:本次退款使用的资金渠道; gmt_refund_pay:退款执行成功的时间; deposit_back_info:银行卡冲退信息; | refund_detail_item_list |
商户可使用该接口查询自已通过alipay.trade.refund提交的退款请求是否执行成功。
注意:1. 该接口的返回码10000,仅代表本次查询操作成功,不代表退款成功,当接口返回的refund_status值为REFUND_SUCCESS时表示退款成功,否则表示退款没有执行成功。
\2. 如果退款未成功,商户可以调用退款接口重试,重试时请务必保证退款请求号和退款金额一致,防止重复退款。
\3. 发起退款查询接口的时间不能离退款请求时间太短,建议之间间隔10秒以上。
个人搭建项目代码地址:
https://github.com/hongjiatao/spring-boot-anyDemo
欢迎收藏点赞三连。谢谢!有问题可以留言博主会24小时内无偿回复。