苍穹外卖(四)

查漏补缺

Stream流

Stream流的常见生成方式

- Stream流的三类方法

  - 获取Stream流
    - 创建一条流水线,并把数据放到流水线上准备进行操作
  - 中间方法
    - 流水线上的操作
    - 一次操作完毕之后,还可以继续进行其他操作
  - 终结方法
    - 一个Stream流只能有一个终结方法
    - 是流水线上的最后一个操作

- 生成Stream流的方式

  - Collection体系集合

    使用默认方法stream()生成流, default Stream<E> stream()

  - Map体系集合

    把Map转成Set集合,间接的生成流

  - 数组

    通过Arrays中的静态方法stream生成流

  - 同种数据类型的多个数据

    通过Stream接口的静态方法of(T... values)生成流

public class StreamDemo {
    public static void main(String[] args) {
        //Collection体系的集合可以使用默认方法stream()生成流
        List<String> list = new ArrayList<String>();
        Stream<String> listStream = list.stream();

        Set<String> set = new HashSet<String>();
        Stream<String> setStream = set.stream();

        //Map体系的集合间接的生成流
        Map<String,Integer> map = new HashMap<String, Integer>();
        Stream<String> keyStream = map.keySet().stream();
        Stream<Integer> valueStream = map.values().stream();
        Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();

        //数组可以通过Arrays中的静态方法stream生成流
        String[] strArray = {"hello","world","java"};
        Stream<String> strArrayStream = Arrays.stream(strArray);
      
      	//同种数据类型的多个数据可以通过Stream接口的静态方法of(T... values)生成流
        Stream<String> strArrayStream2 = Stream.of("hello", "world", "java");
        Stream<Integer> intStream = Stream.of(10, 20, 30);
    }
}
Stream流中间操作方法

- 概念

  中间操作的意思是,执行完此方法之后,Stream流依然可以继续执行其他操作

- 常见方法

方法名说明
Stream<T> filter(Predicate predicate)用于对流中的数据进行过滤
Stream<T> limit(long maxSize)返回此流中的元素组成的流,截取前指定参数个数的数据
Stream<T> skip(long n)跳过指定参数个数的数据,返回由该流的剩余元素组成的流
static <T> Stream<T> concat(Stream a, Stream b)合并a和b两个流为一个流
Stream<T> distinct()返回由该流的不同元素(根据Object.equals(Object) )组成的流

filter代码演示

List<String> list=new ArrayList<>(List.of("张三丰","张无忌","张翠山","王二麻子","张良","谢广坤"));

        list.stream().filter(s -> s.startsWith("张")).forEach(s -> System.out.println(s));

limit&skip代码演示

List<String> list=new ArrayList<>(List.of("张三丰","张无忌","张翠山","王二麻子","张良","谢广坤"));

        //list.stream().filter(s -> s.startsWith("张")).forEach(s -> System.out.println(s));

        list.stream().skip(2).limit(2).forEach(s -> System.out.println(s));

concat&distinct代码演示

public class StreamDemo03 {
    public static void main(String[] args) {
        //创建一个集合,存储多个字符串元素
        ArrayList<String> list = new ArrayList<String>();

        list.add("林青霞");
        list.add("张曼玉");
        list.add("王祖贤");
        list.add("柳岩");
        list.add("张敏");
        list.add("张无忌");

        //需求1:取前4个数据组成一个流
        Stream<String> s1 = list.stream().limit(4);

        //需求2:跳过2个数据组成一个流
        Stream<String> s2 = list.stream().skip(2);

        //需求3:合并需求1和需求2得到的流,并把结果在控制台输出
//        Stream.concat(s1,s2).forEach(s-> System.out.println(s));

        //需求4:合并需求1和需求2得到的流,并把结果在控制台输出,要求字符串元素不能重复
        Stream.concat(s1,s2).distinct().forEach(s-> System.out.println(s));
    }
}
Stream流终结操作方法
  • 概念

    终结操作的意思是,执行完此方法之后,Stream流将不能再执行其他操作

常见方法

方法名说明
void forEach(Consumer action)对此流的每个元素执行操作
long count()返回此流中的元素数
public class MyStream5 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("张三丰");
        list.add("张无忌");
        list.add("张翠山");
        list.add("王二麻子");
        list.add("张良");
        list.add("谢广坤");

        //method1(list);
        
