Bootstrap

毕设项目|基于SpringBoot开发苍穹外卖系统

先看演示视频

基于Spring boot开发的外卖系统

项目技术选型

个人感觉这个项目给我带来的收获是熟悉了之前的知识点。然后也学到了一些小的框架使用,例如:Spring Task、Spring Cache,最重要的锻炼了调用第三方接口和练习了通过产品原型设计接口等。

代码获取

关注公众号,回复Java,就可以获取GitHub的下载地址。

项目中遇到的一些问题

Mybatis在解析Integer值为0时会认为是空串

在进行编写代码的时候,Mybatis 在解析Integer值为0时会认为是空串,并且不止是Integer,还有Float型,Double型。

本质的原因是因为OGNL表达式对空字符串的解析了。

参考地址

微信支付的小问题

因为没有营业执照,所以无法完成微信支付的功能测试,并且难以完成接下来的项目开发,所以就说一下解决方案。

先来理解一下完整的代码和流程,这是一张示意图

前面的1、2、3步就是一个正常的订单生成操作,就不需要多解释了。

第四步:申请微信支付,这个时候访问的是我们的外卖系统。

第五步:调用微信下单接口让它返回一个预支付交易标识并且处理数据返回给微信小程序,而在这边调用微信接口、处理数据、返回数据都是由外卖系统操作的。看代码!


    @PutMapping("/payment")
    @ApiOperation("订单支付")
    public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        log.info("订单支付:{}", ordersPaymentDTO);
        OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
        log.info("生成预支付交易单:{}", orderPaymentVO);
        return Result.success(orderPaymentVO);
    }

现在是由Controller层接收到小程序发送来的两个数据:一个是订单号,一个是支付方式并且封装进了DTO中,随后调用了payment这个方法


    public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();
        User user = userMapper.getById(userId);
        //调用微信支付接口,生成预支付交易单
        JSONObject jsonObject = weChatPayUtil.pay(
                ordersPaymentDTO.getOrderNumber(), //商户订单号
                new BigDecimal(0.01), //支付金额,单位 元
                "苍穹外卖订单", //商品描述
                user.getOpenid() //微信用户的openid
        );
        if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
            throw new OrderBusinessException("该订单已支付");
        }

        OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
        vo.setPackageStr(jsonObject.getString("package"));

        return vo;
    }

随后在Service层中我们调用了weChatPayUtil这个工具类的.pay这个方法,这个时候我们已经在整备参数,为后续的第五步,第十三步做好整备

  /**
     * 小程序支付
     *
     * @param orderNum    商户订单号
     * @param total       金额,单位 元
     * @param description 商品描述
     * @param openid      微信用户的openid
     * @return
     */
    public JSONObject pay(String orderNum, BigDecimal total, String description, String openid) throws Exception {
        //统一下单,生成预支付交易单
        String bodyAsString = jsapi(orderNum, total, description, openid);
        //解析返回结果
        JSONObject jsonObject = JSON.parseObject(bodyAsString);
        System.out.println(jsonObject);

        String prepayId = jsonObject.getString("prepay_id");
        if (prepayId != null) {
            String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
            String nonceStr = RandomStringUtils.randomNumeric(32);
            ArrayList<Object> list = new ArrayList<>();
            list.add(weChatProperties.getAppid());
            list.add(timeStamp);
            list.add(nonceStr);
            list.add("prepay_id=" + prepayId);
            //二次签名,调起支付需要重新签名
            StringBuilder stringBuilder = new StringBuilder();
            for (Object o : list) {
                stringBuilder.append(o).append("\n");
            }
            String signMessage = stringBuilder.toString();
            byte[] message = signMessage.getBytes();

            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign(PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath()))));
            signature.update(message);
            String packageSign = Base64.getEncoder().encodeToString(signature.sign());

            //构造数据给微信小程序,用于调起微信支付
            JSONObject jo = new JSONObject();
            jo.put("timeStamp", timeStamp);
            jo.put("nonceStr", nonceStr);
            jo.put("package", "prepay_id=" + prepayId);
            jo.put("signType", "RSA");
            jo.put("paySign", packageSign);

            return jo;
        }
        return jsonObject;
    }

可以看到第十二行有一行代码非常重要,调用了jsapi这个方法然后返回一个预支付交易单

    private String jsapi(String orderNum, BigDecimal total, String description, String openid) throws Exception {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("appid", weChatProperties.getAppid());
        jsonObject.put("mchid", weChatProperties.getMchid());
        jsonObject.put("description", description);
        jsonObject.put("out_trade_no", orderNum);
        jsonObject.put("notify_url", weChatProperties.getNotifyUrl());

        JSONObject amount = new JSONObject();
        amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
        amount.put("currency", "CNY");

        jsonObject.put("amount", amount);

        JSONObject payer = new JSONObject();
        payer.put("openid", openid);

        jsonObject.put("payer", payer);

        String body = jsonObject.toJSONString();
        return post(JSAPI, body);
    }

