苍穹外卖学习笔记

一、软件开发介绍

1.软件开发流程

苍穹外卖学习笔记_第1张图片

2.角色分工

苍穹外卖学习笔记_第2张图片

3.软件环境

苍穹外卖学习笔记_第3张图片

二、苍穹外卖项目介绍

1.项目介绍

是专门为餐饮企业单独定制的一款外卖软件,包括系统管理后台和移动应用端两部分。

管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、订单、套餐进行维护管理。

移动端应用主要提供给消费者使用,可以在线浏览菜品,添加购物车,下单等

苍穹外卖学习笔记_第4张图片

苍穹外卖学习笔记_第5张图片

2.项目原型

产品原型适用于展示项目的业务功能,一般由产品经历设计【也就是预览项目功能的html页面】

3.技术选型

苍穹外卖学习笔记_第6张图片

三、开发环境搭建

1.前端环境搭建

前端代码已经开发好,直接运行起来就可以

代码说明:

  • 项目位置:前端运行环境—》nginx—》html—》sky(就是前端工程)
  • nginx的放置目录不能有中文
  • 直接启动nginx.exe即可
  • nginx的默认端口号是80,所以浏览器地址栏直接输入:localhost 就可以访问前端页面

2.后端环境搭建

后端工程已经搭建好,直接导入到idea中即可

苍穹外卖学习笔记_第7张图片

苍穹外卖学习笔记_第8张图片

3.数据库搭建

创建数据库sky_take_out,直接执行sql文件

image-20230724142947509

4.启动项目

  1. 修改配置文件中的数据库相关信息
  2. 点击maven,找到父项目,点击compile编译一下,看一下是否有错误
  3. 然后启动前端、后端项目
  4. 浏览器输入:loacalhot 测试项目能否正常启动

四、未知知识补充

1.nginx反向代理

  • 前端请求怎么发送给后端的?

    打开浏览器发现前端发送的请求地址是:http://localhost/api/employee/login

    而后端控制器中的处理地址是:http://localhost:8080/admin/employee/login

    可以看见请求地址不同,请求端口号也不一样

    这是由于nginx的反向代理,也就是将前端发送的动态请求由nginx转发到后端服务器

  • 这样做的好处是什么?

    1. 提高访问速度:nginx可以缓存一部分数据,如果请求的东西nginx有,那么就可以直接让他访问
    2. 进行负载均衡:当业务量很大的情况下,可以部署多台服务器,由nginx均衡的将请求分配给各个服务器,减小压力
    3. 保证后端服务安全:没有直接暴露后端的地址
  • 怎么配置nginx的反向代理呢?

    苍穹外卖学习笔记_第9张图片

    在配置文件中 localtion字段后面的内容就是需要处理的

    例如:上面的数据是localtion /api/那么如果前端发过来的请求地址中包含/api/,就会将请求地址中api及前面的部分换成配置文件中反向代理的部分,api后面的部分直接拼接到反向代理地址后面

  • 怎么配置负载均衡呢?

    苍穹外卖学习笔记_第10张图片

    大部分和反向代理一样,不过请求地址换成了webservers,而webservers是一个关键字,这个关键字中包含多个服务器地址,所以到请求的时候地址就变成了对应的地址

五、导入前端接口文档

1.前后端分离开发流程

苍穹外卖学习笔记_第11张图片

2.导入文档

打开YApi网站https://yapi.pro/,因为我们有两个json文件【前端、后端】,所以创建两个工程

然后将json文件导入

六、swagger

1.介绍

可以做到生成后端接口文档,以及在线接口调试

官网:https://swagger.io/

Knife4j是java MVC框架集成Swagger生成Api文档的增强解决方案,只需要在pom中导入依赖