//        long count():返回此流中的元素数
        long count = list.stream().count();
        System.out.println(count);
    }

    private static void method1(ArrayList<String> list) {
        //  void forEach(Consumer action):对此流的每个元素执行操作
        //  Consumer接口中的方法void accept(T t):对给定的参数执行此操作
        //在forEach方法的底层,会循环获取到流中的每一个数据.
        //并循环调用accept方法,并把每一个数据传递给accept方法
        //s就依次表示了流中的每一个数据.
        //所以,我们只要在accept方法中,写上处理的业务逻辑就可以了.
        list.stream().forEach(
                new Consumer<String>() {
                    @Override
                    public void accept(String s) {
                        System.out.println(s);
                    }
                }
        );
      
        System.out.println("====================");
        //lambda表达式的简化格式
        //是因为Consumer接口中,只有一个accept方法
        list.stream().forEach(
                (String s)->{
                    System.out.println(s);
                }
        );
        System.out.println("====================");
        //lambda表达式还是可以进一步简化的.
        list.stream().forEach(s->System.out.println(s));
    }
}

map

package com.sky.test;/*
 *
 *  @author pengjx
 *
 * */

import io.swagger.models.auth.In;
import org.junit.jupiter.api.Test;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class DemoTest {


    @Test
    public void streamTest(){
        List<String> list=new ArrayList<>(List.of("张三丰","张无忌","张翠山","王二麻子","张良","谢广坤","谢广坤","谢广坤"));

        //list.stream().filter(s -> s.startsWith("张")).forEach(s -> System.out.println(s));

        //list.stream().skip(2).limit(2).forEach(s -> System.out.println(s));
//        long count = list.stream().distinct().count();
//
//        System.out.println(count);

        List<Integer> list1=new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list1.add(i);
        }

        list1.add(10);
        list1.add(10);
        list1.add(10);
        list1.add(10);
        list1.add(10);

//        List<Integer> collect = list1.stream().filter(s -> s % 2 == 0).collect(Collectors.toList());
//        System.out.println(collect);
//        Set<Integer> collect = list1.stream().filter(s -> s % 2 == 0).collect(Collectors.toSet());
//        System.out.println(collect);
        ArrayList<String> list2 = new ArrayList<>();
        list2.add("zhangsan,23");
        list2.add("lisi,24");
        list2.add("wangwu,25");

//        Map<String, String> collect = list2.stream().collect(Collectors.toMap(s -> s.split(",")[0], s -> s.split(",")[1]));
//        System.out.println(collect);

        list2.stream().map(new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                String[] split = s.split(",");
                Integer i = Integer.parseInt(split[1]);
                return i;
            }
        }).forEach(s-> System.out.println(s));


    }

}

Stream流的收集操作

- 概念

  对数据使用Stream流的方式操作完毕后,可以把流中的数据收集到集合中

- 常用方法

方法名说明
R collect(Collector collector)把结果收集到集合中

工具类Collectors提供了具体的收集方式

方法名说明
public static <T> Collector toList()把元素收集到List集合中
public static <T> Collector toSet()把元素收集到Set集合中
public static Collector toMap(Function keyMapper,Function valueMapper)把元素收集到Map集合中
// toList和toSet方法演示 
public class MyStream7 {
    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            list1.add(i);
        }

        list1.add(10);
        list1.add(10);
        list1.add(10);
        list1.add(10);
        list1.add(10);

        //filter负责过滤数据的.
        //collect负责收集数据.
                //获取流中剩余的数据,但是他不负责创建容器,也不负责把数据添加到容器中.
        //Collectors.toList() : 在底层会创建一个List集合.并把所有的数据添加到List集合中.
        List<Integer> list = list1.stream().filter(number -> number % 2 == 0)
                .collect(Collectors.toList());

        System.out.println(list);

    Set<Integer> set = list1.stream().filter(number -> number % 2 == 0)
            .collect(Collectors.toSet());
    System.out.println(set);
}
}
/**
Stream流的收集方法 toMap方法演示
创建一个ArrayList集合,并添加以下字符串。字符串中前面是姓名,后面是年龄
"zhangsan,23"
"lisi,24"
"wangwu,25"
保留年龄大于等于24岁的人,并将结果收集到Map集合中,姓名为键,年龄为值
*/
public class MyStream8 {
	public static void main(String[] args) {
      	ArrayList<String> list = new ArrayList<>();
        list.add("zhangsan,23");
        list.add("lisi,24");
        list.add("wangwu,25");

        Map<String, Integer> map = list.stream().filter(
                s -> {
                    String[] split = s.split(",");
                    int age = Integer.parseInt(split[1]);
                    return age >= 24;
                }

         //   collect方法只能获取到流中剩余的每一个数据.
         //在底层不能创建容器,也不能把数据添加到容器当中

         //Collectors.toMap 创建一个map集合并将数据添加到集合当中

          // s 依次表示流中的每一个数据

          //第一个lambda表达式就是如何获取到Map中的键
          //第二个lambda表达式就是如何获取Map中的值
        ).collect(Collectors.toMap(
                s -> s.split(",")[0],
                s -> Integer.parseInt(s.split(",")[1]) ));

        System.out.println(map);
	}
}

