苍穹外卖小结下

发布时间 2023-11-13 20:52:43作者: 今晚三分饱

一.小程序端

1.1HttpClient

1.1.1 介绍

HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。

下载地址: http://hc.apache.org/downloads.cgi

1.1.2 HttpClient作用
  • 发送HTTP请求
  • 接收响应数据
1.1.3 如何使用

HttpClient的maven坐标:

<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
	<version>4.5.13</version>
</dependency>

注:在之前在使用OSS时导入相关依赖,其中已经包含了HttpClient相关依赖,所以这部分可导可不导

aliyun-sdk-oss坐标:

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
</dependency>

HttpClient的核心API:

  • HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
  • HttpClients:可认为是构建器,可创建HttpClient对象。
  • CloseableHttpClient:实现类,实现了HttpClient接口。
  • HttpGet:Get方式请求类型。
  • HttpPost:Post方式请求类型。

HttpClient发送请求步骤:

  • 创建HttpClient对象
  • 创建Http请求对象
  • 调用HttpClient的execute方法发送请求
1.1.4 GET方式请求实现
  1. 创建HttpClient对象

  2. 创建请求对象

  3. 发送请求,接受响应结果

  4. 解析结果

  5. 关闭资源

    编写测试代码:

package com.sky.test;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class HttpClientTest {

    /**
     * 测试通过httpclient发送GET方式的请求
     */
    @Test
    public void testGET() throws Exception{
        //创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
    
        //创建请求对象
        HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
    
        //发送请求,接受响应结果
        CloseableHttpResponse response = httpClient.execute(httpGet);
    
        //获取服务端返回的状态码
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("服务端返回的状态码为:" + statusCode);
    
        HttpEntity entity = response.getEntity();
        String body = EntityUtils.toString(entity);
        System.out.println("服务端返回的数据为:" + body);
    
        //关闭资源
        response.close();
        httpClient.close();
    }

}

在访问http://localhost:8080/user/shop/status请求时,需要提前启动项目。

1.1.5 POST方式请求实现

在HttpClientTest中添加POST方式请求方法,相比GET请求来说,POST请求若携带参数需要封装请求体对象,并将该对象设置在请求对象中。

实现步骤:

  1. 创建HttpClient对象
  2. 创建请求对象
  3. 发送请求,接收响应结果
  4. 解析响应结果
  5. 关闭资源

编写测试代码:

    //测试通过httpclient发送POST方式的请求

    @Test
    public void testPOST() throws Exception{
       // 创建httpclient对象
       CloseableHttpClient httpClient = HttpClients.createDefault();

        //创建请求对象
        HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
    
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("username","admin");
        jsonObject.put("password","123456");
    
        StringEntity entity = new StringEntity(jsonObject.toString());
        //指定请求编码方式
        entity.setContentEncoding("utf-8");
        //数据格式
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
    
        //发送请求
        CloseableHttpResponse response = httpClient.execute(httpPost);
    
        //解析返回结果
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("响应码为:" + statusCode);
    
        HttpEntity entity1 = response.getEntity();
        String body = EntityUtils.toString(entity1);
        System.out.println("响应数据为:" + body);
    s
        //关闭资源
        response.close();
        httpClient.close();
    }


1.2微信小程序开发

1.21介绍

小程序是一种新的开放能力,开发者可以快速地开发一个小程序。可以在微信内被便捷地获取和传播,同时具有出色的使用体验。

官方网址:https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN

首先,在进行小程序开发时,需要先去注册一个小程序,在注册的时候,它实际上又分成了不同的注册的主体。我们可以以个人的身份来注册一个小程序,当然,也可以以企业政府、媒体或者其他组织的方式来注册小程序。那么,不同的主体注册小程序,最终开放的权限也是不一样的。比如以个人身份来注册小程序,是无法开通支付权限的。若要提供支付功能,必须是企业、政府或者其它组织等。所以,不同的主体注册小程序后,可开发的功能是不一样的。

然后,微信小程序我们提供的一些开发的支持,实际上微信的官方是提供了一系列的工具来帮助开发者快速的接入
并且完成小程序的开发,提供了完善的开发文档,并且专门提供了一个开发者工具,还提供了相应的设计指南,同时也提供了一些小程序体验DEMO,可以快速的体验小程序实现的功能。

最后,开发完一个小程序要上线,也给我们提供了详细地接入流程。

1.2.2准备工作

开发微信小程序之前需要做如下准备工作:

  • 注册小程序
  • 完善小程序信息
  • 下载开发者工具

1)注册小程序

注册地址:https://mp.weixin.qq.com/wxopen/waregister?action=step1

2)完善小程序信息

登录小程序后台:https://mp.weixin.qq.com/

3)查看小程序的 AppID

并且查看密钥(注意保存好

4)下载开发者工具

下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html

5)熟悉开发者工具布局

​ 设置不校验合法域名

