《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】

史上最完整的《苍穹外卖》项目实操笔记系列【下篇】,跟视频的每一P对应,全系列10万字,涵盖详细步骤与问题的解决方案。如果你操作到某一步卡壳,参考这篇,相信会带给你极大启发。

上篇:P1~P65《苍穹外卖》项目实操笔记【上】

中篇:P66~P122《苍穹外卖》项目实操笔记【中】

一、订单状态定时处理、来单提醒和客户催单

Spring Task -> 订单状态定时处理 -> WebSocket ->来单提醒 -> 客户催单。

1.1 Task_介绍 P123

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

定位:定时任务框架。

作用:定时自动执行某段Java代码。

应用场景:信用卡每月还款提醒。银行贷款每月还款提醒。火车票售票系统处理未支付订单(自动取消超时支付的订单)。入职纪念日为用户发送通知。

1.2 Task_cron表达式 P124

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

构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义。

每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第1张图片

cron表达式可以上在线Cron表达式生成器生成。

1.3 Task_入门案例 P125

①导入maven坐标,spring-context(已存在)

②启动类添加注解@EnableScheduling开启任务调度

③自定义定时任务类

在sky-server下面的src/main/java/com/sky下面创建一个task包,在该包下创建MyTask类,写入如下代码:

@Component //实例化,自动生成bean交给容器管理
@Slf4j
public class MyTask {
    @Scheduled(cron="0/5 * * * * ?")
    public void executeTask(){
        log.info("定时任务开始执行:{}",new Date());
    }
}

0/5的意思是从0秒开始,每隔5秒触发一次。

直接启动启动类,然后控制台会每隔5秒输出一次:

1.4 (订单状态定时)设计分析 P126

用户下单后可能出现的问题:

1.下单后未支付,订单一直处于“待支付”状态。

应该通过定时任务每分钟检查一次是否存在支付超时的订单,如果存在则将订单状态修改为“已取消”。

2.用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态。

每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”。

1.5 (订单状态定时)代码开发 P127

生成工具:在线Cron表达式生成器-奇Q工具网 (qqe2.com)

可以用生成工具直接生成:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第2张图片

在sky-server的task包(P125创建的)下创建一个OrderTask类,写入如下代码:

@Component
@Slf4j
public class OrderTask {
    @Autowired
    private OrderMapper orderMapper;
    //处理超时订单的方法
    @Scheduled(cron="0 * * * * ? ")
    public void processTimeoutOrder(){
        log.info("定时处理超时订单:{}", LocalDateTime.now());
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
        //select * from orders status = ? and order_time < (当前时间 - 15分钟)
        List ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
        if(ordersList != null && ordersList.size()>0){
            for(Orders orders : ordersList){
                orders.setStatus(Orders.CANCELLED);
                orders.setCancelReason("订单超时,自动取消");
                orders.setCancelTime(LocalDateTime.now());
                orderMapper.update(orders);
            }
        }
    }

    @Scheduled(cron="0 0 1 * * ?")//每天凌晨1点触发一次
    public void processDeliveryOrder(){
        log.info("定时处理处于派送中的订单:{}",LocalDateTime.now());
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        List ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
        if(ordersList != null && ordersList.size()>0){
            for(Orders orders : ordersList){
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }
    }
}

然后在sky-server的mapper包下的OrderMapper中加入如下方法:

@Select("select * from orders where status=#{status} and order_time < #{orderTime}")
List getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);

1.6 (订单状态定时)功能测试 P128

首先要把原先task包下的MyTask注释掉,避免影响。然后复制下面的注解:

@Scheduled(cron="0/5 * * * * ?")

因为每隔1分钟,和每天凌晨1点这个时间设置不太容易观察。

所以在processTimeoutOrder(处理超时订单)上注释掉原先注解,加注解如下:

@Scheduled(cron="1/5 * * * * ?")

在processDeliveryOrder(处理派送中的订单)上注释掉原先注解,加注解如下:

@Scheduled(cron="0/5 * * * * ?")

控制台输出结果如下,可见没啥问题:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第3张图片

测试完要改回来。

1.7 WebSocket介绍 P129

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