方法引用

体验方法引用
  • 方法引用的出现原因

    在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿参数做操作

    那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑呢?答案肯定是没有必要

    那我们又是如何使用已经存在的方案的呢?

    这就是我们要讲解的方法引用,我们是通过方法引用来使用已经存在的方案

方法引用符
  • 方法引用符

    :: 该符号为引用运算符,而它所在的表达式被称为方法引用

 

引用类方法

引用类方法,其实就是引用类的静态方法

  • 格式

    类名::静态方法

  • 范例

    Integer::parseInt

    Integer类的方法:public static int parseInt(String s) 将此String转换为int类型数据

  • 练习描述

    • 定义一个接口(Converter),里面定义一个抽象方法 int convert(String s);

    • 定义一个测试类(ConverterDemo),在测试类中提供两个方法

      • 一个方法是:useConverter(Converter c)

      • 一个方法是主方法,在主方法中调用useConverter方法

public interface Converter {
    int convert(String s);
}

public class ConverterDemo {
    public static void main(String[] args) {

		//Lambda写法
        useConverter(s -> Integer.parseInt(s));

        //引用类方法
        useConverter(Integer::parseInt);

    }

    private static void useConverter(Converter c) {
        int number = c.convert("666");
        System.out.println(number);
    }
}
  • 使用说明

    Lambda表达式被类方法替代的时候,它的形式参数全部传递给静态方法作为参数

引用对象的实例方法

引用对象的实例方法,其实就引用类中的成员方法

  • 格式

    对象::成员方法

  • 范例

    "HelloWorld"::toUpperCase

    String类中的方法:public String toUpperCase() 将此String所有字符转换为大写

  • 练习描述

    • 定义一个类(PrintString),里面定义一个方法

      public void printUpper(String s):把字符串参数变成大写的数据,然后在控制台输出

    • 定义一个接口(Printer),里面定义一个抽象方法

      void printUpperCase(String s)

    • 定义一个测试类(PrinterDemo),在测试类中提供两个方法

      • 一个方法是:usePrinter(Printer p)

      • 一个方法是主方法,在主方法中调用usePrinter方法

public class PrintString {
    //把字符串参数变成大写的数据,然后在控制台输出
    public void printUpper(String s) {
        String result = s.toUpperCase();
        System.out.println(result);
    }
}

public interface Printer {
    void printUpperCase(String s);
}

public class PrinterDemo {
    public static void main(String[] args) {

		//Lambda简化写法
        usePrinter(s -> System.out.println(s.toUpperCase()));

        //引用对象的实例方法
        PrintString ps = new PrintString();
        usePrinter(ps::printUpper);

    }

    private static void usePrinter(Printer p) {
        p.printUpperCase("HelloWorld");
    }
}
  • 使用说明

    Lambda表达式被对象的实例方法替代的时候,它的形式参数全部传递给该方法作为参数

引用类的实例方法

引用类的实例方法,其实就是引用类中的成员方法

  • 格式

    类名::成员方法

  • 范例

    String::substring

    public String substring(int beginIndex,int endIndex)

    从beginIndex开始到endIndex结束,截取字符串。返回一个子串,子串的长度为endIndex-beginIndex

  • 练习描述

    • 定义一个接口(MyString),里面定义一个抽象方法:

      String mySubString(String s,int x,int y);

    • 定义一个测试类(MyStringDemo),在测试类中提供两个方法

      • 一个方法是:useMyString(MyString my)

      • 一个方法是主方法,在主方法中调用useMyString方法

public interface MyString {
    String mySubString(String s,int x,int y);
}