注:开发阶段,小程序发出请求到后端的Tomcat服务器,若不勾选,请求发送失败。

1.2.3 小程序目录结构
  • 
        小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。主体部分由三个文件组成,必须放在项目的根目录。
      
      文件说明:
      
      - app.js: 必须存在,主要存放小程序的逻辑代码
      
      - app.json:必须存在,小程序配置文件,主要存放小程序的公共配置
      
      - app.wxss: 非必须存在,主要存放小程序公共样式表,类似于前端的CSS样式
      
      - js文件:必须存在,存放页面业务逻辑代码,编写的js代码。
      
      - json文件:非必须,存放页面相关的配置。
      
      - wxml文件:必须存在,存放页面结构,主要是做页面布局,页面效果展示的,类似于HTML页面。
      
      - wxss文件:非必须,存放页面样式表,相当于CSS文件。
    
    1.2.4编写测试用例

    1)编写小程序

    进入到index.wxml,编写页面布局

<view class="container">
  <view>{{msg}}</view>
   <view>
    <button type="default" bindtap="getUserInfo">获取用户信息</button>
    <image style="width: 100px;height: 100px;" src="{{avatarUrl}}"></image>
    {{nickName}}
  </view>
   <view>
    <button type="primary" bindtap="wxlogin">微信登录</button>
    授权码:{{code}}
  </view>
   <view>
    <button type="warn" bindtap="sendRequest">发送请求</button>
    响应结果:{{result}}
  </view>
</view>

进入到index.js,编写业务逻辑代码

Page({
  data:{
    msg:'hello world',
    avatarUrl:'',
    nickName:'',
    code:'',
    result:''
  },
  getUserInfo:function(){
    wx.getUserProfile({
      desc: '获取用户信息',
      success:(res) => {
        console.log(res.userInfo)
        this.setData({
          avatarUrl:res.userInfo.avatarUrl,
          nickName:res.userInfo.nickName
        })
      }
    })
  },
  wxlogin:function(){
    wx.login({
      success: (res) => {
        console.log("授权码:"+res.code)
        this.setData({
          code:res.code
        })
      }
    })
  },
  sendRequest:function(){
    //通过微信小程序向项目服务器Tomcat发送一个请求
    wx.request({
      url: 'http://localhost:8080/user/shop/status',
      method:'GET',
      success:(res) => {
        console.log("响应结果:" + res.data.data)
        this.setData({
          result:res.data.data
        })
      }
    })
  }})

点击编译按钮进行运行
为了防止小程序开发者滥用用户昵称和头像,官方停用了接口;如果想看效果需要切换到旧版基础库

2)发布小程序

小程序的代码都已经开发完毕,要将小程序发布上线,让所有的用户都能使用到这个小程序。
    点击上传按钮:
    指定版本号:
    上传成功:
当前小程序版本只是一个开发版本。
进到微信公众平台,打开版本管理页面。
需提交审核,变成审核版本,审核通过后,进行发布,变成线上版本。
1.2.4 导入小程序代码

导入提供的代码

AppID:使用自己的AppID

因为小程序要请求后端服务,需要修改为自己后端服务的ip地址和端口号(默认不需要修改)
common-->vendor.js-->搜索(ctrl+f)-->baseUri

- 微信登录流程
  微信登录:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

1. 小程序端,调用wx.login()获取code,就是授权码。
2. 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
3. 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
4. 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
5. 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
6. 小程序端,收到自定义登录态,存储storage。
7. 小程序端,后绪通过wx.request()发起业务请求时,携带token。
8. 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
9. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。

说明:
10. 调用https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html 获取 临时登录凭证code ,并回传到开发者服务器。
11. 调用https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 接口,
换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。
1.2.5微信登录流程

说明:

  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

1.2.6配置微信登录所需配置项
#定义相关配置

配置微信登录所需配置项:
application-dev.yml

在application-dev.yml配置文件中配置具体信息

#sky: 改成自己的小程序id和secret
  wechat:
    appid: wxffb3637a228223b8
    secret: 84311df9199ecacdf4f12d27b6b9522d

在application.yml调用dev。yml中的信息(下同)

这样可以避免输入错误,修改不方便等问题

#sky:
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}

配置为微信用户生成jwt令牌时使用的配置项:

#sky:
  #jwt:
    # 省略......
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication

用户端拦截器

  /**
   * jwt令牌校验的拦截器
   */
   @Component
   @Slf4j
   public class JwtTokenUserInterceptor implements HandlerInterceptor {

   @Autowired
   private JwtProperties jwtProperties;

   /**
    * 校验jwt
    *
    * @param request
    * @param response
    * @param handler
    * @return
    * @throws Exception
      */
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      //判断当前拦截到的是Controller的方法还是其他资源
      //==========================================
      //注意HandlerMethod导包:org.springframework.web.method.HandlerMethod
      if (!(handler instanceof org.springframework.web.method.HandlerMethod)) {
          //当前拦截到的不是动态方法,直接放行
          return true;
      }

      //1、从请求头中获取令牌
      String token = request.getHeader(jwtProperties.getUserTokenName());

      //2、校验令牌
      try {
          log.info("jwt校验:{}", token);
          Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
          Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
          log.info("当前用户的id:{}", userId);
          BaseContext.setCurrentId(userId); //放到ThreadLocal里边
          //3、通过,放行
          return true;
      } catch (Exception ex) {
          //4、不通过,响应401状态码
          response.setStatus(401);
          return false;
      }
      }
      }