HTTP是短连接,是单向的,基于请求响应模式;WebSocket是长连接(有点像打电话,双向消息),支持双向通信。HTTP和WebSocket底层都是TCP连接。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第4张图片

应用:视频弹幕,网页聊天(聊天窗口和客服聊天),体育实况更新,股票基金报价实时更新。

1.8 WebSocket入门案例 P130

资料在day10下面都有现成的:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第5张图片

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

②导入WebSocket的maven坐标(已导入)


    org.springframework.boot
    spring-boot-starter-websocket

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

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

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

1.9 WebSocket入门案例 P131

1.在sky-server的src/main/java/com/sky下创建websocket包,然后把资料里的WebSocketServer复制到下面。

通过sid来区分不同的客户端。加入@OnOpen注解,就变成了回调方法。加入@OnMessage注解,收到客户端的消息后会调这个方法。

2.然后在sky-server下的config下把WebSocketConfiguration拷入。

WebSocketServer需要通过配置类来注册。

3.然后在sky-server下的task下把WebSocketTask拷入。

4.最后把启动类运行,打开下图的html文件,自动会进行连接。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第6张图片《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第7张图片

可以建立连接,断开连接,发送消息,接收消息。

1.10 (来单提醒)分析设计 P132

用户下单并且支付成功后,需要第一时间通知外卖商家,通知的形式有如下2种:语音播报,弹出提示框。

通过WebSocket实现管理端页面和服务端保持长连接状态。

当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息。

客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报。

约定服务期发送给客户端浏览器的数据格式位JSON,字段包括:type(消息类型,1来单提醒,2客户催单),orderId,content。

1.11 (来单提醒)代码开发 P133

下面是具体的代码:

在sky-server的service下的OrderServiceImpl中先自动导入WebSocketServer:

@Autowired
private WebSocketServer webSocketServer;

在serviceOrderServiceImpl的payment方法中写入如下代码:

//通过websocket向客户端浏览器推送消息 type orderId content
Map map = new HashMap();
map.put("type",1);
map.put("orderId",this.orders.getId());
map.put("content","订单号:"+this.orders.getNumber());
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);

如下图(在用户下单后点击支付就立即提示接单,因为在前面设置支付的时候,默认都是直接支付成功,所以跳过了paySuccess方法): 

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第8张图片

1.12 (来单提醒)功能测试 P134

我最后测试是没问题的。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第9张图片

但一开始碰了2个坑,下面是我遇到的坑和解决方法:

1.没办法建立连接,看不到下面语句输出:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第10张图片

2.提示音一直响,不停

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第11张图片

对于第1个问题,要确保下面2点:

1.Redis的服务端要开启

2.nginx.conf配置的端口必须是:80(如果不是80,也可以更改前端页面中写的URL)。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第12张图片

对于第2个问题:

提示音一直响不停是因为设置了5秒钟重复发送的缘故,只需要把注解注释掉即可:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第13张图片

1.13 (客户催单)分析设计 P135

用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。通知的形式有如下两种:语音波高,弹出提示框。

条件:待接单状+用户已付款。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第14张图片

传入参数:订单id。

1.14 (客户催单)代码开发 P136

在controller的user下的OrderController中写入如下代码:

@GetMapping("/reminder/{id}")
@ApiOperation("客户催单")
public Result reminder(@PathVariable("id") Long id){
    orderService.reminder(id);
    return Result.success();
}

 在service下的OrderService中写入如下代码:

//客户催单
void reminder(Long id);

在service下的impl下的OrderServiceImpl中写入如下代码:

//客户催单
public void reminder(Long id){
    // 根据id查询订单
    Orders ordersDB = orderMapper.getById(id);
    // 校验订单是否存在
    if (ordersDB == null) {
        throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
    }
    Map map = new HashMap();
    map.put("type",2);
    map.put("orderId",id);
    map.put("content","订单号:"+ordersDB.getNumber());
    webSocketServer.sendToAllClient(JSON.toJSONString(map));
}

 前提:在orderMapper已有getById方法,在webSocketServer中已有sendToAllClient

1.15 (客户催单)功能测试 P137

这里如果测试不通过,可以看1.12 (来单提醒)功能测试 P134这节,一般来单提醒能调通的话,客户催单顺其自然。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第15张图片