2.配置方式【不需要背,拷贝过去改一下就可以】

  1. 导入 knife4j 的maven坐标

    <dependency>
                <groupId>com.github.xiaoymingroupId>
                <artifactId>knife4j-spring-boot-starterartifactId>
                <version>3.0.2version>
    dependency>
    
  2. 在配置类中加入 knife4j 相关配置

    /**
     * 通过knife4j生成接口文档
     * @return
     */
    @Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")//生成文档名
                .version("2.0")//版本
                .description("苍穹外卖项目接口文档")//描述
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))//需要扫描的包
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
    
  3. 设置静态资源映射,否则接口文档页面无法访问【固定的】

    /**
     * 设置静态资源映射
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
    

3.使用方式

  1. 访问后端项目ip:端口号/doc.html

    苍穹外卖学习笔记_第12张图片

  2. 这两个方法就是我们后端写的controller方法

    苍穹外卖学习笔记_第13张图片

  3. 点击需要调试方法,输入参数,点击发送就可以调试代码

4.常用注解【加上接口文档更清晰】

苍穹外卖学习笔记_第14张图片

  • @Api:用在类上,说明类的作用

    @Api(tags = "员工相关接口")
    
  • @ApiOperation:用在方法上,说明方法的作用

    @ApiOperation(value = "员工登录")
    

七、员工相关操作

1.添加员工

1.1 需求分析

说明:需求分析要根据产品原型分析,后面就不说了

苍穹外卖学习笔记_第15张图片

苍穹外卖学习笔记_第16张图片

苍穹外卖学习笔记_第17张图片

后台管理系统点击添加按钮之后就会自动跳转到添加员工页面

表单输入完成之后点击保存就会把数据发送到后端

打开开发者工具,点击保存查看发送的请求信息

请求的地址是:http://localhost:8080/employee

发送的数据是json类型的

{name: "杨", phone: "15831000007", sex: "1", idNumber: "130093131231231237", username: "123142341"}

后端Controller接收提交的数据,调用Service将数据进行保存

Service调用Mapper操作数据库把信息添加到employee表(username字段是unique类型的,status字段有默认值1)

然后发送给前端

1.2 代码开发

  1. 前端传过来的数据跟我们已有的pojo的参数有太大差距,所以我们专门设计DTO用来接收前端传过来的数据【这里资料已经给了,所以就不再自己创建了】

  2. 编写添加员工的Controller方法

    • 前端使用post方式,所以加@PostMapping注解

    • 前端传过来的数据类型是json,所以使用@RequestBody注解

  3. 编写Service接口和Impl类

    1. 因为传过来的是DTO,所以可以使用BeanUtils的copyProperties方法,将已有的属性复制到Bean中
    2. 其他的属性使用set方法手动设置【设置status、password、更新时间、修改时间、创建者】
    3. 然后调用Mapper的方法
  4. 编写Mapper接口【因为这是一条简单sql,所以直接使用注解,不用再映射文件中写了】

1.3 功能测试

直接使用Swagger接口文档测试就可以

  1. 因为有JWT,所以没有登录不能添加数据
  2. 先在接口文档中调用登录方法,获得一个令牌
  3. 然后点击文档管理的添加全局参数设置【参数名是代码中设置的,把令牌输入】
  4. 但是发现如果用户名已经有了,再添加会抛异常

1.4 代码完善

问题:

  1. 如果录入用户名已经存在,我们没有处理
  2. 创建人id没有处理
1.4.1 全局异常处理器

用来解决没有处理的异常信息【项目中这个类已经创建,直接写方法就可以】

  1. common包里创建一个全集异常处理器

  2. 加上@ControllerAdvice注解,使用annotations属性指定拦截注解

    @ControllerAdvice(annotations={RestController.class,Controller.class})//表示会拦截这两个注解
    
  3. 加上@ResponseBody注解【因为添加用户失败会返回json数据】

  4. 编写一个方法,返回R对象

  5. 方法上加上@ExceptionHandler注解,里面是异常的class对象

    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)//表示处理的是这个异常【控制台有】
    
  6. 方法的参数是异常类

  7. 异常类的getMessage方法可以显示异常信息,所以使用下面的操作

    //判断异常信息【控制台:后面的部分】是否包含Duplicate entry这个
    //如果包含说明添加的数据已经存在
    //错误信息的第三部分是说那个key已经存在
    //所以将错误信息拼接起来,返回了
    if(ex.getMessage().contains("Duplicate entry")){
                String[] split = ex.getMessage().split(" ");//以空格分隔
                String msg = split[2] + "已存在";
                return R.error(msg);
    }
    
  8. 如果是其他错误直接返回R.error(“未知错误”)

1.4.2 从JWT获取id信息
1.4.2.1 JWT运行原理

苍穹外卖学习笔记_第18张图片

  1. 用户登录后,后端会给前端返回一个token
  2. 前端保存jwt token
  3. 后面每次请求都会携带token,后端就会拦截检查token
  4. 如果通过就会返回后续结果,如果错误就会跳转错误页面
1.4.2.1 ThreadLocal

ThreadLocal不是一个Thread,而是Thread的局部变量

ThreadLocal为每个线程单独提供一个存储空间,具有线程隔离效果,只有在线程内才能获取到对应的值,线程外则不能访问。

客户端每一次访问,tomacat都会开辟一个线程

所以当我们保证在线程的生命周期只内,能访问到,就可以把值放到里面。

常用方法有下面这些,但是通常还需要进一步封装

苍穹外卖学习笔记_第19张图片

1.4.2.3 解决原理
  1. 代码中已经提供了封装好的ThreadLocal,名字是BaseContext
  2. 因为每次请求都会用开辟一个线程
  3. 而且每次请求前端都会带token过来,所以它发送保存用户的时候也会带token过来进行验证
  4. 所以将jwt验证出来的token放置在ThreadLocal中
  5. 因为这次保存和他是一个线程,所以ThreadLocal还是一个,所以保存的时候直接从ThreadLocal中获取即可
1.4.2.4 解决方式
  1. 在jwt验证的代码中放置token到ThreadLocal
  2. 在service中获取ThreadLocal将id放进去

2.员工分页查询

2.1 需求分析

苍穹外卖学习笔记_第20张图片

苍穹外卖学习笔记_第21张图片

2.2 代码开发

  1. 因为传过来了三个参数,所以封装到DTO中【资料中已经封装好了】

  2. 返回类型是Result

    • Result是我们自己写的类,它的参数和上面接口中需要的数据正好对应【code、msg、data】

    • PageResult也是我们封装的,有两个参数records和total,分别对应前端接口中data的数据

  3. 添加注解@GetMapping注解

  4. 接收对象是DTO

    应为使用的是Get方式,参数以query方式【也就是最常见的Get方式】传过来的,因此数据名和参数名一样可以自动封装

  5. 编写操作,返回Result,参数是PageResult

  6. 编写Service

  7. 使用自动分页插件pagehelper【依赖已经导入】

    调用Pagehelper.startPage方法【会动态将limit拼接到sql中】,不需要创建对象

    然后执行Mapper中的查询方法,返回类型必须是Page【pagehelper要求的】

    然后返回PageResult(total,records)【这两个参数Page对象可以提供】

  8. 编写Mapper

    • 因为使用了动态SQL,所以将sql写到映射文件中
    • 映射文件地址yaml中配置了
    • 这里可以使用模糊查询
    • 最好设置排序

2.3 功能测试

如果测试不成功,可能是token过期了

调试发现日期格式不太正确

2.4 代码完善

完善时间格式

解决方式:

  • 方式一:属性上加上注解,对日期进行格式化【缺点只能处理这一个属性】

    @JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss")
    
  • 方式二:在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式化处理

//这一部分是瑞吉外卖的,和这个是一个解决方法

问题:员工的id是Long型的19位数字,但是js只能保存16位,所以造成js给我们传过来的用户id不正确

解决方法:将Long型的数据统一转换为String字符串

解决原理:后端给前端响应数据的时候会使用到Spring MVC的消息转换器,所以我们扩展一个消息转换器,在这个消息转换器中在java转json中进行统一处理。

处理的时候会调用对象转换器JacksonObjectMapper【不需要自己手写,资料里给了】,这个对象转换器底层基于jackson

注:这个貌似是StringRedisTemplate转json用到的

  1. 拷贝JacksonObjectMapper代码到common包中【工程里有了】

  2. 在WebMvcConfig中重写extendMessageConverters类

        /**
         * 扩展mvc框架的消息转换器
         * @param converters
         */
        @Override
        protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
            log.info("扩展消息转换器...");
            //创建消息转换器对象
            MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
            //设置对象转换器,底层使用Jackson将Java对象转为json
            messageConverter.setObjectMapper(new JacksonObjectMapper());
            //将上面的消息转换器对象追加到mvc框架的转换器集合中
            converters.add(0,messageConverter);//放到第一个使用
        }
    
  3. 然后就可以直接使用了

3.启用禁用员工账号

3.1 需求分析

苍穹外卖学习笔记_第22张图片

苍穹外卖学习笔记_第23张图片

3.2 代码开发

  1. 请求参数status是根据路径传过来的/admin/employee/status/{status}

  2. 请求参数id是query方法直接拼接到地址栏的

  3. 所以添加注解@PostMapping(“/admin/employee/status/{status}”)

  4. 参数列表使用@PathVariable取出status,另一个可以直接注入

    如果参数名和地址栏中的不一样,就需要PathVariable注解有值

  5. 编写Service接口和编写impl类

  6. 编写Mapper,因为想做成一个通用的修改所以使用动态SQL,SQL语句就在映射文件中写