在WebMvcConfiguration配置类中注册拦截器
    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;
	/**
     * 注册自定义拦截器
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        //.........

        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
}

1.3订单支付

要实现微信支付就需要注册微信支付的一个商户号,这个商户号是必须要有一家企业并且有正规的营业执照。只有具备了这些资质之后,才可以去注册商户号,才能开通支付权限。
微信支付产品:本项目选择小程序支付
参考:https://pay.weixin.qq.com/static/product/product_index.shtml
1.3.1微信小程序支付时序图:

1)微信支付相关接口:
JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步)

微信小程序调起支付:通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步)

2)完成微信支付有两个关键的步骤:

  • 第一个 就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。

  • 第二个 就是支付成功之后微信后台会给推送消息。

    解决:微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。

获取微信支付平台证书、商户私钥文件.

3)调用到商户系统

微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。

目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。

解决:内网穿透。通过cpolar软件可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。

1.3.2内网穿透

1)下载安装

下载地址:https://dashboard.cpolar.com/get-started

2)cpolar指定authtoken

复制authtoken:

https://dashboard.cpolar.com/auth

--->注册登录 --->验证---->复制自己的隧道 Authtoken

3)执行命令:
命令行输入:
cpolar.exe authtoken MmJiMTBiZDAtMz333330ZWI5LTlhOTQtODE1ZjcxNmZhOGRl

4)获取临时域名

执行命令:cpolar.exe http 8080

5)验证临时域名有效性

启动项目,访问接口文档:http://localhost:8080/doc.html 使用临时域名访问,证明临时域名生效

1.3.3微信支付相关配置

application-dev.yml

sky:
  wechat:
    appid: wxcd2e39f677fd30ba
    secret: 84fbfdf5ea288f0c432d829599083637
    mchid : 1561414331
    mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
    privateKeyFilePath: D:\apiclient_key.pem
    apiV3Key: CZBK51236435wxpay435434323FFDuv3
    weChatPayCertFilePath: D:\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
    notifyUrl: https://www.weixin.qq.com/wxpay/pay.php
    refundNotifyUrl: https://www.weixin.qq.com/wxpay/pay.php

application.yml

sky:
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}
    mchid : ${sky.wechat.mchid}
    mchSerialNo: ${sky.wechat.mchSerialNo}
    privateKeyFilePath: ${sky.wechat.privateKeyFilePath}
    apiV3Key: ${sky.wechat.apiV3Key}
    weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath}
    notifyUrl: ${sky.wechat.notifyUrl}
    refundNotifyUrl: ${sky.wechat.refundNotifyUrl}

注意:privateKeyFilePath和weChatPayCertFilePath两个路径指微信支付平台证书、商户私钥文件的存储路径

由于需要商户注册,所以这两个文件我没有,所以完成小程序支付功能我还没有实现。在后面用户统计模块,我是直接修改的数据库数据。

1.3.4 具体代码

WeChatProperties.java:读取配置

package com.sky.properties;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {

    private String appid; //小程序的appid
    private String secret; //小程序的秘钥
    private String mchid; //商户号
    private String mchSerialNo; //商户API证书的证书序列号
    private String privateKeyFilePath; //商户私钥文件
    private String apiV3Key; //证书解密的密钥
    private String weChatPayCertFilePath; //平台证书
    private String notifyUrl; //支付成功的回调地址
    private String refundNotifyUrl; //退款成功的回调地址

}

Service层代码

在OrderService.java中添加payment和paySuccess两个方法定义

/**
 * 订单支付
 * @param ordersPaymentDTO
 * @return
 */
OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;

/**
 * 支付成功,修改订单状态
 * @param outTradeNo
 */
void paySuccess(String outTradeNo);

在OrderServiceImpl.java中实现payment和paySuccess两个方法

 	@Autowired
    private UserMapper userMapper;
	@Autowired
    private WeChatPayUtil weChatPayUtil;
    /**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    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;
    }

    /**
     * 支付成功,修改订单状态
     *
     * @param outTradeNo
     */
    public void paySuccess(String outTradeNo) {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();

        // 根据订单号查询当前用户的订单
        Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);

        // 根据订单id更新订单的状态、支付方式、支付状态、结账时间
        Orders orders = Orders.builder()
                .id(ordersDB.getId())
                .status(Orders.TO_BE_CONFIRMED)
                .payStatus(Orders.PAID)
                .checkoutTime(LocalDateTime.now())
                .build();

        orderMapper.update(orders);
    }