这个时候是在整备调用微信支付api的参数,并且将这些参数整合起来交给了一个post方法。

这边可以注意一个点我们封装的参数中有一个叫做notify_url的参数,这个参数就是如果支付成功的话,那么微信会调用这个参数的接口来告诉系统支付成功了

 private String post(String url, String body) throws Exception {
        CloseableHttpClient httpClient = getClient();

        HttpPost httpPost = new HttpPost(url);
        httpPost.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());
        httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());
        httpPost.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo());
        httpPost.setEntity(new StringEntity(body, "UTF-8"));

        CloseableHttpResponse response = httpClient.execute(httpPost);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            return bodyAsString;
        } finally {
            httpClient.close();
            response.close();
        }
    }

也是通过HttpClient完成的接口调用

当上面的方法走完后,这个时候第六步:返回预支付交易标识已经完成了,随后我们将数据进行处理和签名就可以直接返回支付参数。也就是第八步

随后用户输入密码后支付成功,小程序会直接绕过我们的系统调用微信后台,完成第十步

随后微信后台会返回给小程序支付结果;看前端代码

  } else {
        // 如果支付成功进入成功页
        clearTimeout(this.times);
        var params = {
          orderNumber: this.orderDataInfo.orderNumber,
          payMethod: this.activeRadio === 0 ? 1 : 2 };

        (0, _api.paymentOrder)(params).then(function (res) {
          if (res.code === 1) {

            wx.requestPayment({
              nonceStr: res.data.nonceStr,
              package: res.data.packageStr,
              paySign: res.data.paySign,
              timeStamp: res.data.timeStamp,
              signType: res.data.signType,
              success:function(res){
                wx.showModal({
                  title: '提示',
                  content: '支付成功',
                  success:function(){
                    uni.redirectTo({url: '/pages/success/index?orderId=' + _this.orderId });
                  }
                })
                console.log('支付成功!')
              }
            })


            //uni.redirectTo({url: '/pages/success/index?orderId=' + _this.orderId });

          } else {
            wx.showModal({
              title: '提示',
              content: res.msg
            })
          }
        });

这个时候还没完,如果支付成功的话,微信后端还会开始回调我们系统的接口,也就是我们封装的notify_url的参数,随后访问这个接口

    @RequestMapping("/paySuccess")
    public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //读取数据
        String body = readData(request);
        log.info("支付成功回调:{}", body);

        //数据解密
        String plainText = decryptData(body);
        log.info("解密后的文本:{}", plainText);

        JSONObject jsonObject = JSON.parseObject(plainText);
        String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
        String transactionId = jsonObject.getString("transaction_id");//微信支付交易号

        log.info("商户平台订单号:{}", outTradeNo);
        log.info("微信支付交易号:{}", transactionId);

        //业务处理,修改订单状态、来单提醒
        orderService.paySuccess(outTradeNo);

        //给微信响应
        responseToWeixin(response);
    }

当这个接口被调用后,我们就会调用paySuccess这个方法修改数据库中订单的状态,也就完成了第十三步和第十四步

而因为我们没有办法完成这个流程,所以可以直接进行跳过某些步骤,当用户下单的时候,我们直接调用paySuccess这个方法进行修改状态

Controller层

    @PutMapping("/payment")
    @ApiOperation("订单支付")
    public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        log.info("订单支付:{}", ordersPaymentDTO);
        OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
        log.info("生成预支付交易单:{}", orderPaymentVO);

        //TODO 实际开发需要删除以下代码,模拟交易成功,修改数据库地址
        orderService.paySuccess(ordersPaymentDTO.getOrderNumber());
        log.info("模拟交易成功{}",ordersPaymentDTO.getOrderNumber());
        return Result.success(orderPaymentVO);
    }

Service层


    public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();
        User user = userMapper.getById(userId);
//TODO 实际开发需要解除以下代码,并且将JSONObject空对象进行删除
//        //调用微信支付接口,生成预支付交易单
//        JSONObject jsonObject = weChatPayUtil.pay(
//                ordersPaymentDTO.getOrderNumber(), //商户订单号
//                new BigDecimal(0.01), //支付金额,单位 元
//                "苍穹外卖订单", //商品描述
//                user.getOpenid() //微信用户的openid
//        );
        JSONObject jsonObject = new JSONObject();
        if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
            throw new OrderBusinessException("该订单已支付");
        }

        OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
        vo.setPackageStr(jsonObject.getString("package"));

        return vo;
    }