4.编辑员工

4.1需求分析

苍穹外卖学习笔记_第24张图片

苍穹外卖学习笔记_第25张图片

苍穹外卖学习笔记_第26张图片

苍穹外卖学习笔记_第27张图片

  1. 点击编辑后,会发送get请求,请求参数id直接在地址框中
  2. 后端根据id查询员工信息,然后将员工信息以json形式响应
  3. 前端接收到后将数据更新到表单中,供用户修改
  4. 用户保存后又发送给后端
  5. 后端保存完成后给界面响应
  6. 前端根据响应结果进行相关处理

4.2 代码开发

  1. 创建一个根据id获取员工信息的方法

  2. 使用@GetMapping注解,因为参数直接在地址里面了,所以使用占位符的方法

    @GetMapping("/{id}")
    
  3. 方法的参数列表使用@PathVariable注解,因为参数直接在地址中了

  4. 编写代码

    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<Employee> getById(@PathVariable Long id){
        log.info("根据id查询员工信息...");
        Employee employee = employeeService.getById(id);
        if(employee != null){
            return R.success(employee);
        }
        return R.error("没有查询到对应员工信息");
    }
    
  5. 然后编写更新方法,使用@PutMapping,不需要值【因为请求地址就是Controller的请求地址】

  6. 然后将EmployeeDTO的值传给Employee对象,然后设置修改时间、id等

  7. 然后调用上面写的update方法

八、导入菜品分类模块代码

说明:因为这一部分操作和员工操作类似,所以就直接导入了。后面可以自己练手

1.1需求分析

苍穹外卖学习笔记_第28张图片

苍穹外卖学习笔记_第29张图片

苍穹外卖学习笔记_第30张图片

九、菜品管理开发

1.公共字段自动填充

1.1 需求分析

很多表都有下面的公共字段,例如:管理员工、管理菜品

如果后期需要修改,就会很麻烦,所以要完善这一部分

苍穹外卖学习笔记_第31张图片

1.2 实现思路

先明确操作类型,然后使用AOP,给这几个操作

苍穹外卖学习笔记_第32张图片

  • 先自定义注解AutoFill:用于表示需要进行公共字段自动填充的方法
  • 然后自定义切面类AutoFillAspect:统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
  • 然后只需要给Mapper接口加上AutoFill注解

技术点:枚举、注解、AOP、反射

1.3 代码实现

  1. 自定义AutoFill注解【只用来标识,标识那些类需要自动填充】

    1. 创建annotaion包用来存放自定义注解,然后创建AutoFill注解

    2. 然后添加注解@Target(ElementType.METHOD)用来说明注解是加到方法上的

    3. 然后添加@Retention(RetentionPolicy.RUNTIME)注解

    4. 在注解里面指定当前数据库操作类型【可以使用枚举】【定义了之后参数列表的value就可以使用枚举类的值】

      OperationType value();//自定义的枚举类型【里面是update和insert】
      
  2. 自定义切面类AutoFillAspect

    1. 创建aspect包,用于存放切面类,然后创建AutoFillAspect类

    2. 添加注解@Component、@Aspect

    3. 使用注解的方式创建切入点

      /**
      *切入点
      */
      //&&前面一部分是说明拦截哪些方法
      //后面这一部分是说明拦截哪个注解
      //&& 说明这连个条件都需要成立
      @PonitCut("execution(* com.sky.mapper.*.*(..)) && @annotation(come.sky.annotaion.AutoFill)")
      public void autoFillPoint(){
      }
                                                                  
      /**
      *前置通知
      */
      @Before("autoFillPoint")//标识增强哪个方法
      public void autoFill(JoinPoint joinPoint){
          //获取拦截方法的数据库操作类型【更新还是插入】
           MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
           AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//通过方法签名对象可以获得方法上的注解对象
           OperationType operationType = autoFill.value();//通过注解对象的value属性可以获得数据库操作类型
          //获取拦截方法的参数类型
          Object[] args = joinPoint.getArgs();//获取拦截方法的所有参数
              if(args == null || args.length == 0){//判断是否有参数
                  return;
              }
                                                                  
              Object entity = args[0];//我们约定第一个参数是employee,所以直接获取第一个参数
          //使用反射根据数据库操作类型赋予不同的值
          	//准备赋值的数据
              LocalDateTime now = LocalDateTime.now();
              Long currentId = BaseContext.getCurrentId();
          if(operationType == OperationType.INSERT){
                  //为4个公共字段赋值
                  try {
                      //使用反射获取set方法的Method对象
                      //为了防止set方法名写错,所以参数列表使用了常量
                      Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                      Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                      Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                      Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                                                                  
                      //通过反射为对象属性赋值
                      setCreateTime.invoke(entity,now);
                      setCreateUser.invoke(entity,currentId);
                      setUpdateTime.invoke(entity,now);
                      setUpdateUser.invoke(entity,currentId);
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
              }else if(operationType == OperationType.UPDATE){
                  //为2个公共字段赋值
                  try {
                      Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                      Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                                                                  
                      //通过反射为对象属性赋值
                      setUpdateTime.invoke(entity,now);
                      setUpdateUser.invoke(entity,currentId);
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
              }
      }
      
  3. 给需要操作的Mapper中的方法添加@AutoFill注解

2.新增菜品

2.1 需求分析

苍穹外卖学习笔记_第33张图片

苍穹外卖学习笔记_第34张图片

苍穹外卖学习笔记_第35张图片

苍穹外卖学习笔记_第36张图片

苍穹外卖学习笔记_第37张图片

苍穹外卖学习笔记_第38张图片

苍穹外卖学习笔记_第39张图片

逻辑外键:数据库中并没有设置,而是我们java代码自己处理

2.2 代码开发

根据类型分类查询已经导入了,所以直接使用即可

2.2.1 文件上传
  1. 导入阿里云oss的依赖【官网有使用说明】

  2. 创建一个CommonController【作为通用Controller】

  3. 添加注解@RespController、@RequestMapping(“/admin/common”)

  4. 编写文件上传方法,参数类型是MultpartFile

  5. 添加@PostMapping注解【文件上传必须是post方式】

  6. 编写配置属性类,然后再springboot的配置文件中配置【配置oss的地址等】

  7. 编写配置类,管理阿里云工具类的创建【方法上除了加@Bean注解,还可以加@ConditionalOnMissingBean注解,当没有这个对象的时候才创建】

    /**
     * 通用接口
     */
    @RestController
    @RequestMapping("/admin/common")
    @Api(tags = "通用接口")
    @Slf4j
    public class CommonController {
    
        @Autowired
        private AliOssUtil aliOssUtil;
    
        /**
         * 文件上传
         * @param file
         * @return
         */
        @PostMapping("/upload")
        @ApiOperation("文件上传")
        public Result<String> upload(MultipartFile file){
            log.info("文件上传:{}",file);
    
            try {
                //原始文件名
                String originalFilename = file.getOriginalFilename();
                //截取原始文件名的后缀   dfdfdf.png
                String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
                //构造新文件名称
                String objectName = UUID.randomUUID().toString() + extension;
    
                //文件的请求路径
                String filePath = aliOssUtil.upload(file.getBytes(), objectName);
                return Result.success(filePath);
            } catch (IOException e) {
                log.error("文件上传失败:{}", e);
            }
    
            return Result.error(MessageConstant.UPLOAD_FAILED);
        }
    }
    
2.2.2 添加菜品【使用了多表操作、事务】
  1. 编写一个新的Controller用于管理菜品
  2. 编写方法
  3. 编写ServiceImpl【因为需要多表操作,所以需要开启事务】
  4. ServiceImpl方法上加@Transactional
  5. 菜品表会插入一条数据,口味表会插入n条数据
  6. 启动类开启注解事务管理@EnableTransactionManagement
  7. 添加Mapper接口、映射文件
//ServiceImpl
@Service
@Slf4j
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;

    /**
     * 新增菜品和对应的口味
     *
     * @param dishDTO
     */
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO) {

        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);

        //向菜品表插入1条数据
        dishMapper.insert(dish);//后绪步骤实现

        //获取insert语句生成的主键值
        Long dishId = dish.getId();

        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            //向口味表插入n条数据
            dishFlavorMapper.insertBatch(flavors);//后绪步骤实现
        }
    }

}

DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
    //useGeneratedKeys:表示需要自增id(可以把自增的主键返回注入到原对象中)
    //keyProperty:表示需要哪个自增属性
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user, status)
        values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})
    insert>
mapper>

3.菜品分页查询

3.1 业务分析

苍穹外卖学习笔记_第40张图片

苍穹外卖学习笔记_第41张图片

3.2 代码实现

  1. 其他的一样,但是需要多表查询
<select id="pageQuery" resultType="com.sky.vo.DishVO">
        select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id
        <where>
            <if test="name != null">
                and d.name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and d.category_id = #{categoryId}
            </if>
            <if test="status != null">
                and d.status = #{status}
            </if>
        </where>
        order by d.create_time desc
</select>

4.删除菜品

4.1 业务分析

苍穹外卖学习笔记_第42张图片

苍穹外卖学习笔记_第43张图片

苍穹外卖学习笔记_第44张图片

苍穹外卖学习笔记_第45张图片

4.2 代码实现

	/**
     * 菜品批量删除
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("菜品批量删除")
    public Result delete(@RequestParam List<Long> ids) {
        log.info("菜品批量删除:{}", ids);
        dishService.deleteBatch(ids);//后绪步骤实现
        return Result.success();
    }
    @Autowired
    private SetmealDishMapper setmealDishMapper;
	/**
     * 菜品批量删除
     *
     * @param ids
     */
    @Transactional//事务
    public void deleteBatch(List<Long> ids) {
        //判断当前菜品是否能够删除---是否存在起售中的菜品??
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);//后绪步骤实现
            if (dish.getStatus() == StatusConstant.ENABLE) {
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        //判断当前菜品是否能够删除---是否被套餐关联了??
        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if (setmealIds != null && setmealIds.size() > 0) {
            //当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }

        //删除菜品表中的菜品数据
        for (Long id : ids) {
            dishMapper.deleteById(id);//后绪步骤实现
            //删除菜品关联的口味数据
            dishFlavorMapper.deleteByDishId(id);//后绪步骤实现
        }
    }

DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealDishMapper">
    <select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
        select setmeal_id from setmeal_dish where dish_id in
        <foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
            #{dishId}
        foreach>
    select>
mapper>

5.修改菜品

5.1 业务分析

苍穹外卖学习笔记_第46张图片

苍穹外卖学习笔记_第47张图片

5.2 代码开发

5.2.1 根据id查询菜品

就是常规操作

5.2.2 修改菜品

因为口味需要判断加没加,操作很复杂,所以口味那里先删除,然后再根据传过来的数据插入

十、配置RedisTemplate

  1. 导入Redis的依赖

  2. yaml中配置Redis的数据源

  3. 编写配置类【用来管理redisTemplate对象】

    @Configuration
    public class RedisConfig {
        /**
         * redis序列化的工具配置类,下面这个请一定开启配置
         * 127.0.0.1:6379> keys *
         * 1) "ord:102"  序列化过
         * 2) "\xac\xed\x00\x05t\x00\aord:102"   野生,没有序列化过
         * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
         * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法
         * this.redisTemplate.opsForSet(); //提供了操作set的所有方法
         * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法
         * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法
         * @param lettuceConnectionFactory
         * @return
         */
       @Bean
        public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
            // 准备RedisTemplate对象
            RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
            // 设置连接工厂
            redisTemplate.setConnectionFactory(connectionFactory);
            // 创建JSON序列化工具
            GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
            // 设置key的序列化
            redisTemplate.setKeySerializer(RedisSerializer.string());
            redisTemplate.setHashKeySerializer(RedisSerializer.string());
            // 设置value的序列化
            redisTemplate.setValueSerializer(jsonRedisSerializer);
            redisTemplate.setHashValueSerializer(jsonRedisSerializer);
            // 返回
            return redisTemplate;
        }
    }
    
  4. 测试一下是否可以使用

十一、设置店铺营业状态

1.需求分析

苍穹外卖学习笔记_第48张图片

苍穹外卖学习笔记_第49张图片

image-20230727011755423

2.代码开发

因为店铺营业状态只有两种,单独设置一张sql表单很浪费,所以设置到Redis中

苍穹外卖学习笔记_第50张图片

十二、HttpClient

1.HttpClient介绍

HttpClient是客户端编程的工具包【客户端发请求给服务器】

使用HttpClient可以通过java构建、发送HTTP请求

2.核心API

  • HttpClient:可以发送HTTP请求
  • HttpClients:使用它可以创建HTTP对象
  • CloseableHttpClient:是HttpClient的具体实现类
  • HttpGet:Get方式的请求
  • HttpPost:Post方式的请求