PayNotifyController.java

/**
 * 支付回调相关接口
 */
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private WeChatProperties weChatProperties;

    /**
     * 支付成功回调
     *
     * @param request
     */
    @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);
    }

    /**
     * 读取数据
     *
     * @param request
     * @return
     * @throws Exception
     */
    private String readData(HttpServletRequest request) throws Exception {
        BufferedReader reader = request.getReader();
        StringBuilder result = new StringBuilder();
        String line = null;
        while ((line = reader.readLine()) != null) {
            if (result.length() > 0) {
                result.append("\n");
            }
            result.append(line);
        }
        return result.toString();
    }

    /**
     * 数据解密
     *
     * @param body
     * @return
     * @throws Exception
     */
    private String decryptData(String body) throws Exception {
        JSONObject resultObject = JSON.parseObject(body);
        JSONObject resource = resultObject.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String nonce = resource.getString("nonce");
        String associatedData = resource.getString("associated_data");

        AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //密文解密
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        return plainText;
    }

    /**
     * 给微信响应
     * @param response
     */
    private void responseToWeixin(HttpServletResponse response) throws Exception{
        response.setStatus(200);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("code", "SUCCESS");
        map.put("message", "SUCCESS");
        response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
        response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
        response.flushBuffer();
    }
}

二.定时任务SpringTask

2.1介绍

Spring Task是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

定位:定时任务框架
作用:定时自动执行某段Java代码

应用场景:

1). 信用卡每月还款提醒

2). 银行贷款每月还款提醒

3). 火车票售票系统处理未支付订单

4). 入职纪念日为用户发送通知

强调:只要是需要定时处理的场景都可以使用Spring Task

2.2cron表达式

  • cron其实就是一个字符串,通过cron表达式可以定义任务触发的时间

  • 构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义:秒、分钟、小时、日、月、周、年(可选),每部分的含义如下表所示:

组成部分 含义 取值范围
第一部分 Seconds (秒) 0-59
第二部分 Minutes(分) 0-59
第三部分 Hours(时) 0-23
第四部分 Day-of-Month(天) 1-31
第五部分 Month(月) 0-11或JAN-DEC
第六部分 Day-of-Week(星期) 1-7(1表示星期日)或SUN-SAT
第七部分 Year(年) 可选 1970-2099

1)举例:

2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022

说明:一般的值不同时设置,其中一个设置,另一个用?表示。

比如:描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。

为了描述这些信息,提供一些特殊的字符。这些具体的细节,我们就不用自己去手写,因为这个cron表达式,它其实有在线生成器。

cron表达式在线生成器:https://cron.qqe2.com/

2)通配符:

* 表示所有值;

? 表示未说明的值,即不关心它为何值;

- 表示一个指定的范围;

, 表示附加一个可能值;

/ 符号前表示开始时间,符号后表示每次递增的值;

3)cron表达式案例:

*/5 * * * * ? 每隔5秒执行一次

0 */1 * * * ? 每隔1分钟执行一次

0 0 5-15 * * ? 每天5-15点整点触发

0 0/3 * * * ? 每三分钟触发一次

0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发

0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发

0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

2.3使用步骤

1). 导入maven坐标 spring-context

2). 启动类添加注解 @EnableScheduling 开启任务调度

3). 自定义定时任务类

编写定时任务类:

进入sky-server模块中

/**
 * 自定义定时任务类
 */
@Component
@Slf4j
public class MyTask {

    /**
     * 定时任务 每隔5秒触发一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void executeTask(){
        log.info("定时任务开始执行:{}",new Date());
    }
}

开启任务调度:

启动类添加注解 @EnableScheduling

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching
@EnableScheduling
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}

三.WebSocket全双工通信

3.1介绍

WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。

HTTP协议和WebSocket协议对比:

  • HTTP是短连接
  • WebSocket是长连接
  • HTTP通信是单向的,基于请求响应模式
  • WebSocket支持双向通信
  • HTTP和WebSocket底层都是TCP连接

WebSocket缺点:

服务器长期维护长连接需要一定的成本
各个浏览器支持程度不一
WebSocket 是长连接,受网络限制比较大,需要处理好重连

结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用

WebSocket应用场景:

1). 直播弹幕

2). 网页聊天

3). 体育实况更新

4). 股票基金报价实时更新

3.2入门案例

下面通过一个入门案例来详细了解WebSocket

  • 需求:
  • 实现浏览器与服务器全双工通信。浏览器既可以向服务器发送消息,服务器也可主动向浏览器推送消息。

实现步骤:

1). 直接使用websocket.html页面作为WebSocket客户端

2). 导入WebSocket的maven坐标

3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信

4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件

5). 导入定时任务类WebSocketTask,定时向客户端推送数据

1). 定义websocket.html页面

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Demo</title>
</head>
<body>
    <input id="text" type="text" />
    <button onclick="send()">发送消息</button>
    <button onclick="closeWebSocket()">关闭连接</button>
    <div id="message">
    </div>
</body>
<script type="text/javascript">
    
    function abc() {
        //code
    }
    
    var websocket = null;
    //Math.random()返回一个0(包含)到 1(不包含)的浮点数
    //toString(32): 转成字符串
    //substr(): 从指定位置开始截取字符串
    var clientId = Math.random().toString(36).substr(2);

    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        //连接WebSocket节点
        websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
    }else{
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(){
        setMessageInnerHTML("连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data); //event.data是服务器返回的数据
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //监听窗口关闭事件:
    //当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //发送消息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
	
	//关闭连接
    function closeWebSocket() {
        websocket.close();
    }
</script>
</html>

2). 导入maven坐标

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

3). 定义WebSocket服务端组件

/**
 * WebSocket服务
 */
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存放会话对象
    private static Map<String, Session> sessionMap = new HashMap();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param sid
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 群发
     *
     * @param message
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}    

