美文网首页程序员
SSM框架学习日记(8)——支付模块

SSM框架学习日记(8)——支付模块

作者: 糯米团子_大芒果 | 来源:发表于2018-07-29 11:08 被阅读0次

    支付模块

    支付宝demo

    我们需要集成支付宝,会需要一些支付宝的文档和沙箱环境,一步一步看吧
    我们先去蚂蚁金服开放平台下载一个当面付的demo

    demo
    在自己的环境下看能不能跑起来,导入到idea之后,打开demo里的zfbinfo.properties,如图所示

    open_api_domain = https://openapi.alipaydev.com/gateway.do
    这个是支付宝沙箱的网关

    pid = 2088102176227840
    这个是商户UID

    appid = 2016091800542227
    这个就是appid

    沙箱文档中我们可以详细的看到步骤,关于RSA2密钥,我们可以下载提供给我们的工具,下载window版或者mac版,把生成的公钥和私钥放到配置文件相应的位置

    private_key = MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYw....
    public_key = MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB....

    然后回到我们的沙箱应用,点击查看应用公钥 在弹出的弹窗里把公钥复制到应用公钥里 然后点击保存之后发现页面刷新了一下,点击查看支付宝公钥,就得到了支付宝的公钥 然后复制支付宝公钥粘贴到配置文件里的

    SHA256withRsa对应支付宝公钥
    alipay_public_key = MIIBIjA.....

    下面的配置就默认就好了,run一下Main,可以看见如下信息

    就代表这个demo跑起来了,有兴趣的话可以把返回的https://qr.alipay.com/bax05682yyt0hbpqdsex0084用二维码生成器生成一下,再用支付宝提供的安卓版沙箱支付宝扫一扫看看结果

    集成到项目

    从demo里把支付宝需要的jar包复制到项目WEB-INF的lib里,我们只需要复制图中选中的这四个



    因为下面的那些是公用的,我们在pom.xml里配置就好了

    <!-- alipay -->
        <dependency>
          <groupId>commons-codec</groupId>
          <artifactId>commons-codec</artifactId>
          <version>1.10</version>
        </dependency>
        <dependency>
          <groupId>commons-configuration</groupId>
          <artifactId>commons-configuration</artifactId>
          <version>1.10</version>
        </dependency>
        <dependency>
          <groupId>commons-lang</groupId>
          <artifactId>commons-lang</artifactId>
          <version>2.6</version>
        </dependency>
        <dependency>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
          <version>1.1.1</version>
        </dependency>
        <dependency>
          <groupId>com.google.zxing</groupId>
          <artifactId>core</artifactId>
          <version>2.1</version>
        </dependency>
        <dependency>
          <groupId>com.google.code.gson</groupId>
          <artifactId>gson</artifactId>
          <version>2.3.1</version>
        </dependency>
        <dependency>
          <groupId>org.hamcrest</groupId>
          <artifactId>hamcrest-core</artifactId>
          <version>1.3</version>
        </dependency>
    

    还有一点就是需要在pom里增加如下的配置,因为在部署到服务器的时候,需要将lib下那四个文件一并打包上去,如果没有配置到时候部署到服务器的时候就有一堆报错

    <build>
        <finalName>mmall</finalName>
        <plugins>
          <!-- geelynote maven的核心插件之-complier插件默认只支持编译Java 1.4,因此需要加上支持高版本jre的配置,在pom.xml里面加上 增加编译插件 -->
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
              <encoding>UTF-8</encoding>
              <compilerArguments>
                <extdirs>${project.basedir}/src/main/webapp/WEB-INF/lib</extdirs>
              </compilerArguments>
            </configuration>
          </plugin>
        </plugins>
      </build>
    

    支付

    然后照旧新建controller,service,支付和订单紧密联系,所以放在同一个controller下,要通过request拿到上下文拿到upload的路径放二维码

    @Controller
    @RequestMapping("/order/")
    public class OrderController {
        private static  final Logger logger = LoggerFactory.getLogger(OrderController.class);
    
        @Autowired
        private IOrderService iOrderService;
    
        @RequestMapping("pay.do")
        @ResponseBody
        public ServerResponse pay(HttpSession session, Long orderNo, HttpServletRequest request){
            User user = (User)session.getAttribute(Const.CURRENT_USER);
            if(user ==null){
                return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc());
            }
            String path = request.getSession().getServletContext().getRealPath("upload");
            return iOrderService.pay(orderNo,user.getId(),path);
        }
    }
    

    在service里写pay方法,首先验证一下该用户有没有这个订单,查得到的话再把订单号插到map里,然后去demo里copy生成支付二维码的方法过来

    @Service("iOrderService")
    public class OrderServiceImpl implements IOrderService {
    
        @Autowired
        private OrderMapper orderMapper;
    
        public ServerResponse pay(Long orderNo, Integer userId, String path){
            Map<String ,String> resultMap = Maps.newHashMap();
            Order order = orderMapper.selectByUserIdAndOrderNo(userId,orderNo);
            if(order == null){
                return ServerResponse.createByErrorMessage("用户没有该订单");
            }
            resultMap.put("orderNo",String.valueOf(order.getOrderNo()));
            ....
        }
    }
    
    找到demo里的生成支付二维码方法,把所有的属性都copy到pay方法下,再一个一个改
    1. 第一个订单号,改成我们商城的订单号

    String outTradeNo = order.getOrderNo().toString();

    1. 第二个订单标题,我们自己拼装一个

    String subject = new StringBuilder().append("kamisama 扫码支付,订单号:").append(outTradeNo).toString();

    1. 第三个订单总价钱,从订单里拿

    String totalAmount = order.getPayment().toString();

    1. 不可打折金额和Id不改

    String undiscountableAmount = "0";
    String sellerId = "";

    1. 订单描述自己拼装,下面的直到商品明细列表之前的不改

    // 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品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";

    1. 商品详细列表,用for循环把商品详情用支付宝的GoodsDetail.newInstance(...)一个个添加到支付宝的集合中去

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

    // 商品明细列表,需填写购买商品详细信息,
    List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();
    
    List<OrderItem> orderItemList = orderItemMapper.getByOrderNoUserId(orderNo,userId);
    
    然后在orderItemMapper里新增一个新的查询,selectByOrderNoUserId,通过订单号和用户id查询出相应的orderItem,然后for循环去拼装每一个goods,从demo里或者到newInstance方法里可以看到需要的参数分别是商品id,商品名称,价格(单位为分),和数量

    所以在for循环里拼装好goods添加到list里,价钱单位转换为分的时候用到了乘法所以要用我们之前写好的BigDecimalUtil里的mul()方法

    for(OrderItem orderItem : orderItemList){
        // 创建一个商品信息,参数含义分别为商品id(使用国标)、名称、单价(单位为分)、数量,如果需要添加商品类别,详见GoodsDetail
         GoodsDetail goods = GoodsDetail.newInstance(orderItem.getProductId().toString(), orderItem.getProductName(),
         BigDecimalUtil.mul(orderItem.getCurrentUnitPrice().doubleValue(),new Double(100).doubleValue()).longValue(),
                        orderItem.getQuantity());
         goodsDetailList.add(goods);
    }
    
    1. 创建扫码支付请求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"))
    .setGoodsDetailList(goodsDetailList);

    参数都是上面配置的那些,这里的setNotifyUrl()是设置支付宝回调地址,需要在沙箱里配置,然后把地址写在mmall.properties里,读取时用PropertiesUtil读取就可以了

    mmall.properties
    这里有个点要注意的,支付宝的回调地址可以是域名也可以是ip地址,所以如果没有服务器的话,就用花生壳等外网穿透软件,如果有服务器的话并且有域名的话,那就用域名好了,如果没有域名那就把服务器tomcat监听的端口改为80端口,因为支付宝的回调地址不允许ip+端口的形式,只有ip的话就这么处理
    1. 现在代码里tradeService,报错,那么在demo里,找到tradeService,可以看到是声明了一个静态变量然后再静态块里初始化,所以我们把tradeService这部分代码copy到自己的代码里去


      demo

      所以把tradeService这部分代码copy到自己的代码里去

    @Service("iOrderService")
    public class OrderServiceImpl implements IOrderService {
        ...
        private static  AlipayTradeService tradeService;
        static {
            /** 一定要在创建AlipayTradeService之前调用Configs.init()设置默认参数
             *  Configs会读取classpath下的zfbinfo.properties文件配置信息,如果找不到该文件则确认该文件是否在classpath目录
             */
            Configs.init("zfbinfo.properties");
            /** 使用Configs提供的默认参数
             *  AlipayTradeService可以使用单例或者为静态成员对象,不需要反复new
             */
            tradeService = new AlipayTradeServiceImpl.ClientBuilder().build();
        }
        ...
    }
    
    1. 然后发现代码里dumpResponse方法报错,是一个打印应答的方法,也直接从demo拿过来用了
    // 简单打印应答
        private void dumpResponse(AlipayResponse response) {
            if (response != null) {
                log.info(String.format("code:%s, msg:%s", response.getCode(), response.getMsg()));
                if (StringUtils.isNotEmpty(response.getSubCode())) {
                    log.info(String.format("subCode:%s, subMsg:%s", response.getSubCode(),
                        response.getSubMsg()));
                }
                log.info("body:" + response.getBody());
            }
        }
    

    如果下单成功,那就要生成二维码。
    先创建一个File,指向传过来的path,判断不存在后,然后给予写权限然后新建,

    logger.info("支付宝预下单成功: )");
    
    AlipayTradePrecreateResponse response = result.getResponse();
    dumpResponse(response);
    
    File folder = new File(path);
    if(!folder.exists()){
        folder.setWritable(true);
        folder.mkdirs();
    }
    

    然后生成二维码并上传到服务器ZxingUtils.getQRCodeImge()是支付宝封装好的方法,生成二维码,上传之后把url返回回去

    // 需要修改为运行机器上的路径
    String qrPath = String.format(path+"/qr-%s.png",response.getOutTradeNo());
    String qrFileName = String.format("qr-%s.png",response.getOutTradeNo());
    ZxingUtils.getQRCodeImge(response.getQrCode(), 256, qrPath);
    
    File targetFile = new File(path,qrFileName);
    try {
        FTPUtil.uploadFile(Lists.newArrayList(targetFile));
    } catch (IOException e) {
        logger.error("上传二维码异常",e);
    }
    logger.info("qrPath:" + qrPath);
    
    String qrUrl = PropertiesUtil.getProperty("ftp.server.http.prefix")+targetFile.getName();
    resultMap.put("qrUrl",qrUrl);
    return ServerResponse.createBySuccess(resultMap);
    

    把case里的break都换成我们自己的消息return就好啦

    case FAILED:
        logger.error("支付宝预下单失败!!!");
        return ServerResponse.createByErrorMessage("支付宝预下单失败!!!");
    
    case UNKNOWN:
        logger.error("系统异常,预下单状态未知!!!");
        return ServerResponse.createByErrorMessage("系统异常,预下单状态未知!!!");
    
    default:
        logger.error("不支持的交易状态,交易返回异常!!!");
        return ServerResponse.createByErrorMessage("不支持的交易状态,交易返回异常!!!");
    

    到这里pay方法就写完了,在controller里调用就好了

        @RequestMapping("pay.do")
        @ResponseBody
        public ServerResponse pay(HttpSession session, Long orderNo, HttpServletRequest request){
            User user = (User)session.getAttribute(Const.CURRENT_USER);
            if(user ==null){
                return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc());
            }
            String path = request.getSession().getServletContext().getRealPath("upload");
            return iOrderService.pay(orderNo,user.getId(),path);
        }
    

    回调方法

    controller,根据alipay的要求的返回来返回,所以返回一个Object,参数只有request,应为支付宝回调把数据放在request里,取出来放map里就好了,用一个迭代器遍历一下,取出key和value,然后把value数组拼接到一个字符串,用逗号分割就变成了,value1,value2,value3 这种形式,然后把key和拼接好的value字符串放到另一个map里

        @RequestMapping("alipay_callback.do")
        @ResponseBody
        public Object alipayCallback(HttpServletRequest request){
            Map<String,String> params = Maps.newHashMap();
    
            Map requestParams = request.getParameterMap();
            for(Iterator 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]+",";
                }
                params.put(name,valueStr);
            }
            logger.info("支付宝回调,sign:{},trade_status:{},参数:{}",params.get("sign"),params.get("trade_status"),params.toString());
        }
    

    接下来呢非常重要,非常重要,要验证回调的正确性,是不是支付宝发的,并且呢还要避免重复通知
    看看支付宝的文档怎么说

    支付宝文档
    那么按步骤做就行了,有一些部分sdk已经做了,去看看源码就知道了 依赖jar包里验证的部分源码 RSA2check
    可以看到源码里并没有去掉sign_type,所以只能手动remove掉。check里第一个参数传我们自己组装的map,然后是支付宝公钥,然后是字符集,最后是sign_type(配置文件里有)
        @RequestMapping("alipay_callback.do")
        @ResponseBody
        public Object alipayCallback(HttpServletRequest request){
            ....
            //非常重要,验证回调的正确性,是不是支付宝发的.并且呢还要避免重复通知.
            params.remove("sign_type");
            try {
                boolean alipayRSACheckedV2 = AlipaySignature.rsaCheckV2(params, Configs.getAlipayPublicKey(),"utf-8",Configs.getSignType());
                if(!alipayRSACheckedV2){
                    return ServerResponse.createByErrorMessage("非法请求,验证不通过");
                }
            } catch (AlipayApiException e) {
                logger.error("支付宝验证回调异常",e);
            }
            //todo 验证各种数据
    
            //
    }
    

    验证通过之后还得验证各种数据,就先放个todo之后再做吧



    所有都验证完了之后,就要有一些订单状态库存之类的处理了,再service里新增方法,先判断订单号是否有效,然后判断订单状态,如果是交易成功就把订单状态置成已付款。然后组装payinfo

        public ServerResponse 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 ServerResponse.createByErrorMessage("非商城的订单,回调忽略");
            }
            if(order.getStatus() >= Const.OrderStatusEnum.PAID.getCode()){
                return ServerResponse.createBySuccess("支付宝重复调用");
            }
            if(Const.AlipayCallback.TRADE_STATUS_TRADE_SUCCESS.equals(tradeStatus)){
                order.setPaymentTime(DateTimeUtil.strToDate(params.get("gmt_payment")));
                order.setStatus(Const.OrderStatusEnum.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 ServerResponse.createBySuccess();
        }
    

    controller调用一下

    ServerResponse serverResponse = iOrderService.aliCallback(params);
    if(serverResponse.isSuccess()){
        return Const.AlipayCallback.RESPONSE_SUCCESS;
    }
    return Const.AlipayCallback.RESPONSE_FAILED;
    

    返回成功,这样回调就做完了

    订单状态接口

    用户扫完二维码付款之后,要查一下是不是付款成功了,在controller新增

        @RequestMapping("query_order_pay_status.do")
        @ResponseBody
        public ServerResponse<Boolean> queryOrderPayStatus(HttpSession session, Long orderNo){
            User user = (User)session.getAttribute(Const.CURRENT_USER);
            if(user ==null){
                return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc());
            }
            ServerResponse serverResponse = iOrderService.queryOrderPayStatus(user.getId(),orderNo);
            if(serverResponse.isSuccess()){
                return ServerResponse.createBySuccess(true);
            }
            return ServerResponse.createBySuccess(false);
        }
    

    然后在service里新增一下代码,让controller调用就行了

        public ServerResponse queryOrderPayStatus(Integer userId,Long orderNo){
            Order order = orderMapper.selectByUserIdAndOrderNo(userId,orderNo);
            if(order == null){
                return ServerResponse.createByErrorMessage("用户没有该订单");
            }
            if(order.getStatus() >= Const.OrderStatusEnum.PAID.getCode()){
                return ServerResponse.createBySuccess();
            }
            return ServerResponse.createByError();
        }
    

    这样与支付宝对接的三个接口就都写完了

    相关文章

      网友评论

        本文标题:SSM框架学习日记(8)——支付模块

        本文链接:https://www.haomeiwen.com/subject/wleqmftx.html