3.发送请求步骤

  1. 创建HttpClient对象
  2. 创建HTTP请求对象【GET方式就创建GET对象、Post方式就创建Post对象】
  3. 调用HttpClient的execute方法发送请求

4.使用练习

  1. 导入依赖

    <dependency>
    	<groupId>org.apache.httpcomponentsgroupId>
    	<artifactId>httpclientartifactId>
    	<version>4.5.13version>
    dependency>
    
  2. 使用Get方式发送

    public class HttpClientTest {
        /**
         * 使用HttpClient发送get请求
         */
        @Test
        public void testGetMethod() throws IOException {
            //创建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();
        }
    }
    
  3. 使用Post方式发送【需要发送参数,所以封装到接送中】

    public void testPostMethod() throws JSONException, IOException {
        //创建httpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
    
        //创建post对象,参数是请求地址
        HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
    
        //因为需要发送json类型的参数,所以把数据封装到json对象中
        JSONObject jsonObject = new JSONObject();//fastJson提供的
        jsonObject.put("username","admin");
        jsonObject.put("password","123456");
    
        StringEntity stringEntity = new StringEntity(jsonObject.toString());
        //指定请求编码方式
        stringEntity.setContentEncoding("utf-8");
        //设置可以接收的数据格式
        stringEntity.setContentType("application/json");
        httpPost.setEntity(stringEntity);
    
        //发送请求,获取响应结果
        CloseableHttpResponse response = httpClient.execute(httpPost);
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("响应状态码为:"+statusCode);
        HttpEntity entity = response.getEntity();
        String s = EntityUtils.toString(entity);
        System.out.println("响应体为:"+s);
    }
    

5.说明

为了方便开发,已经封装了一个工具类

十三、微信小程序开发介绍

1.微信小程序准备工作

1.1 注册小程序

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

1.2 完善小程序信息

1.3 下载开发者工具

微信开发者工具下载地址与更新日志 | 微信开放文档 (qq.com)

需要设置不校验合法域名

2.小程序目录结构

一个小程序主体部分由三个文件组成,必须放在项目的根目录下

苍穹外卖学习笔记_第51张图片

一个小程序页面由四个文件组成

苍穹外卖学习笔记_第52张图片

3.发布小程序

开发者工具中点击上传

苍穹外卖学习笔记_第53张图片

然后到开发者后台网页,提交审核就就能发布上线

苍穹外卖学习笔记_第54张图片

十四、微信用户端登录功能

1.导入小程序代码

  1. 导入源码

苍穹外卖学习笔记_第55张图片

  1. 填写APPID
  2. 找到common中的vendor.jsbaseUrl改为自己的后端ip

2.了解微信登录流程

  1. 小程序中调用wx.login方法获取code
  2. 然后wx.request方法带着code请求后端
  3. 后端接收请求,调用httpclient请求微信接口服务【需要携带三个参数:appidappsecretcode
  4. 接口服务会返回openid等
  5. 然后我们可以使用token定义用户登录状态
  6. 客户端收到token会保存起来,进行操作会发token
  7. 后台根据token进行操作

3.需求分析

苍穹外卖学习笔记_第56张图片

苍穹外卖学习笔记_第57张图片

4.代码实现

  1. 在配置文件中配置小程序信息的配置项【appid、secret】

    sky:
      wechat:
        appid: wxffb3637a228223b8
        secret: 84311df9199ecacdf4f12d27b6b9522d
    
  2. 配置为微信用户生成jwt令牌的配置项

    sky:
      jwt:
        # 设置jwt签名加密时使用的秘钥
        admin-secret-key: itcast
        # 设置jwt过期时间
        admin-ttl: 7200000
        # 设置前端传递过来的令牌名称
        admin-token-name: token
        # 设置jwt签名加密时使用的秘钥
        user-secret-key: itheima
        # 设置jwt过期时间
        user-ttl: 7200000
        # 设置前端传递过来的令牌名称
        user-token-name: authentication
    
  3. 编写用户接口,返回值是UserLoginVo

  4. 编写service

    1. 封装数据发送给微信接口
    2. 解析微信接口发过来的数据
    3. 如果openid是空的那么登录失败
    4. 如果不是空的,看openid是否在数据库中
    5. 不在数据库中表示是新用户,那么保存到数据库中
    6. 在数据库中表示是老用户
    7. 返回VO
  5. 编写Mapper【查询用户的openid是否在数据库中、添加用户】

5.完善代码

编写用户操作的拦截器

protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login")
                .addPathPatterns("/user/**").
                excludePathPatterns("/user/user/login").
                excludePathPatterns("/user/shop/status");
    }

十五、导入商品浏览功能

1.需求分析

image-20230727193346210

十六、缓存菜品

1.问题说明

如果客户访问量大的时候,数据库访问压力增大,导致用户体验差

2.实现思路

通过Redis缓存菜品数据,减少数据库查询操作

苍穹外卖学习笔记_第58张图片

将list集合序列化成String

苍穹外卖学习笔记_第59张图片

数据库中的餐品数据有变更的时候,要清空Redis中的数据【例如:管理员添加菜品、修改菜品、删除菜品】

3.代码开发

直接改造已有的用户端查询菜品方法就可以(DishController中的list方法)

新增菜品、修改菜品、删除菜品、起售停售菜品都需要清理缓存

  1. 根据客户端传过来的id,查询Redis,返回菜单列表list

  2. 如果list不为空,则菜品存在直接返回数据,无需查询数据库

    java后端给Redis放进去什么类型,取出来就是什么类型(强转就可以不需要序列化了)

  3. 如果list为空,则缓存中没有,查询数据库,并将数据放到Redis中

  4. 返回数据

4. 遇到的问题

将数据放到Redis的时候,说序列化异常,把序列化配置中的下面这段代码注释掉就可以了,具体为啥不知道

redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);

十七、Spring Cache

1.介绍

是一个框架,实现了基于注解的缓存功能,只需要加一个注解,就能实现缓存功能。

提供了一层抽象,底层可以千幻不同的缓存实现,例如:EHCache、Caffeine、Redis

2.使用介绍

2.1 引入依赖

<dependency>   
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>2.7.3</version>
</dependency>

2.2 常用注解

注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类上
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除