4). 定义配置类,注册WebSocket的服务端组件

 /**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

5). 定义定时任务类,定时向客户端推送数据

@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + LocalTime.now());
    }
}

四.注册百度地图服务

4.1注册百度地图服务

1.基于百度地图开放平台实现(https://lbsyun.baidu.com/)

2.注册账号--->控制台--->我的应用-->创建应用获取AK(服务端应用)--->调用接口
创建应用时:
类型:选服务端
#IP白名单:0.0.0.0/0 

3.相关接口
地理编码服务:https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding

地理编码服务(又名Geocoder)是一类Web API接口服务;
地理编码服务提供将结构化地址数据(如:北京市海淀区上地十街十号)转换为对应坐标点(经纬度)功能;

GET: https://api.map.baidu.com/geocoding/v3/?address=北京市海淀区上地十街10号&output=json&ak=您的ak

在url中传递3个参数即可,返回数据格式如下:Java中将返回的json字符串转成JSONObject

{ // JSONObject
    "status": 0, // jsonObject.getIntValue("status")
    "result": { //对象: jsonObject.getJSONObject("result")
        "location": { //对象: jsonObject.getJSONObject("location") 
            "lng": 116.3076223267197, //经度 getString("lng")
            "lat": 40.05682848596073 //纬度 getString("lat")
        },
        "precise": 1,
        "confidence": 80,
        "comprehension": 100,
        "level": "门址"
    }
}

路线规划服务:https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1

轻量级路线规划服务(又名DirectionLite API )是一套REST风格的Web服务API,以HTTP/HTTPS形式提供了路线规划服务。相较于Direction API,DirectionLite API更注重服务的高性能和接口的轻便简洁,满足基础的路线规划需求,并不具备Direciton API中的驾车多路线/未来出行和公交跨城规划等高级功能。DirectionLite API支持驾车、骑行、步行、公交路线规划,支持中国大陆地区。

GET: https://api.map.baidu.com/directionlite/v1/driving?origin=40.01116,116.339303&destination=39.936404,116.452562&steps_info=0&ak=您的AK

传递4个参数即可,返回数据如下:

{
    "status": 0, //getIntValue
    "message": "ok",
    "result": { //对象: getJSONObject
        "origin": {
            "lng": 116.33929505188,
            "lat": 40.011157363344
        },
        "destination": {
            "lng": 116.45255341058,
            "lat": 39.936401378723
        },
        "routes": [ //数组: result.getJSONArray("routes");
            {
                "distance": 18129, //getString 得到的两地间的距离
                "duration": 6193
            }
        ]
    }
}

4.2检验配送距离

商家门店地址可以配置在配置文件中,例如:application.yml

#sky:
  shop:
    address: 北京市海淀区上地十街10号
  baidu:
    ak: sdfsdfsdfsd #百度应用id

用户下单时添加校验代码:

//两项用途过少没有必要新建一个配置类 
@Value("${sky.shop.address}")//将yml文件中的属性赋值到shopAddress中
private String shopAddress;

@Value("${sky.baidu.ak}")
private String ak;

/**
 * 检查客户的收货地址是否超出配送范围
 * @param address 收货地址
 */
private void checkOutOfRange(String address) {

    //1、调用百度地理编码服务根据店铺地址获取经纬度
    String shopGeo = getGeoByAddress(shopAddress);

    //2、调用地理编码服务服务根据用户配送地址获取经纬度
    String userGeo = getGeoByAddress(address);

    //3、调用路线规划服务对两个地址进行规划,根据返回的距离判断是否在配送范围
    Integer distance = getDistance(shopGeo, userGeo);

    if(distance > 5000){
        //配送距离超过5000米
        throw new OrderBusinessException("超出配送范围: " + distance);
    }
}

