前因
温馨提醒:阅读本文需要8分钟
最近在GitHub上二次开源一个基于SpringBoot的半藏商城的项目,在弄支付的时候,本来的打算是,整合所有支付接口,后来被现实打败了,个人是无法申请任何支付接口的权限的,还好支付宝为开发人员提供了一个沙箱环境的接口(与正式的只是调用接口地址不同)。接下来分享一下我的整个支付接口的代码流程。
Maven引包
首先进行在pom.xml中进行引包,还在为引哪个版本的包而困扰的同学推荐这个Maven在线查找依赖的网站,想用什么输入搜索,复制过来就可以了。我这里引用的是3.1.0版本的alipay-sdk-java。
<!-- 阿里支付-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>3.1.0</version>
</dependency>
获取支付宝公钥私钥
小伙伴们是不是以为加载完阿里的sdk就可以愉快的开发了,现在还缺少支付宝沙箱环境的公钥,私匙,以及请求的应用ID。首先,进入蚂蚁金服开放平台官方主页, 点击文档中心的开发文档,往下翻,找到开发工具-沙箱环境,进入沙箱环境页面,系统已经自动为你创建一个应用,在基础信息中可以看到应用信息。这里RSA2
公钥已经生成了,不过需要下载一个阿里生成密钥的软件使用RSA2的方式来生成私钥。(生成公钥私钥的不过多讲解,不懂可自行百度。)
编写公共常量类
我自己习惯将一些常量信息放在Contans.java
类中,不过有的人喜欢放在application.properties
中配置,再用@value
注解引用。看个人习惯,反正我喜欢这样写,浏览浏览代码体验更佳。下面放出Contans.java
类中的常量配置代码。
public final static String APP_ID = "填写自己的id";//沙箱支付宝环境发起支付请求的应用ID
//生成的应用私钥
public final static String APP_PRIVATE_KEY = "填写自己的私钥";//生成的应用私钥
//支付宝公钥
public final static String ALIPAY_PUBLIC_KEY = "填写自己的公钥";//支付宝公钥
//这是沙箱接口路径,正式路径为https://openapi.alipay.com/gateway.do
public final static String GATEWAY_URL ="https://openapi.alipaydev.com/gateway.do";//沙箱请求网关地址
public final static String CHARSET = "UTF-8";// 编码
public final static String FORMAT = "JSON";// 返回格式
public final static String SIGN_TYPE = "RSA2";// 签名方式RSA2
//支付宝服务器异步通知页面路径,付款完毕后会异步调用本项目的方法,必须为公网地址
public final static String NOTIFY_URL = "http://mall.babehome.com:28089/alipay/alipayNotifyNotice";
//支付宝同步通知路径,也就是当付款完毕后跳转本项目的页面,可以不是公网地址
public final static String RETURN_URL = "http://mall.babehome.com:28089/alipay/alipayReturnNotice";
// public final static String RETURN_URL = "http://localhost:28089/alipay/alipayReturnNotice";//本地回调
public final static int ALIPAY_TYPE = 1;//支付宝支 付类型为1 微信为2
编写Controller层代码
其实这段代码应该放在业务层的,不过我的代码我做主,当时不知道为啥就放在Controller层了,懒得改了。其实也不影响啥,嘿嘿。需要先实例化支付的客户端,以及设置请求的参数,同步回调和异步回调的地址(注意上文常量代码中有配置,下一节会详细讲解同步回调和异步回调的区别)。下面分享一下具体的实现代码。同时推荐大家了解一下lombok这个插件,还挺好玩的,我最喜欢的就是@Slf4j
可以直接使用log打印日志。或者@Data
等。其他的感觉作者有点在炫技,一行代码可以写完的东西,也要用注解就没必要了,太影响代码的可读性与维护性了。
/**
* @author 皓宇QAQ
* @email 2469653218@qq.com
* @link https://github.com/Tianhaoy/hanzomall
* @阿里支付接口
*/
@Slf4j
@Controller
@RequestMapping("/alipay")
public class AlipayController {
@Resource
private HanZoMallOrderService hanZoMallOrderService;
@Resource
private MailSendService mailSendService;
//前往支付宝沙箱网关进行支付
@RequestMapping(value = "/goAlipay", produces = "text/html; charset=UTF-8")
@ResponseBody
public String goAlipay(@RequestParam("orderNo") String orderNo,@RequestParam("totalPrice") String totalPrice,
@RequestParam("itemString") String itemString,HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws Exception {
//实例化客户端
AlipayClient alipayClient = new DefaultAlipayClient(Constants.GATEWAY_URL, Constants.APP_ID, Constants.APP_PRIVATE_KEY, Constants.FORMAT, Constants.CHARSET, Constants.ALIPAY_PUBLIC_KEY, Constants.SIGN_TYPE);
//设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(Constants.RETURN_URL);//同步
alipayRequest.setNotifyUrl(Constants.NOTIFY_URL);//异步
String out_trade_no = orderNo; //订单号
String total_amount = totalPrice;//付款金额,必填
String subject = "【半藏商城】"+itemString; //订单名称,必填
String body = null;//商品描述,可空
// 该笔订单允许的最晚付款时间,逾期将关闭交易。取值范围:1m~15d。m-分钟,h-小时,d-天,1c-当天(1c-当天的情况下,无论交易何时创建,都在0点关闭)。 该参数数值不接受小数点, 如 1.5h,可转换为 90m。
String timeout_express = "1c";
if(null!=total_amount) { //支付金额不等于空
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"timeout_express\":\""+ timeout_express +"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
//请求
String result = alipayClient.pageExecute(alipayRequest).getBody();
return result;
}
return "error/error_5xx";
}
}
支付宝接口之回调
同步回调与异步回调的区别
我的理解同步回调是给客户看的,异步是服务器处理请求。同步是客户支付成功了,告诉客户这个操作的结果是成功还是失败,同时也会更改订单状态来引导客户下一步操作。起到的作用就是提示客户这个操作的结果是成功还是失败。异步是服务器在后端处理支付成功或失败时的业务逻辑。
同步通知
:用于用户在支付宝页面付款完毕后自动跳转。
异步通知
:其实是处理业务逻辑,比如说修改客户的支付状态。
同步得到通知后跳转到自己的网址,然后根据参数告诉客户支付结果,然后在更新状态。异步其实就是一个双保险,如果同步没有跳转你的网址,可能是关机了,或者网速慢,无法完成数据更新的状态,这时候异步就发挥作用了,先判断是否支付,支付了就不必更新了,只返回支付宝 success 就行了,不然会一直异步通知。
举个例子
:
假如用户支付后,立即关闭了浏览器窗口,那么回调通知就会失败,订单状态不会更改,但是用户的确是支付了,所以需要异步通知再校验一下更改状态。
不过支付宝沙箱环境的同步回调和异步回调的速度有点迷,有时候异步回调比同步回调还要快,同步回调每次都需要20秒才能回来。(多次验证,是支付宝沙箱环境的问题,估计没人维护。)
同步回调代码
下面分享出同步回调的实现代码,Service层接口代码就不贴出了,只是进行调用改动订单状态的一些接口。
//支付宝同步通知页面,成功返回
@RequestMapping(value = "/alipayReturnNotice")
public String alipayReturnNotice(HttpServletRequest request, HttpServletRequest response, HttpSession httpSession) throws Exception {
log.info("支付成功, 进入同步通知接口...");
//获取支付宝GET过来反馈信息
Map<String,String> params = new HashMap<String,String>();
Map<String,String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
log.info("支付宝返回参数:"+params);
// 调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(params, Constants.ALIPAY_PUBLIC_KEY, Constants.CHARSET, Constants.SIGN_TYPE);
if(signVerified) {
//商户订单号
String orderNo = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
//支付宝交易号
String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
//付款金额
String total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"), "UTF-8");
HanZoMallUserVO user = (HanZoMallUserVO) httpSession.getAttribute(Constants.MALL_USER_SESSION_KEY);
HanZoMallOrder hanZoMallOrder = hanZoMallOrderService.getHanZoMallOrderByOrderNo(orderNo);
String emailAddress = user.getEmailAddress();
if (!"".equals(emailAddress)){
mailSendService.sendSimpleMail(emailAddress, "【半藏商城付款成功】", "您好,订单号为"+orderNo+"的订单通过支付宝付款"+total_amount+"元!");
}
if (hanZoMallOrder.getOrderStatus()== HanZoMallOrderStatusEnum.OREDER_PAID.getOrderStatus()){
//有可能异步回调比同步回调块,已经更改支付状态了 不做任何处理
log.info("订单同步回调时已更新支付状态为已支付");
}else if (hanZoMallOrder.getOrderStatus()== HanZoMallOrderStatusEnum.ORDER_PRE_PAY.getOrderStatus()){
//支付成功 并且订单支付状态为待支付 更新状态为已支付
String payResult = hanZoMallOrderService.paySuccess(orderNo, Constants.ALIPAY_TYPE,user.getUserId());
if (ServiceResultEnum.SUCCESS.getResult().equals(payResult)) {
log.info("修改订单状态成功");
return "redirect:/orders/"+orderNo;
}else{
//更新订单状态失败
log.error("修改订单状态失败");
return "error/error_5xx";
}
}
//String payResult = hanZoMallOrderService.paySuccess(orderNo, Constants.ALIPAY_TYPE,user.getUserId());
log.info("******************** 支付成功(支付宝同步通知) ********************");
log.info("* 订单号: {}", orderNo);
log.info("* 支付宝交易号: {}", trade_no);
log.info("* 实付金额: {}", total_amount);
log.info("***************************************************************");
}else{
log.error("同步回调签名验证失败");
}
return "redirect:/orders";
}
异步回调代码
下面分享出异步回调的实现代码,Service层接口代码依旧不贴出了。
//支付宝异步 通知页面
@RequestMapping(value = "/alipayNotifyNotice")
@ResponseBody
public String alipayNotifyNotice(HttpServletRequest request, HttpServletRequest response) throws Exception {
log.info("支付成功, 进入异步通知接口...");
//获取支付宝POST过来反馈信息
Map<String,String> params = new HashMap<String,String>();
Map<String,String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
//valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
// 调用SDK验证签名
log.info("支付宝返回参数:"+params);
boolean signVerified = AlipaySignature.rsaCheckV1(params, Constants.ALIPAY_PUBLIC_KEY, Constants.CHARSET, Constants.SIGN_TYPE);
if(signVerified) {
//商户订单号
String orderNo = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
//支付宝交易号
String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
//付款金额
String total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"), "UTF-8");
//交易状态
String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");
if(trade_status.equals("TRADE_FINISHED")){
//订单没有退款功能, 这个条件判断是进不来的, 所以此处不必写代码
//退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
}else if (trade_status.equals("TRADE_SUCCESS")){
log.info("******************* 支付成功(支付宝异步通知) *******************");
log.info("* 订单号: {}", orderNo);
log.info("* 支付宝交易号: {}", trade_no);
log.info("* 实付金额: {}", total_amount);
log.info("*************************************************************");
//付款完成后,支付宝系统发送该交易状态通知
//验证支付成功后 需要验证是否更新过支付状态了
HanZoMallOrder hanZoMallOrder = hanZoMallOrderService.getHanZoMallOrderByOrderNo(orderNo);
if (hanZoMallOrder.getOrderStatus()== HanZoMallOrderStatusEnum.OREDER_PAID.getOrderStatus()){
//并且同步回调时已经更改支付状态了 不做任何处理
log.info("订单同步回调时已更新支付状态为已支付");
}else if (hanZoMallOrder.getOrderStatus()== HanZoMallOrderStatusEnum.ORDER_PRE_PAY.getOrderStatus()){
//支付成功 并且订单支付状态为待支付 更新状态为已支付
String payResult = hanZoMallOrderService.paySuccess(orderNo, Constants.ALIPAY_TYPE,hanZoMallOrder.getUserId());
if (ServiceResultEnum.SUCCESS.getResult().equals(payResult)) {
log.info("修改订单状态成功");
}else{
//更新订单状态失败
log.error("修改订单状态失败");
}
}
}
}else {
log.error("异步回调签名验证失败");
}
return "success";
}
小结
到此为止,整个支付宝的支付流程就介绍完毕了,知识只有分享出来才有价值。如果有问题的话,可以在关于我的页面,通过我的邮箱联系我进行探讨。