public class MyStringDemo {
    public static void main(String[] args) {
		//Lambda简化写法
        useMyString((s,x,y) -> s.substring(x,y));

        //引用类的实例方法
        useMyString(String::substring);

    }

    private static void useMyString(MyString my) {
        String s = my.mySubString("HelloWorld", 2, 5);
        System.out.println(s);
    }
}
  • 使用说明

    Lambda表达式被类的实例方法替代的时候 ​ 第一个参数作为调用者 ​ 后面的参数全部传递给该方法作为参数

 引用构造器

引用构造器,其实就是引用构造方法

  • l格式

    类名::new

  • 范例

    Student::new

  • 练习描述

    • 定义一个类(Student),里面有两个成员变量(name,age)

      并提供无参构造方法和带参构造方法,以及成员变量对应的get和set方法

    • 定义一个接口(StudentBuilder),里面定义一个抽象方法

      Student build(String name,int age);

    • 定义一个测试类(StudentDemo),在测试类中提供两个方法

      • 一个方法是:useStudentBuilder(StudentBuilder s)

      • 一个方法是主方法,在主方法中调用useStudentBuilder方法

 

public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public interface StudentBuilder {
    Student build(String name,int age);
}

public class StudentDemo {
    public static void main(String[] args) {

		//Lambda简化写法
        useStudentBuilder((name,age) -> new Student(name,age));

        //引用构造器
        useStudentBuilder(Student::new);

    }

    private static void useStudentBuilder(StudentBuilder sb) {
        Student s = sb.build("林青霞", 30);
        System.out.println(s.getName() + "," + s.getAge());
    }
}

 

  • 使用说明

    Lambda表达式被构造器替代的时候,它的形式参数全部传递给构造器作为参数

 @ConfigurationProperties

 用来注入外部配置的属性的,可以批量的将外部的属性配置注入到bean对象的属性中

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

 第三方bean

如果要管理的 bean 对象来自于第三方(不是自定义的),是无法用 @Component 及衍生注解声明 bean 的,就需要用到 @Bean 注解。
要管理的第三方 bean 对象,建议对这些 bean 进行集中分类配置,可以通过 @Configuration 注解声明一个配置类。
通过 @Bean 注解的 name value 属性可以声明 bean 的名称,如果不指定,默认 bean 的名称就是方法名。
如果第三方 bean 需要依赖其它 bean 对象,直接在 bean 定义方法中设置形参即可,容器会根据类型自动装配。

 

 

1. Spring Task

1.1 介绍

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

应用场景:

信用卡每月还款提醒
银行贷款每月还款提醒
火车票售票系统处理未支付订单
入职纪念日为用户发送通知

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

1.2 cron表达式

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

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

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

1.3 入门案例

Spring Task使用步骤:

导入 maven 坐标 spring-context (已存在)
启动类添加注解 @EnableScheduling 开启任务调度
自定义定时任务类
package com.sky.task;/*
 *
 *  @author pengjx
 *
 * */

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@Slf4j
public class MyTask {


    @Scheduled(cron = "0/5 * * * * ?")
    public void task(){
        log.info("{}",new Date());
    }

}

订单状态定时处理

2.1 需求分析

用户下单后可能存在的情况:

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

对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:

通过定时任务 每分钟检查一次 是否存在支付超时订单(下单后超过 15 分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
通过定时任务 每天凌晨 1 点检查一次 是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”

2.2 代码开发

OrderMapper接口中扩展方法

@Select("select * from orders where status=#{status} and order_time < #{time}")
    List<Orders> getByStatusAndTimeLT(Integer status, LocalDateTime time);

完善定时任务类的processTimeoutOrder方法:

/**
     * @description:支付超时
     * @date: 2023/12/26 19:36
     **/
    @Scheduled(cron = "0 * * * * ?")
    public void processTimeOutOrder(){
        log.info("开始进行支付超时订单查询:{}", LocalDateTime.now());

        LocalDateTime time = LocalDateTime.now();
        time = time.plusMinutes(-15);

        List<Orders> ordersList=orderMapper.getByStatusAndTimeLT(Orders.PENDING_PAYMENT,time);

        if(ordersList !=null && ordersList.size()>0){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.CANCELLED);
                orders.setCancelTime(LocalDateTime.now());
                orders.setCancelReason("支付超时");
                orderMapper.update(orders);
            }

        }

    }

完善定时任务类的processDeliveryOrder方法:

@Scheduled(cron = "0 0 1 * * ?")
    public void processDeliveryOrder(){
        log.info("开始进行未完成订单状态处理:{}",LocalDateTime.now());
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);

        List<Orders> ordersList=orderMapper.getByStatusAndTimeLT(Orders.DELIVERY_IN_PROGRESS,time);

        if(ordersList !=null && ordersList.size()>0){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }

WebSocket

3.1 介绍

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

HTTP协议和WebSocket协议对比:

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

应用场景:

视频弹幕
网页聊天
体育实况更新
股票基金报价实时更新

3.2 入门案例

实现步骤:

直接使用 websocket.html 页面作为 WebSocket 客户端
导入 WebSocket maven 坐标
导入 WebSocket 服务端组件 WebSocketServer ,用于和客户端通信
导入配置类 WebSocketConfiguration ,注册 WebSocket 的服务端组件
导入定时任务类 WebSocketTask ,定时向客户端推送数据

既然WebSocket支持双向通信,功能看似比HTTP强大,那么我们是不是可以基于WebSocket开发所有的业务功能?

WebSocket缺点:

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

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

4 来单提醒

4.1 需求分析和设计

用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:

语音播报
弹出提示框

设计:

通过 WebSocket 实现管理端页面和服务端保持长连接状态
当客户支付后,调用 WebSocket 的相关 API 实现服务端向客户端推送消息
客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
约定服务端发送给客户端浏览器的数据格式为 JSON ,字段包括: type orderId content

- type 为消息类型,1为来单提醒 2为客户催单

- orderId 为订单id

- content 为消息内容

4.2 代码开发

OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:

Map map=new HashMap();
        map.put("type",1);//1表示来单提醒,2表示用户存单
        map.put("orderId",ordersDB.getId());
        map.put("content","订单号:"+outTradeNo);

        String jsonString = JSON.toJSONString(map);
        webSocketServer.sendToAllClient(jsonString);

5 客户催单

5.1 需求分析和设计

5.2 代码开发

根据用户催单的接口定义,在user/OrderController中创建催单方法:

/**
     * @description:客户存单
     * @date: 2023/12/27 20:10
     * @param: id
     * @return: com.sky.result.Result
     **/
    @GetMapping("/reminder/{id}")
    @ApiOperation("客户存单")
    public Result reminder(@PathVariable("id") Long id){
        log.info("客户催单:{}",id);
        orderService.reminder(id);
        return Result.success();

    }

OrderService接口中声明reminder方法:

/**
     * @description:客户存单
     * @date: 2023/12/27 20:10
     * @param: id
     **/
    void reminder(Long id);

OrderServiceImpl中实现reminder方法:

/**
     * @description:客户存单
     * @date: 2023/12/27 20:10
     * @param: id
     **/
    @Override
    public void reminder(Long id) {
        Orders orders = orderMapper.getById(id);

        if(orders==null){
            throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
        }

        Map map = new HashMap<>();
        map.put("type",2);
        map.put("orderId",orders.getId());
        map.put("content","订单号:"+orders.getNumber());

        webSocketServer.sendToAllClient(JSON.toJSONString(map));
    }

6. 营业额统计

6.1 需求分析和设计

业务规则:

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

接口设计:

6.2 代码开发

根据接口定义创建ReportController

/**
     * @description:营业额统计
     * @date: 2023/12/28 8:24
     * @param: begin
     * @param: end
     * @return: com.sky.result.Result<com.sky.vo.TurnoverReportVO>
     **/
    @GetMapping("turnoverStatistics")
    @ApiOperation("营业额统计")
    public Result<TurnoverReportVO> turnoverStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end
            ){

        log.info("营业额统计:{},{}",begin,end);
        TurnoverReportVO turnoverReportVO=reportService.getTurnoverStatistics(begin,end);
        return Result.success(turnoverReportVO);
    }

创建ReportService接口,声明getTurnover方法:

/**
     * @description:营业额统计
     * @date: 2023/12/28 8:24
     * @param: begin
     * @param: end
     * @return: com.sky.vo.TurnoverReportVO
     **/
    TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end);

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