编写两个私有方法:getGeoByAddress和getDistance,在方法中用HttpClientUtil.doGet()调用百度接口

/**
 * 根据详细地址获取经纬度坐标
 * 地理编码服务:https://lbsyun.baidu.com/index.php?title=webapi/gui
 *
 * @param address 详细地址(包含省市区)
 * @return 经纬度,格式为:纬度,经度;小数点后不超过6位,40.056878,116.30815
 */
private String getGeoByAddress(String address) {
    
    //1、使用map构建请求参数
    Map<String, String> map = new HashMap<>();
    map.put("address",address); //地址
    map.put("output","json");//指定返回数据格式
    map.put("ak",ak);
    
    //2、调用百度地图服务:获取经纬度坐标
    String json =
            HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
    log.info("地址: {},返回值:{}", address, json);
    
    //3、解析响应结果
    JSONObject jsonObject = JSON.parseObject(json);
    //status=0,表示成功
    if(jsonObject.getIntValue("status") != 0){
        throw new OrderBusinessException("地址解析失败");
    }
    //数据解析
    JSONObject location =
            jsonObject.getJSONObject("result").getJSONObject(
    String lat = location.getString("lat"); //纬度
    String lng = location.getString("lng"); //经度
    //返回经纬度坐标
    return lat + "," + lng;
}

完善下单功能:

public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
    //1、下单数据校验(收货地址为空、超出配送范围、购物车为空)
    Long userId = BaseContext.getCurrentId();
    //1.1 收货地址为空
    AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
    if (addressBook == null) {
        throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
    }

    //====================================================新增代码
    //1.2 超出配送范围
    checkOutOfRange(addressBook.getProvinceName() +
            addressBook.getCityName() + addressBook.getDetail());

    //1.3 购物车为空(userId)
    //……省略其他代码
}

五.Apache ECharts

5.1 介绍

Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。
官网地址:https://echarts.apache.org/zh/index.html

常见效果展示:

1). 柱状图-bar

2). 饼图-pie

3). 折线图-line

总结:

​ 不管是哪种形式的图形,最本质的东西实际上是数据,它其实是对数据的一种可视化展示。

5.2 实现

Apache Echarts官方提供的快速入门:https://echarts.apache.org/handbook/zh/get-started/

实现步骤:

1). 引入echarts.js 文件(当天资料已提供)

2). 为 ECharts 准备一个设置宽高的 DOM

3). 初始化echarts实例

4). 指定图表的配置项和数据

5). 使用指定的配置项和数据显示图表

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>ECharts</title>
    <!-- 引入刚刚下载的 ECharts 文件 -->
    <script src="echarts.js"></script>
  </head>
  <body>
    <!-- 为 ECharts 准备一个定义了宽高的 DOM -->
    <div id="main" style="width: 600px;height:400px;"></div>
    <script type="text/javascript">
      // 基于准备好的dom,初始化echarts实例
      var myChart = echarts.init(document.getElementById('main'));

      // 指定图表的配置项和数据
      var option = {
        title: {
          text: 'ECharts 入门示例'
        },
        tooltip: {},
        legend: {
          data: ['销量']
        },
        xAxis: {
          data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
        },
        yAxis: {},
        series: [
          {
            name: '销量',
            type: 'bar',
            data: [5, 20, 36, 10, 10, 20]
          }
        ]
      };

      // 使用刚指定的配置项和数据显示图表。
      myChart.setOption(option);
    </script>
  </body>
</html>

-总结:使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。

5.3具体业务实现 - 营业额统计

该项目数据统计用到Apache ECharts的有

营业额统计功能模块,

用户统计功能模块,

订单统计功能模块,

销量排名Top10功能模块

下面列举营业额统计模块简单分析一下其业务逻辑

5.3.1产品原型

营业额统计是基于折现图来展现,并且按照天来展示的。实际上,就是某一个时间范围之内的每一天的营业额。同时,不管光标放在哪个点上,那么它就会把具体的数值展示出来。并且还需要注意日期并不是固定写死的,是由上边时间选择器来决定。比如选择是近7天、或者是近30日,或者是本周,就会把相应这个时间段之内的每一天日期通过横坐标展示。

原型图:

业务规则:

  • 营业额指订单状态为已完成的订单金额合计
  • 基于可视化报表的折线图展示营业额数据,X轴为日期,Y轴为营业额
  • 根据时间选择区间,展示每天的营业额数据
5.3.2 接口设计

注意:具体返回数据一般由前端来决定,前端展示图表,具体折现图对应数据是什么格式,是有固定的要求的。
所以说,后端需要去适应前端,它需要什么格式的数据,我们就给它返回什么格式的数据。

5.3.3 代码开发

根据接口定义设计对应的VO:

前端需要的返回数据有两个list集合,dateList日期和turnoverList营业额数据

在sky-pojo模块,创建TurnoverReportVO.java

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TurnoverReportVO implements Serializable {

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
    private String dateList;
    
    //营业额,以逗号分隔,例如:406.0,1520.0,75.0
    private String turnoverList;
}