3.使用案例

  1. 给启动类加上EnableCaching注解【说明开启注解缓存功能】

  2. 给Controller方法加上对应的注解

    例如:保存用户功能,不需要查询是否有没有,所以可以加上@CachePut注解

  3. 设置注解的参数

    cacheNames:表示key的前缀

    key:表示后缀

    那么存在Redis中的格式就是cacheNames::key

    例如:@CachePut(cacheNames=“user”,key=“abc”)

    那么Redis中的key就是user::abc

    如果需要动态设置key属性的值那么就使用#

    例如:@CachePut(cacheNames=“user”,key=“#user.id”)那么就会动态获取到id

    这里key属性中的user是被修饰的方法中参数的名

    如果key属性的值需要根据返回值的设置,那么就是用#result

    例如:@CachePut(cacheNames=“user”,key=“#result.id”)那么就会动态获取到返回值对象的id

    说明:#result会得到返回值

    说明:Cacheable不能用#result

    说明:CacheEvict可以删除单个键,也可以批量删除键

    单个删除就是正常的方法,批量删除就需要设置allEntries,那么就会删除所有cacheNames

    例如:@CacheEvict(cacheNames=“user”,allEntries=true)那么就会删除所有以user开头的

4.使用实战(缓存套餐)

image-20230729011238350

十七、购物车

1.添加购物车

1.1 需求分析

苍穹外卖学习笔记_第60张图片

苍穹外卖学习笔记_第61张图片

苍穹外卖学习笔记_第62张图片

苍穹外卖学习笔记_第63张图片

1.2 代码实现

  1. 创建购物车controller【添加相关注解】

  2. 创建添加购物车方法【添加注解,方法参数是ShoppingCartDTO】

  3. 创建购物车Service,添加方法

  4. 创建Service实现类

    先判断商品是否在购物车,如果不在就添加商品,如果在商品数量加一

  5. 创建Mapper接口

  6. 创建映射文件,编写动态sql

2.查看购物车

用户id通过ThreadLocal获得

3.清空购物车

4. 删除购物车中一个商品

作业

十八、用户下单功能

1.导入地址薄功能代码

1.1 需求分析

  • 列表查询地址功能
  • 设为默认地址功能
  • 新增地址功能
  • 删除地址功能
  • 修改地址功能
  • 查询默认地址

1.2 导入代码

因为都是单表查询,没有复杂业务需求,所以代码直接导入

2.用户下单功能

2.1 需求分析

苍穹外卖学习笔记_第64张图片

苍穹外卖学习笔记_第65张图片

苍穹外卖学习笔记_第66张图片

苍穹外卖学习笔记_第67张图片

苍穹外卖学习笔记_第68张图片

2.2 代码开发

接收前端信息使用OrderSubmintDTO,返回给前端数据使用OrderSubmitVo

  1. 创建Controller

    处理各种原因的异常【地址薄为空、购物车商品为空】

    向订单表插入1条数据

    根据返回的id,向订单明细表插入N条数据

    然后清空购物车

    然后清空当前用户的购物车数据

  2. 创建Service

  3. 创建Mapper以及映射文件

十九、订单支付功能

由于这部分代码是固定的,而且个人账号无法使用微信支付,所以这部分直接导入就好

1.微信支付介绍

1.1 微信支付流程

苍穹外卖学习笔记_第69张图片

1.2 微信支付时序图

苍穹外卖学习笔记_第70张图片

2.微信支付准备功能

2.1 获得微信给的两个文件

这两个文件都是微信支付给商户提供的,个人无法获得

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

2.2 内网穿透

解决个人测试的时候,微信无法访问我们本机IP的问题

安装cpolar:cpolar - 安全的内网穿透工具

在安装目录执行命令

苍穹外卖学习笔记_第71张图片

然后执行下面的命令

image-20230729161138523

3.导入微信支付代码

3.1配置文件中添加微信支付配置

说明:yaml中能有这些属性,是因为定义了配置类 【里面的都是别人的数据,需要改成自己的

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

3.2 OrderController中的代码

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

3.3 OrderService中的代码

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

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

3.4 OrderServiceImpl中的代码

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

3.5 OrderMapper中的代码

	/**
     * 根据订单号和用户id查询订单
     * @param orderNumber
     * @param userId
     */
    @Select("select * from orders where number = #{orderNumber} and user_id= #{userId}")
    Orders getByNumberAndUserId(String orderNumber, Long userId);

    /**
     * 修改订单信息
     * @param orders
     */
    void update(Orders orders);

3.6 OrderMapper.xml中的代码

<update id="update" parameterType="com.sky.entity.Orders">
        update orders
        <set>
            <if test="cancelReason != null and cancelReason!='' ">
                cancel_reason=#{cancelReason},
            if>
            <if test="rejectionReason != null and rejectionReason!='' ">
                rejection_reason=#{rejectionReason},
            if>
            <if test="cancelTime != null">
                cancel_time=#{cancelTime},
            if>
            <if test="payStatus != null">
                pay_status=#{payStatus},
            if>
            <if test="payMethod != null">
                pay_method=#{payMethod},
            if>
            <if test="checkoutTime != null">
                checkout_time=#{checkoutTime},
            if>
            <if test="status != null">
                status = #{status},
            if>
            <if test="deliveryTime != null">
                delivery_time = #{deliveryTime}
            if>
        set>
        where id = #{id}
update>

3.7 PayNotifyController中的代码

package com.sky.controller.notify;

import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.annotation.IgnoreToken;
import com.sky.properties.WeChatProperties;
import com.sky.service.OrderService;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;

/**
 * 支付回调相关接口
 */
@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();
    }
}

二十、Spring Task

1.Spring Task介绍

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

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

应用场景:

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

2.cron表达式

本质就是一个字符串,通过cron表达式可以定义任务触发的事件

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

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

不指定的位置用表达,每个用*表达

苍穹外卖学习笔记_第72张图片

因为有很多表达式很复杂,所以不需要我们自己去写,直接去网上生成即可:https://cron.qqe2.com/

3.入门案例

3.1 导入依赖

SpringTask没有自己的依赖,只要导入了start依赖,那么他的包就引入了

3.2 开启任务调度

启动类上加上==@EnableScheduling==注解开启任务调度

3.3 自定义定时任务类

  1. 创建一个类,加上@Component注解

  2. 定义一个方法,没有返回值,方法名随意

  3. 方法上添加@Scheduled注解,在cron属性中写cron表达式

    @Scheduled(cron = "0/5 * * * * ?")//cron里面式cron表达式
    

二十一、订单状态定时处理功能

1.问题引入

用户下了订单,长时间没支付,直到订单超时,那么超时的订单我们怎么让系统自动处理

还有一种就是长时间没有点击送达的订单,怎么自动处理

2.需求分析

苍穹外卖学习笔记_第73张图片