/**
     * @description:营业额统计
     * @date: 2023/12/28 8:24
     * @param: begin
     * @param: end
     * @return: com.sky.vo.TurnoverReportVO
     **/
    @Override
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
        List<LocalDate> localDateList=new ArrayList<>();
        localDateList.add(begin);
        while (!begin.equals(end)){
            begin = begin.plusDays(1);
            localDateList.add(begin);
        }

        List<Double> turnoverList=new ArrayList<>();
        for (LocalDate date : localDateList) {
            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);

        }

        TurnoverReportVO turnoverReportVO = TurnoverReportVO.builder()
                .dateList(StringUtils.join(localDateList, ","))
                .turnoverList(StringUtils.join(turnoverList, ","))
                .build();
        return turnoverReportVO;
    }

OrderMapper接口声明sumByMap方法:

Double sumByMap(Map map);

OrderMapper.xml文件中编写动态SQL

<select id="sumByMap" resultType="java.lang.Double" parameterType="java.util.Map">
        select sum(amount) from orders
        <where>
            <if test="status!=null"> 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>

7 用户统计

7.1 需求分析和设计

业务规则:

基于可视化报表的折线图展示用户数据, X 轴为日期, Y 轴为用户数
根据时间选择区间,展示每天的用户总量和新增用户量数据

接口设计:

7.2 代码开发

根据接口定义,在ReportController中创建userStatistics方法:

/**
     * @description:用户统计
     * @date: 2023/12/28 9:50
     * @param: begin
     * @param: end
     * @return: com.sky.result.Result<com.sky.vo.UserReportVO>
     **/
    @GetMapping("/userStatistics")
    @ApiOperation("用户统计")
    public Result<UserReportVO> userStatistic(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end
    ){
        log.info("用户统计:{},{}",begin,end);
        UserReportVO userReportVO=reportService.getUserStatistic(begin,end);
        return Result.success(userReportVO);

    }

ReportService接口中声明getUserStatistics方法:

/**
     * @description:用户统计
     * @date: 2023/12/28 9:51
     * @param: begin
     * @param: end
     * @return: com.sky.vo.UserReportVO
     **/
    UserReportVO getUserStatistic(LocalDate begin, LocalDate end);

ReportServiceImpl实现类中实现getUserStatistics方法

/**
     * @description:用户统计
     * @date: 2023/12/28 9:51
     * @param: begin
     * @param: end
     * @return: com.sky.vo.UserReportVO
     **/
    @Override
    public UserReportVO getUserStatistic(LocalDate begin, LocalDate end) {
        List<LocalDate> localDateList=new ArrayList<>();
        localDateList.add(begin);
        while (!begin.equals(end)){
            begin=begin.plusDays(1);
            localDateList.add(begin);
        }

        List<Integer> totalUserList=new ArrayList<>();
        List<Integer> newUserList=new ArrayList<>();

        for (LocalDate date : localDateList) {
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);

            Map map=new HashMap<>();
            map.put("end",endTime);
            Integer totalUser=userMapper.countByMap(map);
            totalUser=totalUser==null?0:totalUser;
            totalUserList.add(totalUser);

            map.put("begin",beginTime);
            Integer newUser=userMapper.countByMap(map);
            newUser=newUser==null?0:newUser;
            newUserList.add(newUser);
        }

        UserReportVO userReportVO = UserReportVO.builder()
                .dateList(StringUtils.join(localDateList, ","))
                .newUserList(StringUtils.join(newUserList, ","))
                .totalUserList(StringUtils.join(totalUserList, ","))
                .build();

        return userReportVO;
    }

ReportServiceImpl实现类中创建私有方法getUserCount

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);
    }

UserMapper接口中声明countByMap方法:

Integer countByMap(Map map);

UserMapper.xml文件中编写动态SQL

<select id="countByMap" resultType="java.lang.Integer" parameterType="java.util.Map">
        select count(id) from user
        <where>
            <if test="begin != null"> create_time &gt;= #{begin}</if>
            <if test="end != null">and create_time &lt;= #{end}</if>
        </where>

    </select>

订单统计

8.1 需求分析和设计

业务规则:

有效订单指状态为 已完成 的订单
基于可视化报表的折线图展示订单数据, X 轴为日期, Y 轴为订单数量
根据时间选择区间,展示每天的订单总数和有效订单数
展示所选时间区间内的有效订单数、总订单数、订单完成率,订单完成率 = 有效订单数 / 总订单数 * 100%

接口设计:

8.2 代码开发

ReportController中根据订单统计接口创建orderStatistics方法:

/**
     * @description:订单统计
     * @date: 2023/12/28 14:26
     * @param: begin
     * @param: end
     * @return: com.sky.result.Result<com.sky.vo.OrderReportVO>
     **/
    @GetMapping("/ordersStatistics")
    @ApiOperation("订单统计")
    public Result<OrderReportVO> ordersStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end
    ){
        log.info("订单统计:{},{}",begin,end);
        OrderReportVO orderReportVO=reportService.getOrdersStatistics(begin,end);
        return Result.success(orderReportVO);

    }

ReportService接口中声明getOrderStatistics方法: 

/**
     * @description:订单统计
     * @date: 2023/12/28 14:27
     * @param: begin
     * @param: end
     * @return: com.sky.vo.OrderReportVO
     **/
    OrderReportVO getOrdersStatistics(LocalDate begin, LocalDate end);

ReportServiceImpl实现类中实现getOrderStatistics方法

/**
     * @description:订单统计
     * @date: 2023/12/28 14:27
     * @param: begin
     * @param: end
     * @return: com.sky.vo.OrderReportVO
     **/
    @Override
    public OrderReportVO getOrdersStatistics(LocalDate begin, LocalDate end) {
        List<LocalDate> localDateList=new ArrayList<>();
        localDateList.add(begin);
        while (!begin.equals(end)){
            begin=begin.plusDays(1);
            localDateList.add(begin);
        }

        List<Integer> orderCountList=new ArrayList<>();
        List<Integer> validOrderCountList=new ArrayList<>();
        for (LocalDate date : localDateList) {
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);

            Integer orderCount = getOrderCount(beginTime, endTime, null);
            orderCount=orderCount==null?0:orderCount;
            orderCountList.add(orderCount);

            Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);
            validOrderCount=validOrderCount==null?0:validOrderCount;
            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(localDateList,","))
                .orderCountList(StringUtils.join(orderCountList,","))
                .validOrderCountList(StringUtils.join(validOrderCountList,","))
                .totalOrderCount(totalOrderCount)
                .validOrderCount(validOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .build();
    }

OrderMapper接口中声明countByMap方法:

Integer countByMap(Map map);

OrderMapper.xml文件中编写动态SQL

<select id="countByMap" resultType="java.lang.Integer" parameterType="java.util.Map">
        select count(id) from orders
        <where>
            <if test="status!=null"> 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>

9 销量排名Top10

9.1 需求分析和设计

业务规则:

根据时间选择区间,展示销量前 10 的商品(包括菜品和套餐)
基于可视化报表的柱状图降序展示商品销量
此处的销量为商品销售的份数

接口设计:

9.3 代码开发

ReportController中根据销量排名接口创建top10方法:

/**
     * @description:销售排名Top10
     * @date: 2023/12/28 15:15
     * @param: begin
     * @param: end
     * @return: com.sky.result.Result<com.sky.vo.SalesTop10ReportVO>
     **/
    @GetMapping("/top10")
    @ApiOperation("销售排名Top10")
    public Result<SalesTop10ReportVO> salesTop10Statistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end
    ){
        log.info("销售排名Top10:{},{}",begin,end);
        SalesTop10ReportVO salesTop10ReportVO=reportService.getSalesTop10Statistics(begin,end);
        return Result.success(salesTop10ReportVO);

    }

ReportService接口中声明getSalesTop10方法:

/**
     * @description:销售排名Top10
     * @date: 2023/12/28 15:15
     * @param: begin
     * @param: end
     * @return: com.sky.vo.SalesTop10ReportVO
     **/
    SalesTop10ReportVO getSalesTop10Statistics(LocalDate begin, LocalDate end);

ReportServiceImpl实现类中实现getSalesTop10方法:

/**
     * @description:销售排名Top10
     * @date: 2023/12/28 15:15
     * @param: begin
     * @param: end
     * @return: com.sky.vo.SalesTop10ReportVO
     **/
    @Override
    public SalesTop10ReportVO getSalesTop10Statistics(LocalDate begin, LocalDate end) {
        LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);

        List<GoodsSalesDTO> goodsSalesDTOList = orderMapper.getSalesTop10(beginTime,endTime);

        List<String> name = goodsSalesDTOList.stream().map(s -> s.getName()).collect(Collectors.toList());
        String nameList = StringUtils.join(name, ",");

        List<Integer> number = goodsSalesDTOList.stream().map(s -> s.getNumber()).collect(Collectors.toList());
        String numberList = StringUtils.join(number, ",");

        return SalesTop10ReportVO.builder()
                .nameList(nameList)
                .numberList(numberList)
                .build();


    }