管理端页面要求统计一段时间内的营业额数据,传给后端的就是一个开始日期和结束日期

在Controller层创建ReportController.java

@RestController
@RequestMapping("/admin/report")
@Slf4j
@Api(tags = "统计报表相关接口")
public class ReportController {

    @Autowired
    private ReportService reportService;

    /**
     * 营业额数据统计
     *
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/turnoverStatistics")
    @ApiOperation("营业额数据统计")
    public Result<TurnoverReportVO> turnoverStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd")
                    LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd")
                    LocalDate end) {
        return Result.success(reportService.getTurnover(begin, end));
    }

}

在Service层接口中声明方法,使用具体的实现类来实现

创建ReportServiceImpl实现类,实现getTurnover方法:

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 根据时间区间统计营业额
     * @param begin
     * @param end
     * @return
     */
    public TurnoverReportVO getTurnover(LocalDate begin, LocalDate end) {
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);

        while (!begin.equals(end)){
            begin = begin.plusDays(1);//日期计算,获得指定日期后1天的日期
            dateList.add(begin);
        }
        
       List<Double> turnoverList = new ArrayList<>();
        for (LocalDate date : dateList) {
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
            Map map = new HashMap();
        	map.put("status", Orders.COMPLETED);
        	map.put("begin",beginTime);
        	map.put("end", endTime);
            Double turnover = orderMapper.sumByMap(map); 
            turnover = turnover == null ? 0.0 : turnover;
            turnoverList.add(turnover);
        }

        //数据封装
        return TurnoverReportVO.builder()
                .dateList(StringUtils.join(dateList,","))
                .turnoverList(StringUtils.join(turnoverList,","))
                .build();
    }
}

在调用Mapper层查询数据时,需要给出具体的时间段,需要知道起始日期的最小时间以及结束日期的最大时间

在这里我们使用java.time包中的LocalTime类来获取,具体实现为:

LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);

在OrderMapper接口声明sumByMap方法:

    /**
     * 根据动态条件统计营业额
     * @param map
     */
    Double sumByMap(Map map);

在OrderMapper.xml文件中编写动态SQL:

<select id="sumByMap" resultType="java.lang.Double">
        select sum(amount) from orders
        <where>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="begin != null">
                and order_time &gt;= #{begin}
            </if>
            <if test="end != null">
                and order_time &lt;= #{end}
            </if>
        </where>
</select>

因为数据库中的订单数据都是手动添加的,所以数据量有点少,看上去没有很帅气。大家测试时可以多添加一些数据。

六.Apache POI

6.1介绍

Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。
一般情况下,POI 都是用于操作 Excel 文件。

Apache POI 的应用场景:

1)银行网银系统导出交易明细

2)各种业务系统导出Excel报表

3)批量导入业务数据

6.2入门案例

1)导入对应的maven坐标

Apache POI的maven坐标:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
</dependency>

2)将数据写入Excel文件

public class POITest {

    /**
     * 基于POI向Excel文件写入数据
     * @throws Exception
     */
    public static void write() throws Exception{
        //在内存中创建一个Excel文件对象
        XSSFWorkbook excel = new XSSFWorkbook();
        //创建Sheet页
        XSSFSheet sheet = excel.createSheet("itcast");

        //在Sheet页中创建行,0表示第1行
        XSSFRow row1 = sheet.createRow(0);
        //创建单元格并在单元格中设置值,单元格编号也是从0开始,1表示第2个单元格
        row1.createCell(1).setCellValue("姓名");
        row1.createCell(2).setCellValue("城市");

        XSSFRow row2 = sheet.createRow(1);
        row2.createCell(1).setCellValue("张三");
        row2.createCell(2).setCellValue("北京");

        XSSFRow row3 = sheet.createRow(2);
        row3.createCell(1).setCellValue("李四");
        row3.createCell(2).setCellValue("上海");

        FileOutputStream out = 
            new FileOutputStream(new File("D:\\itcast.xlsx"));
        //通过输出流将内存中的Excel文件写入到磁盘上
        excel.write(out);

        //关闭资源
        out.flush();
        out.close();
        excel.close();
    }
    public static void main(String[] args) throws Exception {
        write();
    }
}

3)读取Excel文件中的数据

/**
 * 基于POI读取Excel文件
 * @throws Exception
 */