苍穹外卖学习笔记_第74张图片

说明:为什么每天一点才检查,因为我们的项目是专门为某个店铺定制的,一点的时候店铺早就打烊了,所以直接改订单状态对我们的订单没有影响

3.代码实现

  1. 创建任务定时类【添加@Component注解】
  2. 编写方法【添加@Scheduled注解】
@Scheduled(cron = "0 * * * * ?")//1分钟检查一次是否有超时未支付订单
    public void orderTime(){
        log.info("开始处理超时订单");
        //根据当前时间查询数据库
        Orders orders = new Orders();
        //设置查询条件
        orders.setOrderTime(LocalDateTime.now().plusMinutes(-15));//当前时间的前15分钟
        orders.setPayStatus(0);
        orders.setStatus(1);
        List<Orders> list=ordersMapper.searchNoPay(orders);
        //将查询出来的list集合便遍历
        if (list!=null && list.size()>0){
            for (Orders o:list){
                //修改订单的状态为取消
                o.setStatus(6);
                o.setPayStatus(2);
                o.setCancelReason("用户支付超时");
                o.setCancelTime(LocalDateTime.now());
                ordersMapper.cancel(o);
            }
        }
    }

    /**
     * 处理超时送达订单
     */
    @Scheduled(cron = "0 0 1 * * ? *")//每天凌晨1点处理未确认收获订单
    public void orderSend(){
        log.info("开始处理超时送达订单");
        //根据当前时间查询数据库
        Orders orders = new Orders();
        orders.setStatus(4);
        List<Orders> list=ordersMapper.serchNoDelivery(orders);
        //将查询出来的list集合便遍历
        if (list!=null && list.size()>0){
            for (Orders o:list){
                o.setStatus(5);
                //修改订单的状态为送达
                ordersMapper.updateStatusById(o);
            }
        }
    }

二十二、Web Socket

1.介绍

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

苍穹外卖学习笔记_第75张图片

HTTP和WebSocket的对比
HTTP协议 客户端先发起请求,服务端才能响应。一次请求只能相应一次【单向、短链接、底层是TCP连接】
WebSocket协议 客户端和服务器握手之后,服务端和客户端可以持久双向通信【双向、长连接、底层是TCP连接】

应用场景【需要实时更新的业务】:

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

2.入门案例

要求浏览器能够发送websocket连接

2.1导入maven依赖

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-websocketartifactId>
dependency>

2.2导入WebSocketServer

这是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();
            }
        }
    }

}

2.3导入配置类WebSocketConfiguration

用于注册WebSocket的服务端组件

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

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

}

2.4业务类调用Server提供的方法

需要提前注入WebSocketServer对象

如果前端需要json类型的,可以使用JSON.toJSONString把数据转成Sting类型的json字符串

二十三、来单提醒功能

1.需求分析

苍穹外卖学习笔记_第76张图片

苍穹外卖学习笔记_第77张图片

2.代码实现

在用户付款成功方法中完善业务【用户订单方法需要提前注入webSocketServer对象】

Orders ordersDB = ordersMapper.getByNumberAndUserId(ordersSubmitDTO.getOrderNumber());
//用户下单之后调用websocket发送信息到后台管理端处理订单
Map map=new HashMap();
//下面的这三个数据是这个项目和前端约定好的
map.put("type",1);
map.put("orderId", ordersDB.getId());
map.put("content", "订单号:" + ordersDB.getNumber());
String s = JSON.toJSONString(map);
webSocketServer.sendToAllClient(s);

二十四、客户催单功能

1.需求分析

苍穹外卖学习笔记_第78张图片

苍穹外卖学习笔记_第79张图片

2.代码设计

默认Web Socket的后端配置已经完成

  1. 用户订单接口编写接口【用户点击催单后会请求到这里】
  2. serviceImpl中实现该方法【校验订单是否存在,如果存在就给浏览器推送消息】
  3. 调用webSocketServer的sendToAllClient方法,将信息发送过去
    /**
     * 用户催单
     * @param id
     */
    @Override
    public void reminder(Long id) {
        //已经传过来菜品的id了,直接崔就行了
        // 查询订单是否存在
        Orders orders = ordersMapper.getById(id);
        if (orders == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
        }

        //基于WebSocket实现催单
        Map map = new HashMap();
        map.put("type", 2);//2代表用户催单
        map.put("orderId", id);
        map.put("content", "订单号:" + orders.getNumber());
        webSocketServer.sendToAllClient(JSON.toJSONString(map));
    }

二十五、Apache ECharts

1.介绍

Apache ECharts是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。

官网地址:https://echarts.apache.org/zh/index.htm

后端了解就可以,这是一个前端的奇数,我们知道怎么响应数据就可以

图形的中类:柱状图、折线图、饼形图

苍穹外卖学习笔记_第80张图片

2.入门案例

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

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

  1. 提前下载这个js文件
  2. 前端页面中引入这个js文件
  3. 定义一个div,id是main
  4. 然后写一段js代码【将数据展示到页面上】

二十六、营业额统计功能

1.需求分析

起始日期和结束日期前端已经传过来了,后天只需要返回一个日期字符串、营业额字符串

苍穹外卖学习笔记_第81张图片

苍穹外卖学习笔记_第82张图片

2.代码开发

方法跟其他的代码一样,只不过业务逻辑是根据传过来的日期范围直接计算出日期集合,然后循环日期集合查询数据库,查询这一天已完成订单的总营业额

/**
 * 报表
 */
@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
@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;//如果这一天没有订单,就设置为0.0
            turnoverList.add(turnover);
        }

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

二十七、用户统计功能

1.需求分析

所谓用户统计,实际上统计的是用户的数量。通过折线图来展示,上面这根蓝色线代表的是用户总量,下边这根绿色线代表的是新增用户数量,是具体到每一天。所以说用户统计主要统计两个数据,一个是总的用户数量,另外一个是新增用户数量

苍穹外卖学习笔记_第83张图片

苍穹外卖学习笔记_第84张图片

2.代码实现