OrderMapper接口中声明getSalesTop10方法:

List<GoodsSalesDTO> getSalesTop10(LocalDateTime beginTime, LocalDateTime endTime);

OrderMapper.xml文件中编写动态SQL

<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
        select name ,count(od.number) number from orders o,order_detail od where o.id=od.order_id and o.status = 5
        <if test="beginTime != null"> and order_time &gt;= #{beginTime}</if>
        <if test="endTime != null"> and order_time &lt;= #{endTime}</if>
        group by name
        order by number desc
        limit 0,10



    </select>

 10 Apache POI

10.1 介绍

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

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

Apache POI 的应用场景:

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

10.2 入门案例

Apache POImaven坐标:

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

将数据写入Excel文件:

//通过POI创建Excel文件并写入文件内容
     public static void write() throws Exception {
         XSSFWorkbook excel = new XSSFWorkbook();
         XSSFSheet sheet = excel.createSheet("info");

         XSSFRow row = sheet.createRow(1);

         row.createCell(1).setCellValue("姓名");
         row.createCell(2).setCellValue("城市");

         XSSFRow row1 = sheet.createRow(2);
         row1.createCell(1).setCellValue("小明");
         row1.createCell(2).setCellValue("赣州");

         FileOutputStream fileOutputStream=new FileOutputStream(new File("D:\\pengjixuan\\Documents\\excel\\POITest.xlsx"));

         excel.write(fileOutputStream);

         fileOutputStream.close();
         excel.close();

     }

读取Excel文件中的数据:

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();

11 导出运营数据Excel报表

11.1 •需求分析和设计

业务规则:

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

接口设计:

注意:当前接口没有返回数据,因为报表导出功能本质上是文件下载,

服务端会通过输出流将Excel文件下载到客户端浏览器

11.2 代码开发

实现步骤:

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

根据接口定义,在ReportController中创建export方法:

 /**
     * @description:导出运营数据Excel报表
     * @date: 2023/12/28 20:18
     * @param: httpServletResponse
     **/
    @GetMapping("/export")
    @ApiOperation("导出运营数据Excel报表")
    public void export(HttpServletResponse httpServletResponse){
        log.info("导出运营数据Excel报表");
        reportService.exportBusinessData(httpServletResponse);

    }

ReportService接口中声明导出运营数据报表的方法:

/**
     * @description:导出运营数据Excel报表
     * @date: 2023/12/28 20:18
     * @param: httpServletResponse
     **/
    void exportBusinessData(HttpServletResponse httpServletResponse);

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

/**
     * @description:导出运营数据Excel报表
     * @date: 2023/12/28 20:18
     * @param: httpServletResponse
     **/
    @Override
    public void exportBusinessData(HttpServletResponse httpServletResponse) {

        LocalDate time = LocalDate.now();
        LocalDate beginTime = time.minusDays(30);
        LocalDateTime begin=LocalDateTime.of(beginTime,LocalTime.MIN);

        LocalDate endTime = time.minusDays(1);
        LocalDateTime end = LocalDateTime.of(endTime, LocalTime.MAX);

        BusinessDataVO businessData = workspaceService.getBusinessData(begin, end);

        InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");

        try {
            XSSFWorkbook excel = new XSSFWorkbook(in);
            XSSFSheet sheet = excel.getSheetAt(0);
            sheet.getRow(1).getCell(1).setCellValue(beginTime+" 至 "+endTime);

            XSSFRow row = sheet.getRow(3);

            row.getCell(2).setCellValue(businessData.getTurnover());
            row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
            row.getCell(6).setCellValue(businessData.getNewUsers());

            row=sheet.getRow(4);
            row.getCell(2).setCellValue(businessData.getValidOrderCount());
            row.getCell(4).setCellValue(businessData.getUnitPrice());

            for (int i = 0; i < 30; i++) {
                LocalDate date = beginTime.plusDays(i);
                LocalDateTime beginDate = LocalDateTime.of(date, LocalTime.MIN);
                LocalDateTime endDate = LocalDateTime.of(date, LocalTime.MAX);
                businessData = workspaceService.getBusinessData(beginDate, endDate);

                row = sheet.getRow(i + 7);
                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 outputStream = httpServletResponse.getOutputStream();
            excel.write(outputStream);

            outputStream.close();
            in.close();
            excel.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }

 苍穹外卖项目完结

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值