public static void read() throws Exception{
    FileInputStream in = new FileInputStream(new File("D:\\itcast.xlsx"));
    //通过输入流读取指定的Excel文件
    XSSFWorkbook excel = new XSSFWorkbook(in);
    //获取Excel文件的第1个Sheet页
    XSSFSheet sheet = excel.getSheetAt(0);

    //获取Sheet页中的最后一行的行号
    int lastRowNum = sheet.getLastRowNum();

    for (int i = 0; i <= lastRowNum; i++) {
        //获取Sheet页中的行
        XSSFRow titleRow = sheet.getRow(i);
        //获取行的第2个单元格
        XSSFCell cell1 = titleRow.getCell(1);
        //获取单元格中的文本内容
        String cellValue1 = cell1.getStringCellValue();
        //获取行的第3个单元格
        XSSFCell cell2 = titleRow.getCell(2);
        //获取单元格中的文本内容
        String cellValue2 = cell2.getStringCellValue();

        System.out.println(cellValue1 + " " +cellValue2);
    }

    //关闭资源
    in.close();
    excel.close();
}

public static void main(String[] args) throws Exception {
    read();
}

6.3具体的业务代码

导出Excel报表

在ReportServiceImpl实现类中实现导出运营数据报表的方法:

1、将资料中的运营数据报表模板.xlsx拷贝到项目的resources/template目录中
2、停止项目,删除target目录

@Autowired
private WorkspaceService workspaceService;

/**
 * 导出近30天的运营数据报表
 *
 * @param response
 **/
public void exportBusinessData(HttpServletResponse response) {
    LocalDate begin = LocalDate.now().minusDays(30);
    LocalDate end = LocalDate.now().minusDays(1);
    //查询概览运营数据,提供给Excel模板文件
    BusinessDataVO businessData = workspaceService.getBusinessData(
                                    LocalDateTime.of(begin, LocalTime.MIN),
                                    LocalDateTime.of(end, LocalTime.MAX));
    
    //需要从当前运行路径下获取excel模版文件
    InputStream inputStream = this.getClass().getClassLoader()
            .getResourceAsStream("template/运营数据报表模板.xlsx");
    try {
        //基于提供好的模板文件创建一个新的Excel表格对象
        XSSFWorkbook excel = new XSSFWorkbook(inputStream);
        //获得Excel文件中的一个Sheet页
        XSSFSheet sheet = excel.getSheet("Sheet1");

        //在第2行,第2列设置统计日期:2026-05-01至2026-05-30
        sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end);

        //获得第4行
        XSSFRow row = sheet.getRow(3);

        //在第3列设置:营业额
        row.getCell(2).setCellValue(businessData.getTurnover());
        //在第5列设置:订单完成率
        row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
        //在第7列设置:新增用户数
        row.getCell(6).setCellValue(businessData.getNewUsers());

        //获取第5行
        row = sheet.getRow(4);
        //在第3列设置:有效订单
        row.getCell(2).setCellValue(businessData.getValidOrderCount());
        //在第5列设置:平均客单价
        row.getCell(4).setCellValue(businessData.getUnitPrice());

        //写入明细数据(按天显示)
        for (int i = 0; i < 30; i++) {
            LocalDate date = begin.plusDays(i);
            //准备订单明细数据
            businessData = workspaceService.getBusinessData(
                            LocalDateTime.of(date, LocalTime.MIN),
                            LocalDateTime.of(date, LocalTime.MAX));
            //从第8行开始写入
            row = sheet.getRow(7 + i);
            row.getCell(1).setCellValue(date.toString());
            row.getCell(2).setCellValue(businessData.getTurnover());
            row.getCell(3).setCellValue(businessData.getValidOrderCount());
            row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
            row.getCell(5).setCellValue(businessData.getUnitPrice());
            row.getCell(6).setCellValue(businessData.getNewUsers());
        }

        //通过输出流将文件下载到客户端浏览器中
        ServletOutputStream out = response.getOutputStream();
        excel.write(out);
        //关闭资源
        out.flush();
        out.close();
        excel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

苍穹外卖这个项目所用到的技术以及其初步的使用方法大概就是这些了。

在项目编码过程中,遇到次数最多的问题大概就是导包错误了。总是导入错误的包,从而代码报错。还有好多次忘记添加注解。

在业务逻辑上,有很多解决方法时之前没有学过的,同时之前Java中的一些基础知识也有些忘记了,有空还是需要回顾之前所学的。

同时知识的深度也很重要,之前有些方法只是知道基本的,常见的用法,并没有很详细的去深挖其他用法,也没有很看过其源码等。知识的了解深度不够,这点以后学习需要注意。

通过跟学这次项目,虽然很多部分是老师直接提供的,自己真正完成的部分,现在回顾起来,也就那几部分。但是通过这次学习,仍旧学习到了很多东西。

令我最为震撼的就是作为一名程序员应有的一份严谨。有很多的常量都是事先编写好放在包内以供使用,同时防止在具体使用时程序员拼写错误,又方便修改,避免代码写死。同时看起来非常优雅!优雅!优雅!(重要的事情说三遍)

这次简述了项目用到的技术方法等,一些具体的业务逻辑可能没有说明白,这次先空着,有空再写。

:在写这篇文章时参考了黑马老师所提供的每日讲义

以及大佬的博客:原文链接:https://blog.csdn.net/DU1149507047/article/details/132511480