二、数据统计-图形报表

Apache ECharts -> 营业额统计 -> 用户统计 ->订单统计 ->销量排名统计top10

2.1 Apache ECharts介绍 P138

柱形图,饼状图,折线图。

2.2 ECharts入门案例 P139

在给的资料里有现成的:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第16张图片

点击html会展示最终效果:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第17张图片

html的代码如下:

html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>EChartstitle>
    
    <script src="echarts.js">script>
  head>
  <body>
    
    <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: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']   //x轴
        },
        yAxis: {},
        series: [
          {
            name: '销量',
            type: 'bar',
            data: [5, 20, 36, 10, 10, 20]   //具体数据
          }
        ]
      };
      // 使用刚指定的配置项和数据显示图表。
      myChart.setOption(option);
    script>
  body>
html>

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

2.3 (营业额统计)分析设计 P140

业务规则:

1.营业额指的是订单状态为已完成的订单金额合计。

2.X轴为日期,Y轴为营业额。

3.根据时间选择区间,展示每天的营业额数据。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第18张图片

传入的是开始日期和结束日期。

后端返回的值要有日期列表和营业额列表。营业额和日期之间用逗号分隔。

2.4 (营业额统计)代码开发 P141

本节主要是搭建一个基本的代码框架:

在sky-server的controller层下的admin下新建ReportController类,写入如下代码:

@RestController
@RequestMapping("/admin/report")
@Api(tags="数据统计相关接口")
@Slf4j
public class ReportController {
    @Autowired
    private ReportService reportService;
    //营业额统计
    @GetMapping("/turnoverStatistics")
    @ApiOperation("营业额统计")
    public Result turnoverStatistics(
            @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate end){
        log.info("营业额数据统计:{},{}",begin,end);
        return Result.success(reportService.getTurnoverStatistics(begin,end));

    }
}

在sky-server的service层下新建ReportService接口,写入如下代码:

public interface ReportService {
    //统计指定时间区间内的营业额数据
    TurnoverReportVO getTurnoverStatistics(LocalDate begin,LocalDate end);
}

在sky-server的service层下的Impl下新建ReportServiceImpl类,写入如下代码:

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
    @Autowired
    private OrderMapper orderMapper;
    //统计指定时间区间内的营业额数据
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin,LocalDate end){
        return null;
    }
}

2.5 (营业额统计)代码开发 P142

完善sky-server的service层下的Impl下的ReportServiceImpl类,代码修改后如下:

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
    @Autowired
    private OrderMapper orderMapper;
    //统计指定时间区间内的营业额数据
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin,LocalDate end){
        //当前集合用于存放从begin到end范围内的每天的日期
        List dateList = new ArrayList<>();
        dateList.add(begin);
        while(!begin.equals(end)) {
            //日期计算,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        return TurnoverReportVO.builder().dateList(StringUtils.join(dateList,",")).build();
    }
}

2.6 (营业额统计)代码开发 P143

完善sky-server的service层下的Impl下的ReportServiceImpl类,代码修改后如下:

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
    @Autowired
    private OrderMapper orderMapper;
    //统计指定时间区间内的营业额数据
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin,LocalDate end){
        //当前集合用于存放从begin到end范围内的每天的日期
        List dateList = new ArrayList<>();
        dateList.add(begin);
        while(!begin.equals(end)) {
            //日期计算,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        //存放每天的营业额
        List turnoverList = new ArrayList<>();
        for(LocalDate date : dateList){
            //查询date日期对应的营业额数据,营业额是指:状态为“已完成”的订单金额合计。
            //LocalDate只有年月日
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); //LocalTime.MIN相当于获得0点0分
            LocalDateTime endTime = LocalDateTime.of(date,LocalTime.MAX);//无限接近于下一个日期的0点0分0秒
            //select sum(amount) from orders where order_time > ? and order_time < ? and status = 5
            //status==5代表订单已完成
            Map map = new HashMap();
            map.put("begin",beginTime);
            map.put("end",endTime);
            map.put("status", Orders.COMPLETED);
            Double turnover = orderMapper.sumByMap(map); //算出当天的营业额
            turnoverList.add(turnover);
        }
        //封装返回结果
        return TurnoverReportVO
                .builder()
                .dateList(StringUtils.join(dateList,","))
                .turnoverList(StringUtils.join(turnoverList,","))
                .build();
    }
}

