对接支付宝支付接口开发笔记

支付宝对接学习笔记

功能介绍:

  • 支付宝对接
  • 支付宝回调
  • 查询支付状态(略过不讲)

要求:

  • 熟悉支付宝对接核心文档,调通支付宝官方Demo
  • 解析支付宝SDK对接源码
  • RSA1和RSA2验证签名及加解密
  • 避免支付宝的重复通知而加数据校验(略)

技巧:

  • ngrok 外网穿透
  • 生成二维码并持久化到图片服务器

调试完demo后,集合到开发项目。
把支付宝依赖的jar宝按照提供版本要求导入,sdk则放在web下lib文件夹下。然后在module的依赖中导入lib下的本地jar包(坑!!不然会报红)
那么为什么不统一使用pom导入呢?原因就是阿里没有提供该jar包的线上导入,只能本地导入。为了统一jar地址,所以必须先配置sdk的jar包的位置。(在这之前还要配置一个maven插件以加载本地jar包).

接下来简单梳理一遍流程:

一、登录进入蚂蚁金服

本次使用沙箱环境下进行整合,沙箱环境开发上线流程差别不大,和正式几乎是一致的,只是切换不同的APPID和支付宝网关。


image.png

二、下载官方的demo

这里选中java版的demo


image.png

选中idea导入。先在本地调通再集成到系统中去。


image.png

右键运行主函数会发现运行不了,那是因为我们还没有修改配置文件中设置。

对应配置如下。


image.png

那么问题来了怎么生成这些公钥私钥呢?前往这里根据系统下载对应的工具。

image.png

接着:

image.png

配置好配置文件后,运行一下:
image.png

运行没有问题,证明已经调通。下载沙箱版的支付宝,登录沙箱提供的买家账户,复制当面付二维码找一个二维码生成工具扫描支付看能不能成功。
image.png

扫描支付后:


image.png

到此为止本地支付宝已经调通,这个还是相对来说比较简单的。从demo的项目结构来看,这是一个web项目,可以自行配置运行环境再运行,测试会更加方便一点,如果没有出错的话就会出现下图:


image.png

image.png

三、系统对接支付宝支付接口

虽然官网已经写得很清楚了,但是第一次对接还是很吃力,这里写一下思路:

1、先把demo中的aplipay那个包及配置文件复制放到需要集成项目的类路径下:
image.png
2、把支付宝依赖的jar宝按照提供版本要求导入,sdk则放在web下lib文件夹下。然后在module的依赖中导入lib下的本地jar包(坑!!不然会报红)

那么为什么不统一使用pom导入呢?原因就是阿里没有提供该jar包的线上导入,只能本地导入。为了统一jar地址,所以必须先配置sdk的jar包的位置。


image.png

image.png

还要配置一个maven插件以加载本地jar包.


image.png
3、运行下主函数没有报错就是初步导入成功。
image.png

四、对接支付宝支付接口

1、这里是整个过程中最难的部分。

从下订单到支付到支付完成,省去下订单的接口,支付过程需要用到两个接口,一个是支付接口,一个是给支付宝授权回调接口。订单这里采用模拟数据。

2、首先支付接口

扫码支付调用流程:

image.png

官方文档参数描述:


image.png

因此先查询数据组装支付宝要求的参数值:

Map<String, String> resultMap = Maps.newHashMap();
        Order order = orderMapper.selectByUserAndOrderNo(userId, orderNo);
        if (order == null) {
            return ServerRespond.createByErrorMessage("用户没有该订单");

        }
        resultMap.put("orderNo", String.valueOf(order.getOrderNo()));

        // (必填) 商户网站订单系统中唯一订单号,64个字符以内,只能包含字母、数字、下划线,
        // 需保证商户系统端不能重复,建议通过数据库sequence生成,
        String outTradeNo = order.getOrderNo().toString();

        // (必填) 订单标题,粗略描述用户的支付目的。如“xxx品牌xxx门店当面付扫码消费”
        String subject = new StringBuilder().append("寸金在线商城,订单号:").append(outTradeNo).toString();

        // (必填) 订单总金额,单位为元,不能超过1亿元
        // 如果同时传入了【打折金额】,【不可打折金额】,【订单总金额】三者,则必须满足如下条件:【订单总金额】=【打折金额】+【不可打折金额】
        String totalAmount = order.getPayment().toString();

        // (可选) 订单不可打折金额,可以配合商家平台配置折扣活动,如果酒水不参与打折,则将对应金额填写至此字段
        // 如果该值未传入,但传入了【订单总金额】,【打折金额】,则该值默认为【订单总金额】-【打折金额】
        String undiscountableAmount = "0";

        // 卖家支付宝账号ID,用于支持一个签约账号下支持打款到不同的收款账号,(打款到sellerId对应的支付宝账号)
        // 如果该字段为空,则默认为与支付宝签约的商户的PID,也就是appid对应的PID
        String sellerId = "";

        // 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品2件共15.00元"
        String body = new StringBuilder().append("订单").append(outTradeNo).append("购买商品共").append(totalAmount).append("元").toString();

        // 商户操作员编号,添加此参数可以为商户操作员做销售统计
        String operatorId = "test_operator_id";

        // (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持
        String storeId = "test_store_id";

        // 业务扩展参数,目前可添加由支付宝分配的系统商编号(通过setSysServiceProviderId方法),详情请咨询支付宝技术支持
        ExtendParams extendParams = new ExtendParams();
        extendParams.setSysServiceProviderId("2088100200300400500");

        // 支付超时,定义为120分钟
        String timeoutExpress = "120m";

        // 商品明细列表,需填写购买商品详细信息,
        List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();

        List<OrderItem> orderItemList = orderItemMapper.getByOrderNoUserId(orderNo, userId);
        System.out.println(orderItemList.get(0));
        for (OrderItem orderItem : orderItemList) {

            GoodsDetail goods = GoodsDetail.newInstance(orderItem.getProductId().toString(), orderItem.getProductName().toString(),

                    BigDecimalUtil.mul(orderItem.getCurrentUnitPrice().doubleValue(), new Double(100).doubleValue()).longValue(), orderItem.getQuantity());
            goodsDetailList.add(goods);
        }

//        // 创建一个商品信息,参数含义分别为商品id(使用国标)、名称、单价(单位为分)、数量,如果需要添加商品类别,详见GoodsDetail
//        GoodsDetail goods1 = GoodsDetail.newInstance("goods_id001", "xxx小面包", 1000, 1);
//        // 创建好一个商品后添加至商品明细列表
//        goodsDetailList.add(goods1);
//
//        // 继续创建并添加第一条商品信息,用户购买的产品为“黑人牙刷”,单价为5.00元,购买了两件
//        GoodsDetail goods2 = GoodsDetail.newInstance("goods_id002", "xxx牙刷", 500, 2);
//        goodsDetailList.add(goods2);

        // 创建扫码支付请求builder,设置请求参数
        AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
                .setSubject(subject).setTotalAmount(totalAmount).setOutTradeNo(outTradeNo)
                .setUndiscountableAmount(undiscountableAmount).setSellerId(sellerId).setBody(body)
                .setOperatorId(operatorId).setStoreId(storeId).setExtendParams(extendParams)
                .setTimeoutExpress(timeoutExpress)

                .setNotifyUrl(PropertiesUtil.getProperty("alipay.callback.url"))//支付宝服务器主动通知商户服务器里指定的页面http路径,根据需要设置
                .setGoodsDetailList(goodsDetailList);
        /** 一定要在创建AlipayTradeService之前调用Configs.init()设置默认参数
         *  Configs会读取classpath下的zfbinfo.properties文件配置信息,如果找不到该文件则确认该文件是否在classpath目录
         */
        Configs.init("zfbinfo.properties");

        /** 使用Configs提供的默认参数
         *  AlipayTradeService可以使用单例或者为静态成员对象,不需要反复new
         */
        AlipayTradeService tradeService = new AlipayTradeServiceImpl.ClientBuilder().build();

        AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder);

接着就是出参,二维码的生成,并展示给用户支付。图片展示通过上传到图片服务器的方式。所以前提得已经有一个ftp服务器和连接服务器的ftp工具类。