/**
     * 用户数据统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/userStatistics")
    @ApiOperation("用户数据统计")
    public Result<UserReportVO> userStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){

        return Result.success(reportService.getUserStatistics(begin,end));            
}
	@Override
    public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);

        while (!begin.equals(end)){
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        List<Integer> newUserList = new ArrayList<>(); //新增用户数
        List<Integer> totalUserList = new ArrayList<>(); //总用户数

        for (LocalDate date : dateList) {
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
            //新增用户数量 select count(id) from user where create_time > ? and create_time < ?
            Integer newUser = getUserCount(beginTime, endTime);
            //总用户数量 select count(id) from user where  create_time < ?
            Integer totalUser = getUserCount(null, endTime);

            newUserList.add(newUser);
            totalUserList.add(totalUser);
        }

        return UserReportVO.builder()
                .dateList(StringUtils.join(dateList,","))
                .newUserList(StringUtils.join(newUserList,","))
                .totalUserList(StringUtils.join(totalUserList,","))
                .build();
    }
    	/**
     * 根据时间区间统计用户数量
     * @param beginTime
     * @param endTime
     * @return
     */
    private Integer getUserCount(LocalDateTime beginTime, LocalDateTime endTime) {
        Map map = new HashMap();
        map.put("begin",beginTime);
        map.put("end", endTime);
        return userMapper.countByMap(map);
    }

二十八、订单统计功能

1.需求分析

订单统计通过一个折现图来展现,折线图上有两根线,这根蓝色的线代表的是订单总数,而下边这根绿色的线代表的是有效订单数,指的就是状态是已完成的订单就属于有效订单,分别反映的是每一天的数据。上面还有3个数字,分别是订单总数、有效订单、订单完成率,它指的是整个时间区间之内总的数据。

原型图:

苍穹外卖学习笔记_第85张图片

业务规则:

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

苍穹外卖学习笔记_第86张图片

2.代码实现

/**
* 根据时间区间统计订单数量
* @param begin 
* @param end
* @return 
*/
public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end){
	List<LocalDate> dateList = new ArrayList<>();
    dateList.add(begin);

    while (!begin.equals(end)){
          begin = begin.plusDays(1);
          dateList.add(begin);
     }
    //每天订单总数集合
     List<Integer> orderCountList = new ArrayList<>();
    //每天有效订单数集合
    List<Integer> validOrderCountList = new ArrayList<>();
    for (LocalDate date : dateList) {
         LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
         LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
   //查询每天的总订单数 select count(id) from orders where order_time > ? and order_time < ?
         Integer orderCount = getOrderCount(beginTime, endTime, null);

  //查询每天的有效订单数 select count(id) from orders where order_time > ? and order_time < ? and status = ?
         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();
    
}

/**
* 根据时间区间统计指定状态的订单数量
* @param beginTime
* @param endTime
* @param status
* @return
*/
private Integer getOrderCount(LocalDateTime beginTime, LocalDateTime endTime, Integer status) {
	Map map = new HashMap();
	map.put("status", status);
	map.put("begin",beginTime);
	map.put("end", endTime);
	return orderMapper.countByMap(map);
}

二十九、销量排名统计功能

1.需求分析

所谓销量排名,销量指的是商品销售的数量。项目当中的商品主要包含两类:一个是套餐,一个是菜品,所以销量排名其实指的就是菜品和套餐销售的数量排名。通过柱形图来展示销量排名,这些销量是按照降序来排列,并且只需要统计销量排名前十的商品。

原型图:

苍穹外卖学习笔记_第87张图片

业务规则:

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

苍穹外卖学习笔记_第88张图片

2.代码开发

/**
* 销量排名统计
* @param begin
* @param end
* @return
*/
@GetMapping("/top10")
@ApiOperation("销量排名统计")
public Result<SalesTop10ReportVO> top10(
    @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
    @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
	return Result.success(reportService.getSalesTop10(begin,end));
}
/**
     * 查询指定时间区间内的销量排名top10
     * @param begin
     * @param end
     * @return
     * */
    public SalesTop10ReportVO getSalesTop10(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);

        String nameList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList()),",");
        String numberList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList()),",");

        return SalesTop10ReportVO.builder()
                .nameList(nameList)
                .numberList(numberList)
                .build();
    }
<!--查询订单完成的详情-->
<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
        select od.name name,sum(od.number) number from order_detail od ,orders o
        where od.order_id = o.id
            and o.status = 5
            <if test="begin != null">
                and order_time &gt;= #{begin}
            </if>
            <if test="end != null">
                and order_time &lt;= #{end}
            </if>
        group by name
        order by number desc
        limit 0, 10
</select>

三十、工作台

1.需求分析

image-20230801141443597

2.代码导入

就是简单的增删改查,直接导入

三十一、Apache POI

1.介绍

Apache POI是一个处理office各种文件格式的开源项目,简单来说我们使用POI在java程序中对office文件进行各种读写操作

苍穹外卖学习笔记_第89张图片

2.入门案例

2.1 导入Maven坐标

<dependency>
    <groupId>org.apache.poigroupId>
    <artifactId>poiartifactId>
    <version>3.16version>
dependency>
<dependency>
    <groupId>org.apache.poigroupId>
    <artifactId>poi-ooxmlartifactId>
    <version>3.16version>
dependency>

2.2 向文件中写入内容

  • 如果没有excel文件
    1. 在内存中创建excel文件
    2. 在excel文件中创建sheet页【可以设置名字】
    3. 在sheet中创建行对象【序号从0开始(0表示第一行)】
    4. 在行对象中创建单元格【序号从0开始】
    5. 将内存中的文件写入到硬盘上
//在内存中创建一个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();

2.3 读取文件内容到程序中

  1. 读取磁盘上已经存在的文件,然后创建excel文件
  2. 获取sheet页对象
  3. 获取最后一行有文字的行号
  4. 然后循环遍历【获得行对象,然后获得单元格对象,然后获取单元格的内容】
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);
}
//关闭资源
excel.close();
in.close();

三十二、导出数据Excel报表

1.需求分析

苍穹外卖学习笔记_第90张图片

苍穹外卖学习笔记_第91张图片

2.代码开发

  • 开发步骤
    1. 电脑上先创建好excel的模板,放到程序里面
    2. 查询30天的运营数据
    3. 将查询到的运营数据写入模板
    4. 通过输出流将Excel文件下载到客户端浏览器
	/**
     * 导出运营数据报表
     * @param response
     */
    @GetMapping("/export")
    @ApiOperation("导出运营数据报表")
    public void export(HttpServletResponse response){
        reportService.exportBusinessData(response);
    }
    /**导出近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));
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
        try {
            //基于提供好的模板文件创建一个新的Excel表格对象
            XSSFWorkbook excel = new XSSFWorkbook(inputStream);
            //获得Excel文件中的一个Sheet页
            XSSFSheet sheet = excel.getSheet("Sheet1");

            sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end);
            //获得第4行
            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 = begin.plusDays(i);
               //准备明细数据
                businessData = 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(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();
        }
    }

你可能感兴趣的:(项目笔记,学习,笔记)