2.7 (营业额统计)代码开发 P144

完善sky-server的service层下的Impl下的ReportServiceImpl类,代码最终版本如下:

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
    @Autowired
    private OrderMapper orderMapper;
    //统计指定时间区间内的营业额数据
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin,LocalDate end){
        //当前集合用于存放从begin到end范围内的每天的日期
        List dateList = new ArrayList<>();
        dateList.add(begin);
        while(!begin.equals(end)) {
            //日期计算,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        //存放每天的营业额
        List turnoverList = new ArrayList<>();
        for(LocalDate date : dateList){
            //查询date日期对应的营业额数据,营业额是指:状态为“已完成”的订单金额合计。
            //LocalDate只有年月日
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); //LocalTime.MIN相当于获得0点0分
            LocalDateTime endTime = LocalDateTime.of(date,LocalTime.MAX);//无限接近于下一个日期的0点0分0秒
            //select sum(amount) from orders where order_time > ? and order_time < ? and status = 5
            //status==5代表订单已完成
            Map map = new HashMap();
            map.put("begin",beginTime);
            map.put("end",endTime);
            map.put("status", Orders.COMPLETED);
            Double turnover = orderMapper.sumByMap(map); //算出当天的营业额
            //考虑当天营业额为0的情况,会返回空
            turnover = turnover == null ? 0.0:turnover;
            turnoverList.add(turnover);
        }
        //封装返回结果
        return TurnoverReportVO
                .builder()
                .dateList(StringUtils.join(dateList,","))
                .turnoverList(StringUtils.join(turnoverList,","))
                .build();
    }
}

完善sky-server的mapper层下的OrderMapper类,新增如下:

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

完善sky-server的resources的mapper下的ReportMapper.xml,新增如下:

2.8 (营业额统计)功能测试 P145

运行项目后前后端联调没问题:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第19张图片

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第20张图片

2.9 (用户统计)分析设计 P146

蓝线代表用户总量,绿线代表新增的用户量。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第21张图片

业务规则:x为日期,y轴为用户数。根据时间选择区间,展示每天用户总量和新增用户数。

2.10 (用户统计)代码开发 P147

 本节主要是搭建一个基本的代码框架:

在sky-server的controller层的admin下的ReportController类,写入如下代码:

//用户统计
@GetMapping("/userStatistics")
@ApiOperation("用户统计")
public Result userStatistics(
        @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate begin,
        @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate end){
    log.info("用户数据统计:{},{}",begin,end);
    return Result.success(reportService.getUserStatistics(begin,end));
}

在sky-server的service层的ReportService接口,写入如下代码:

//统计指定时间区间内的营业额数据
UserReportVO getUserStatistics(LocalDate begin, LocalDate end);

在sky-server的service层的Impl下的ReportServiceImpl类,写入如下代码:

//统计指定时间区间内的用户数据
public UserReportVO getUserStatistics(LocalDate begin,LocalDate end){
     return null;
}

2.11 (用户统计)代码开发 P148

完善sky-server的service层的Impl下的ReportServiceImpl类,写入如下代码:

//统计指定时间区间内的营业额数据
@Autowired
private UserMapper userMapper;

//统计指定时间区间内的用户数据
public UserReportVO getUserStatistics(LocalDate begin,LocalDate end){
    //存放从begin到end之间的每天对应的日期
    List dateList = new ArrayList<>();
    dateList.add(begin);
    while(!begin.equals(end)){
        begin = begin.plusDays(1);
        dateList.add(begin);
    }
    //存放每天的新增用户数量 select count(id) from user where create_time < ? and create_time> ?
    List newUserList = new ArrayList<>();
    //存放每天的总用户数量 select count(id) from user where create_time < ?
    List totalUserList = new ArrayList<>();
    return null;
}

完善sky-server的mapper层下的UserMapper类,写入如下代码:

//根据动态条件统计用户数量
Integer countByMap(Map map);

在sky-server的resources的mapper层下的UserMapper.xml,写入如下代码:

2.12 (用户统计)代码开发 P149

最终完善sky-server的service层的Impl下的ReportServiceImpl类,写入如下代码:

//统计指定时间区间内的用户数据
public UserReportVO getUserStatistics(LocalDate begin,LocalDate end){
        //存放从begin到end之间的每天对应的日期
        List dateList = new ArrayList<>();
        dateList.add(begin);
        while(!begin.equals(end)){
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        //存放每天的新增用户数量 select count(id) from user where create_time < ? and create_time> ?
        List newUserList = new ArrayList<>();
        //存放每天的总用户数量 select count(id) from user where create_time < ?
        List totalUserList = 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("end",endTime);//只加一个end(1个参数)自动匹配统计总计的SQL语句
            //总用户数量
            Integer totalUser = userMapper.countByMap(map);
            map.put("begin",beginTime);//再加一个参数匹配统计每日新增的SQL语句
            //新增用户数量
            Integer newUser = userMapper.countByMap(map);
            totalUserList.add(totalUser);
            newUserList.add(newUser);
        }
        return UserReportVO
                .builder()
                .dateList(StringUtils.join(dateList,","))
                .totalUserList(StringUtils.join(totalUserList,","))
                .newUserList(StringUtils.join(newUserList,","))
                .build();
    }

2.13 (用户统计)功能测试 P150

简单测试没问题,不过多赘述。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第22张图片

2.14 (订单统计) 分析设计 P151

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第23张图片

业务规则:

1.有效订单指状态为已完成的订单

2.x轴为日期,y轴为订单数量

3.在时间选择区间内,展示每天的订单总数和有效订单数。

4.展示区间内有效订单数、总订单数、订单完成率,订单完成率=有效订单数/总订单数x100%

2.15 (订单统计) 代码开发 P152 P153

因为比较简单,所以直接给出完整代码。

在sky-server的controller层的admin下的ReportController类,写入如下代码:

//订单统计
@GetMapping("/ordersStatistics")
@ApiOperation("订单统计")
public Result ordersStatistics(
        @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate begin,
        @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate end){
    log.info("订单数据统计:{},{}",begin,end);
    return Result.success(reportService.getOrderStatistics(begin,end));
}

在sky-server的service层的ReportService接口,写入如下代码:

//统计指定时间区间内的订单数据
OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end);

在sky-server的service层的Impl下的ReportServiceImpl类,写入如下代码:

//统计指定时间区间内的订单数据
public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
    List dateList = new ArrayList<>();
    dateList.add(begin);
    while(!begin.equals(end)){
        begin = begin.plusDays(1);
        dateList.add(begin);
    }
    //存放每天的订单总数
    List orderCountList = new ArrayList<>();
    //存放每天的有效订单数
    List validOrderCountList = new ArrayList<>();
    //便利dateList集合,查询每天的有效订单数和订单总数
    for(LocalDate date : dateList){
        //查询每天的订单总数 select count(id) from orders where order_time > ? and order_time < ?
        LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
        Integer orderCount = getOrderCount(beginTime, endTime, null);
        //查询每天的有效订单数select count(id) from orders where order_time > ? and order_time < ? and status = 5
        Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);
        orderCountList.add(orderCount);
        validOrderCountList.add(validOrderCount);
    }
    //计算时间区间内的订单总数量
    Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();
    //计算时间区间内的有效订单数量
    Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();

    //计算订单完成率
    Double orderCompletionRate = 0.0;
    if(totalOrderCount != 0){
        //计算订单完成率
        orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
    }

    return OrderReportVO.builder()
            .dateList(StringUtils.join(dateList,","))
            .orderCountList(StringUtils.join(orderCountList,","))
            .validOrderCountList(StringUtils.join(validOrderCountList,","))
            .totalOrderCount(totalOrderCount)
            .validOrderCount(validOrderCount)
            .orderCompletionRate(orderCompletionRate)
            .build();
}
//根据条件统计订单数量
private Integer getOrderCount(LocalDateTime begin,LocalDateTime end,Integer status){
    Map map = new HashMap();
    map.put("begin",begin);
    map.put("end",end);
    map.put("status",status);
    return orderMapper.countByMap(map);
}

在sky-server的mapper层下的orderMapper类,写入如下代码:

//根据动态条件统计订单数量
Integer countByMap(Map map);

在sky-server的resources的mapper层下的UserMapper.xml,写入如下代码:

2.16 (订单统计) 功能测试 P154

简单测试没问题,不过多赘述。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第24张图片

2.17 (销售排名统计) 分析设计 P155

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第25张图片

根据时间选择区间,展示销量前10的商品(包括菜品和套餐)。

基于柱状图展示商品销量。

此处的销量为商品销售的份数。

2.18 (排名统计) 代码开发 P156 P157

因为比较简单,所以直接给出完整代码。

在sky-server的controller层的admin下的ReportController类,写入如下代码:

//销量排名top10
@GetMapping("/top10")
@ApiOperation("销量排名top10")
public Result top10(
        @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate begin,
        @DateTimeFormat(pattern="yyyy-MM-dd") LocalDate end){
    log.info("销量排名top10:{},{}",begin,end);
    return Result.success(reportService.getSalesTop10(begin,end));
}

在sky-server的service层的ReportService接口,写入如下代码:

//统计指定时间区间内的销量排名前10
SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end);

在sky-server的service层的Impl下的ReportServiceImpl类,写入如下代码:

//统计指定时间区间内的销量排名前10
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
    LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
    LocalDateTime endTime = LocalDateTime.of(end,LocalTime.MAX);
    List salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
    List names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
    String nameList = StringUtils.join(names, ",");
    List numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
    String numberList = StringUtils.join(numbers, ",");
    return SalesTop10ReportVO.builder().nameList(nameList).numberList(numberList).build();
}

 《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第26张图片

select od.name,sum(od.number) number
from order_detail od,orders o 
where od.order_id = o.id and o.status = 5 and o.order_time > '2024-1-26' and o.order_time < '2024-1-28'
group by od.name
order by number desc
limit 0,10

在sky-server的mapper层下的orderMapper类,写入如下代码:

//统计指定时间区间内的销量排名前10
List getSalesTop10(LocalDateTime begin,LocalDateTime end);

在sky-server的resources的mapper层下的UserMapper.xml,写入如下代码:

2.19 (排名统计) 功能测试 P158

简单测试没问题,不过多赘述。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第27张图片

三、数据统计-Excel报表

3.1 本章内容介绍

实现工作台的功能+数据统计菜单的数据导出到Excel文件的功能。

3.2 (工作台) 分析设计 P160

工作台是系统运营的数据看板,并提供快捷操作入口,可以有效提高商家的工作效率。

工作台展示的数据:今日数据(当天营业数据),订单管理(不同状态订单个数),菜品总览(起售停售的菜品),套餐总览,订单信息(只显示待接单和待派送的)。

名词解释:营业额:已完成订单的总金额。有效订单:已完成订单的数量。订单完成率:有效订单数/总订单数x100%。平均客单价:营业额/有效订单数。

3.3 (工作台) 代码导入 P161

1.把WorkSpaceController导入controller/admin

2.把WorkspaceService导入service

3.把WorkspaceServiceImpl导入serviceImpl

4.把DishMapper中的countByMap单独导入mapper下的DishMapper

5.把DishMapper.xml中的countByMap单独导入resources/mapper下

6.把SetmealMapper中的countByMap单独导入mapper下的SetmealMapper

7.把SetmealMapper.xml中的countByMap单独导入resources/mapper下

3.4 (工作台) 功能测试 P162

简单测试没问题,不过多赘述。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第28张图片

3.5 (Apache POI) 介绍 P163

一般情况下,POI都是用于操作Excel文件。

应用场景:

银行网银系统导出交易明细;各种业务系统导出Excel报表;批量导入业务数据。

3.6 (Apache POI) 入门案例 P164

使用POI需要导入下面2个坐标:


    org.apache.poi
    poi


    org.apache.poi
    poi-ooxml

在sky-server\src\test\java\com\sky\test下面创建一个POITest类,写入如下代码:

public class POITest {
    /*
    * 通过POI创建Excel文件并且写入文件内容
    * */
    @Test
    public void writeTest() throws IOException {
        //在内存中创建一个Excel文件
        XSSFWorkbook excel = new XSSFWorkbook();
        //在Excel文件中创建一个Sheet页
        XSSFSheet sheet = excel.createSheet("info");
        //在Sheet中创建行对象,rownum编号从0开始
        XSSFRow row = sheet.createRow(1); //1代表第2行
        row.createCell(1).setCellValue("姓名");//创建单元格写入内容
        row.createCell(2).setCellValue("城市");
        //创建一个新行
        row = sheet.createRow(2);//第3行
        row.createCell(1).setCellValue("张三");//创建单元格写入内容
        row.createCell(2).setCellValue("厦门");

        row = sheet.createRow(3);//第4行
        row.createCell(1).setCellValue("李四");//创建单元格写入内容
        row.createCell(2).setCellValue("南京");
        //上面写的都是在内存,现在想在磁盘看到
        FileOutputStream out = new FileOutputStream(new File("C://software/info.xlsx"));//设置文件
        excel.write(out);//写入到文件
        //关闭资源
        out.close();
        excel.close();

    }
}

最终效果如下: 

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第29张图片

3.7 (Apache POI) 入门案例 P165

把文本读取出来。

在sky-server\src\test\java\com\sky\test下面的POITest类,写入如下代码:

@Test
public void readTest() throws IOException{
    FileInputStream in = new FileInputStream(new File("C://software/info.xlsx"));
    //读取磁盘上已经存在的Excel文件
    XSSFWorkbook excel = new XSSFWorkbook(in);
    //读取Excel文件中的第一个Sheet页
    XSSFSheet sheet = excel.getSheetAt(0);
    //获取Sheet中最后一行行号
    int lastRowNum = sheet.getLastRowNum();
    for(int i=1;i<=lastRowNum;i++){
        //获得某一行
        XSSFRow row = sheet.getRow(i);
        //获得单元格对象
        String cellValue1 = row.getCell(1).getStringCellValue();
        String cellValue2 = row.getCell(2).getStringCellValue();
        System.out.println(cellValue1+" "+cellValue2);
    }
    //关闭资源
    in.close();

}

3.8 (导出Excel表) 分析设计 P166

导出Excel形式的报表文件;导出最近30天的运营数据。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第30张图片

接口没有返回数据,导出报表本底是文件下载。服务端会通过输出流将Excel文件下载到客户端浏览器。

 一般是先创建原始的Excel文件,这个文件被称为模板文件,先设置好包括颜色和字体等。

步骤:①设计Excel模板文件②查询近30天的运营数据③将查询到的运营数据写入模板文件④通过输出流将Excel文件下载到客户端浏览器。

下面这个是模板文件:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第31张图片

先在resources下面创建一个template包,然后把运营数据报表模块.xlsx复制进去。

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第32张图片

3.9 (导出Excel表) 代码开发 P167 P168 P169

在sky-server的controller层的admin下的ReportController类,写入如下代码:

//导出运营数据报表
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
    reportService.exportBusinessData(response);
}

在sky-server的service层的ReportService接口,写入如下代码:

void exportBusinessData(HttpServletResponse response);

在sky-server的service层的Impl下的ReportServiceImpl类,写入如下代码:

@Autowired
private WorkspaceService workspaceService;
//统计指定时间区间内的销量排名前10
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
    LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
    LocalDateTime endTime = LocalDateTime.of(end,LocalTime.MAX);
    List salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
    List names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
    String nameList = StringUtils.join(names, ",");
    List numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
    String numberList = StringUtils.join(numbers, ",");
    return SalesTop10ReportVO.builder().nameList(nameList).numberList(numberList).build();
}
@Autowired
private WorkspaceService workspaceService;
//导出运营数据报表
public void exportBusinessData(HttpServletResponse response){
    //1.查询数据库,获取营业数据--查询最近30天的运营数据
    LocalDate dateBegin = LocalDate.now().minusDays(30); //减30天的时间
    LocalDate dateEnd = LocalDate.now().minusDays(1);
    BusinessDataVO businessDatavo = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));
    //2.通过POI将数据写入到Excel文件中
    InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");//在类路径下读取资源返回输入流对象

    try {
        //基于模板文件创建一个新的Excel文件
        XSSFWorkbook excel = new XSSFWorkbook(in);
        //获取表格文件的Sheet文件
        XSSFSheet sheet = excel.getSheet("Sheet1");
        //填充数据--时间
        sheet.getRow(1).getCell(1).setCellValue("时间:"+dateBegin+"至"+dateEnd);
        //获得第4行
        XSSFRow row = sheet.getRow(3);
        row.getCell(2).setCellValue(businessDatavo.getTurnover()); //第3个单元格
        row.getCell(4).setCellValue(businessDatavo.getOrderCompletionRate());
        row.getCell(6).setCellValue(businessDatavo.getNewUsers());
        //获得第5行
        row = sheet.getRow(4);
        row.getCell(2).setCellValue(businessDatavo.getValidOrderCount());
        row.getCell(4).setCellValue(businessDatavo.getUnitPrice());
        //填充明细数据
        for(int i=0;i<30;i++){
            LocalDate date = dateBegin.plusDays(i);
            //查询某一天的营业数据
            workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN),LocalDateTime.of(date,LocalTime.MAX));
            //获得某一行
            row = sheet.getRow(7+i);
            row.getCell(1).setCellValue(date.toString());
            row.getCell(2).setCellValue(businessDatavo.getTurnover());
            row.getCell(3).setCellValue(businessDatavo.getValidOrderCount());
            row.getCell(4).setCellValue(businessDatavo.getOrderCompletionRate());
            row.getCell(5).setCellValue(businessDatavo.getUnitPrice());
            row.getCell(6).setCellValue(businessDatavo.getNewUsers());
        }
        //3.通过输出流将Excel文件下载到客户端浏览器
        ServletOutputStream out = response.getOutputStream();
        excel.write(out);
        //关闭资源
        out.close();
        excel.close();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

}

3.12 (导出Excel表) 功能测试 P170

点击数据导出后会有一个xlsx文件被下载下来

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第33张图片

下面是数据的效果:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第34张图片

四、前端

4.1 课程介绍 P171

1.VUE基础知识回顾+VUE进阶(router、vuex、typescript)

2.苍穹外卖前端项目环境搭建+开发员工管理模块

3.开发套餐管理模块

4.2 脚手架创建前端 P172

1.环境配置

node.js : 前端项目的运行环境

Node.js安装与配置(详细步骤)_nodejs安装及环境配置-CSDN博客

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第35张图片

npm : JavaScript的包管理工具

(Node自带npm)安装完后输入如下命令检查没问题:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第36张图片

Vue CLI :基于Vue进行快速开发的完整系统,实现交互式的项目脚手架

npm i @vue/cli -g

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第37张图片

2.使用 Vue CLI 创建前端工程

我先在C盘下创建了code/vue_project文件。然后在这个目录下打开一个cmd窗口:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第38张图片

方法1:vue create 项目名称

输入下面代码: 

vue create vue-demo1

选择Vue 2,然后选择npm

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第39张图片

生成的脚手架工程大概是下面这样的:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第40张图片

方法2:vue ui (网页界面创建)

输入vue ui会弹出一个网页,进入code/vue_project点击“在此创建新项目”,

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第41张图片《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第42张图片

填写名称,选择好包管理器,选择Vue2 即可:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第43张图片《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第44张图片

项目结构和重点文件目录:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第45张图片

3.启动前端项目

使用vscode打开文件,点击右上角那个按钮:

输入(注意serve对应的是package.json里面的serve):

npm run serve

 《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第46张图片

出现下面表示成功(记得下载完vscode和nodejs后要重启电脑),点击链接后可进入网页:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第47张图片《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第48张图片

如果想退出可以按住ctrl +c

如果想更改端口号,可以在vue.config.js文件中输入如下代码(注意一定要在写完后ctrl+s保存!!!):

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer:{
    port:7070
  }
})

如下图没啥问题: 

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第49张图片

4.3 Vue使用方法 P173

 可以先在HelloWorld.vue中先把div下的内容删掉,然后再把App.vue下的图片删掉:

《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第50张图片《苍穹外卖》电商实战项目实操笔记系列(P123~P184)【下】_第51张图片

1.vue组件

Vue的组件文件以.vue结尾,每个组件由三部分组成。

结构