switch (result.getTradeStatus()) {
            case SUCCESS:
                log.info("支付宝预下单成功: )");

                AlipayTradePrecreateResponse response = result.getResponse();
                dumpResponse(response);

//                关键部分,把生成二维码上传到图片服务器
                File folder = new File(path);
                if (!folder.exists()) {

                    folder.setWritable(true);
                    folder.mkdirs();
                }

                // 需要修改为运行机器上的路径
                //替换s占位符
                String QRPath = String.format(path + "/qr-%s.png",
                        response.getOutTradeNo());
                String qrFileName = String.format("qr-%s.png", response.getOutTradeNo());
                //支付宝调用guava生成二维码
                ZxingUtils.getQRCodeImge(response.getQrCode(), 256, QRPath);

                File targetFile = new File(path, qrFileName);
                try {
                    FTPUtil.uploadFile(Lists.newArrayList(targetFile));
                } catch (IOException e) {
                    log.error("上传二维码异常", e);
                }
                log.info("QRPath:" + QRPath);
                String qrUrl = PropertiesUtil.getProperty("ftp.server.http.prefix") + targetFile.getName();
                resultMap.put("qrUrl", qrUrl);

                return ServerRespond.createBySuccess(resultMap);

            case FAILED:
                log.error("支付宝预下单失败!!!");
                return ServerRespond.createByErrorMessage("支付宝预下单失败");

            case UNKNOWN:
                log.error("系统异常,预下单状态未知!!!");
                return ServerRespond.createByErrorMessage("系统异常,预下单状态未知!!!");
            default:
                log.error("不支持的交易状态,交易返回异常!!!");
                return ServerRespond.createByErrorMessage("不支持的交易状态,交易返回异常!!!");
        }

3、支付宝回调接口

这个授权支付宝调用的接口,所以不能是本地ip,必须得有一个外网ip,最直接的方式是服务器上操作,但显然现在是没办法这样做的,于是采用了内网穿透的办法,内网穿透工具我采用ngrok,缺点是不能绑定固定域名。


image.png

授权回调接口:

public ServerRespond aliCallback(Map<String, String> params) {
    //处理回调数据
        Long orderNo = Long.parseLong(params.get("out_trade_no"));
        String tradeNo = params.get("trade_no");
        String tradeStatus = params.get("trade_status");
        Order order = orderMapper.selectByOrderNo(orderNo);
        if (order == null) {
            return ServerRespond.createByErrorMessage("寸金商场订单,回调忽略");
        }
        if (order.getStatus() >= Const.OrderStatus.PAID.getCode()) {
            return ServerRespond.createBySuccess("支付宝重复调用");
        }
        if (Const.alipayCallback.TRADE_STATUS_TRADE_SUCCESS.equals(tradeStatus)) {
            order.setPaymentTime(DateTimeUtil.strToDate(params.get("gmt_payment")));
            order.setStatus(Const.OrderStatus.PAID.getCode());
            orderMapper.updateByPrimaryKeySelective(order);
        }
        PayInfo payInfo = new PayInfo();
        payInfo.setUserId(order.getUserId());
        payInfo.setOrderNo(order.getOrderNo());
        payInfo.setPayPlatform(Const.PayPlatFormEnum.ALIPAY.getCode());
        payInfo.setPlatformNumber(tradeNo);
        payInfo.setPlatformStatus(tradeStatus);
        payInfoMapper.insert(payInfo);
        return ServerRespond.createBySuccess();
    }

关于回调接口可以看看文档

image.png

第一步: 在通知返回参数列表中,除去sign、sign_type两个参数外,凡是通知返回回来的参数皆是待验签的参数。

这一步很重要,不然没办法验签,看源码,便可知sdk已经做了,接着组装StringBuffer,因为StringBuffer是线程安全的,可以以应付高并发操作。


image.png

第三步: 将签名参数(sign)使用base64解码为字节码串。

这一步sdk也做了。


image.png

第四步: 使用RSA的验签方法,通过签名字符串、签名参数(经过base64解码)及支付宝公钥验证签名。

然而需要注意的是上面这个方法实际上是不ok的,因为它的算法请求类型跟配置中的不一致。我们的请求算法类型是RSA2不是SHA1WithRSA。


image.png

然而还有一个函数重载允许多了一个可以选择加密类型的参数。


image.png

点击rsaCheck,当signType的值equal不同的值调用不同的方法,很明显第二个就是我们要的。
image.png

于是在控制器中就得这样写:


image.png

五、对接测试

完成接口编写后就是接口测试

从数据库中提一个未付款的订单号做测试

image.png

成功的话会返回一个付款二维码
image.png

打开该二维码:
image.png

如图则已经对接成功:
image.png

以上就是支付宝集成的主要过程,代码只是贴了一部分具体可以看这里

六、总结

虽然官方的说明已经够详细了,但是真正入手去做还是有很多坑,此次对接过程中学习很多,其中尤其要注意的是因为官方的关系,sdk必须放在lib下,为了打包时能够同其他依赖包一起打包,还需配置好对sdk的打包插件,其他支付宝需要的依赖包如果用maven引入的话尽量保持版本一致或者一起跟支付宝sdk一同从Demo中复制过来放在lib包下

推荐阅读更多精彩内容