前因

温馨提醒:阅读本文需要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";
    }

小结

到此为止,整个支付宝的支付流程就介绍完毕了,知识只有分享出来才有价值。如果有问题的话,可以在关于我的页面,通过我的邮箱联系我进行探讨。

Q.E.D.


Remain true to our original aspiration.