前端:

    } else {
        // 如果支付成功进入成功页
        clearTimeout(this.times);
        var params = {
          orderNumber: this.orderDataInfo.orderNumber,
          payMethod: this.activeRadio === 0 ? 1 : 2 };

        (0, _api.paymentOrder)(params).then(function (res) {
          if (res.code === 1) {
            //直接调用支付成功页面,实际开发需删除代码,并解除注释代码
            wx.showModal({
              title: '提示',
              content: '支付成功',
              success:function(){
                uni.redirectTo({url: '/pages/success/index?orderId' + _this.orderId});
              }
            })
            console.log('支付成功!')
            // wx.requestPayment({
            //   nonceStr: res.data.nonceStr,
            //   package: res.data.packageStr,
            //   paySign: res.data.paySign,
            //   timeStamp: res.data.timeStamp,
            //   signType: res.data.signType,
            //   success:function(res){
            //     wx.showModal({
            //       title: '提示',
            //       content: '支付成功',
            //       success:function(){
            //         uni.redirectTo({url: '/pages/success/index?orderId=' + _this.orderId });
            //       }
            //     })
            //     console.log('支付成功!')
            //   }
            // })


            //uni.redirectTo({url: '/pages/success/index?orderId=' + _this.orderId });

          } else {
            wx.showModal({
              title: '提示',
              content: res.msg
            })
          }
        });

百度地图接口调用

其实很简单,至于为什么要写出来,因为这是第一次自己一个人看着api文档调用接口的实战,所以记录一下。

要完成的业务是计算用户的收货地址和店铺的地址,如果超过5公里那么就禁止下单

  /**
     * 判断是否超出距离
     */
    public Integer exceededOrNot(String userAddress) {
        //两者进行计算,可以使用百度地图的骑行路线规划 进行计算
        Map map = new HashMap<>();
        map.put("ak",baiduAk);
        map.put("origin",Geocoding(shopAddress));
        map.put("destination",Geocoding(userAddress));
        map.put("steps_info","0");
        String s = HttpClientUtil.doGet(drivingUrl, map);
        JSONObject jsonObject = JSONObject.parseObject(s);
        if (!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("配送路线规划失败");
        }
        //先获取到返回的结果集
        return (Integer)jsonObject.getJSONObject("result").getJSONArray("routes").getJSONObject(0).get("distance");



    }

    public String Geocoding(String address){
        Map map = new HashMap<>();
        map.put("address",address);
        map.put("ak",baiduAk);
        map.put("output","json");
        //调用百度的地理编码坐标 进行转换为经纬度
        String s = HttpClientUtil.doGet(url, map);
        JSONObject jsonObject = JSONObject.parseObject(s);
        //status  int  本次API访问状态,如果成功返回0,如果失败返回其他数字。
        if (!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("地址解析失败");
        }
        JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location");
        Float lng = location.getFloat("lng");
        Float lat = location.getFloat("lat");
        return lat + "," + lng;
    }

在这边要注意一个坑,就是当你调用第一个接口完成地理编码坐标转换成经纬度的时候,返回的是lng+lat,但是在你调用骑行路线规划进行计算距离的时候,是要先传lat在传lng!!!

图片

我的粗心让我浪费了一个小时在这上面

Nginx反向代理和负载均衡的概念

前端的访问路径是:http://localhost/api/employee/login

后端的路径是:http://localhost/8080/admin/employee/login

为什么请求路径都不相同但是可以请求成功?

  • 因为nginx进行了反向代理。简单的理解就是将前端的请求转发到了后端。也就是说前端一开始请求的并不是后端而是nginx

为什么要反向代理?而不是直接访问后端?

  • 可以提高访问速度;在nginx可以进行缓存。例如我们访问同一个接口地址,那么这个时候就不需要在访问服务器可以直接返回数据

  • 可以进行负载均衡;就是把大量的访问数据均衡的分配给每一台服务器

  • 保证后端的服务安全;避免了后端地址的暴露,只能通过nginx进行反向代理访问

  • 也可以用于SSL终止,解密传入的SSL请求,然后将请求转发给后端服务器,减轻了后端服务器的计算负担

  • 提供一个统一的入口点,将所有请求路由到后端服务器,从而简化了网络架构

nginx反向代理的配置方式:在conf文件夹->nginx.conf文件中

负载均衡的配置:就是在原先的基础上多了一些配置,因为它也是基于反向代理实现的

总结:

学到了一些小框架的使用,就不慢慢说了

然后这个标题的话,其实我是标题党,但是这个项目确实不错!

;