开源项目相关技术点和功能点

文章目录

开源项目-sky-take-out

gitee仓库地址:https://gitee.com/the-idea-of-starry-sky/sky-take-out.git

一、git

1.1 介绍

Git是一个分布式版本控制工具,主要用于管理开发过程中的源代码文件。

在开发中产生如下问题,该如何解决呢?

  • 如果磁盘满了,或者是磁盘损坏了,是不是就意味着代码就丢失了呢?
  • 一般项目都是协同开发,需要多人合作,那同事之间的代码该如何共享呢?
  • 在项目开发的过程中,由于需求问题,想要把代码回退到前5天的代码,又该如何完成

以上问题都可以使用git来解决,它的核心作用如下:

  • 代码回溯: 代码回溯是指在Git中返回到先前的代码状态或版本。对于修复错误或者恢复不小心删除的代码非常有用。
  • 版本切换: 在Git中,你可以通过切换分支或检出不同的提交来切换代码的版本。
  • 多人协作: Git允许多个开发者在同一个代码库上协同工作。开发者可以共享代码、合并变更、解决冲突等
  • 远程备份: Git允许你将代码库备份到远程服务器,以防止数据丢失。常见的远程备份方式包括使用GitHub、GitLab、Gitee等代码托管服务。

git的工作流程

  • 本地仓库:开发人员自己电脑上的 Git 仓库
  • 远程仓库:远程服务器上的 Git 仓库
  • 工作空间:工作目录,主要用于存放开发的代码
  • 暂存区:暂存区是一个临时保存修改文件的地方

1.2 git常用命令

1.2.1 全局配置命令

一个服务器一般只配置一次

  • git config --list
  • git config --global user.name “XXX”
  • git config --global user.email “XXXX@XXX”

1.2.2 本地仓库命令

初始化Git仓库:git init

  • git add abc.txt 把指定文件添加暂存区
  • git add * | git add . 把当前目录及其子目录下的所有文件都添加暂存区
  • git commit -m "abc.txt文件第一次提交到本地仓库" 提交一个文件
  • git commit -m "abc.txt文件第一次提交到本地仓库" 提交多个文件

1.2.3 远程仓库命令

1.2.4 分支常见命令

1.查询分支

  • git branch 列出所有本地分支
  • git branch -r 列出所有远程分支
  • git branch -a 列出所有本地分支和远程分支

2.创建分支

  • git branch 分支名称

3.切换分支

  • git checkout 分支名称

4.将当前分支推送到远端

  • git push 远程仓库简称 分支命令

1.2.5 分支冲突命令

1.查看冲突:

  • 使用git status命令查看哪些文件存在冲突。
  • 使用git diff命令查看冲突文件的差异。

2.手动解决冲突

3.标记冲突已解决

  • 使用git add命令将解决冲突后的文件添加到暂存区,标记为冲突已解决。

4.提交更改

  • 使用git commit命令提交解决冲突后的更改。在提交信息中,可以简要描述解决冲突的过程。
  • git commit -a 全部提交
  • git commit abc.txt -i -m "冲突解决" 提交部分文件

5.继续合并或推送

  • 如果是在合并分支时产生的冲突,解决冲突后可以继续合并其他分支。
  • 如果需要,将更改推送到远程仓库。

1.3 idea中集成git

关联git

详细使用地址,请打开,我这里就不多赘述了https://blog.csdn.net/king_faner/article/details/143066442?ops_request_misc=%257B%2522request%255Fid%2522%253A%25224f3f853ac445b655a097d267b6933b8c%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=4f3f853ac445b655a097d267b6933b8c&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-1-143066442-null-null.142v101pc_search_result_base4&utm_term=%E4%B8%8B%E8%BD%BDgit%E5%B9%B6%E9%9B%86%E6%88%90%E5%9C%A8idea&spm=1018.2226.3001.4187

1.7 相关问题

1.7.1 右键显示的两个选项GUI和Bash区别?

  • Open Git GUI Here:打开Git图形界面
  • Open Git Bash Here:打开Git命令行(推荐使用)

1.7.2 谈谈对git暂存区的理解?

暂存区是一个临时保存修改文件的地方,里面保存了下一次要提交到本地仓库的代码。

二、nginx反向代理机制

2.1 介绍

优势:

  • 提高访问速度
  • 进行负载均衡
  • 保证后端服务器的安全

常用配置方式:

负载均衡配置方式:

负载均衡配置策略:

2.2 使用批处理文件.bat执行nginx

好处:桌面双击启动nginx,关闭命令行窗口时,同时关闭运行的nginx。当多个nginx运行的时候就尤为方便。

操作步骤:

1.桌面新建nginx.txt

2.在文本中写入

3.修改文件后缀为.bat

4.双击运行

三、Knife4j

3.1 介绍

3.2 使用方式

访问:http://localhost:8080/doc.html

通过Swagger可以生成接口文档,我们是否就不需要Yapi了?
在这里插入图片描述

3.3 Knife4j常用注解

通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:

3.4 界面使用

3.4.1 添加全局参数设置

每次测试接口都需要获取token令牌,为了方便测试,使用全局参数设置。

四、Redis

4.1 Redis入门

Redis是一个基于内存key-value结构数据库。

  • 基于内存存储,读写性能高
  • 适合存储热点数据(热点商品、咨询、新闻等短时间访问量大的数据)
  • 企业应用广泛

安装Redis

1.Redis的windows版本属于绿色版本,解压就可以直接使用了

2.测试Redis基础命令:当前文件下打开cmd命令窗口

服务端:

redis-server.exe redis.windows.conf(根据配置文件启动)

ctrl+c(停止服务)

访问服务:

redis-cli.exe(默认端口启动)

redis-cli.exe -h localhost -p 6379(自定义访问地址和端口号)

redis-cli.exe -h localhost -p 6379 -a 密码(根据密码访问)

exit(停止访问)

修改配置文件密码:注意你启动使用的配置文件

使用自定义密码替换掉foobared,并删除#(取消注释)

4.2 Redis数据类型

4.3 Redis常用命令


注:keys (匹配所有)、keys set(匹配以set开头的key)

4.4 在Java中操作Redis

4.4.1 Redis的java客户端

4.4.2 Spring Data Redis使用方式

测试使用:

获取不同操作对象,用于操作不同类型数据

操作字符串类型数据string

操作哈希类型数据hash

操作列表数据类型list

操作集合类型数据set

操作有序集合zset

通用命令

五、HttpClient

5.1 介绍

5.2 核心API

已经通过aliyun依赖传递进来,可以直接注入使用

5.3 请求测试

package com.sky;

import com.alibaba.fastjson.JSONObject;
import com.google.gson.JsonObject;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;


@SpringBootTest
public class HttpClientTest {


    @Test
    public void testGet() throws Exception{
        //创建httpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        //创建HttpGet请求
        HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
        //执行HttpGet请求,并拿到返回体
        CloseableHttpResponse res = httpClient.execute(httpGet);
        //获取后端返回的状态码
        int statusCode = res.getStatusLine().getStatusCode();
        System.out.println("后端返回的状态码为:"+statusCode);

        //获取后端返回的数据
        HttpEntity entity = res.getEntity();
        String body = EntityUtils.toString(entity);
        System.out.println(body);

        //关闭资源
        res.close();
        httpClient.close();
    }

    @Test
    public void testPost() throws Exception{
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

        //创建json对象
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("username","admin");
        jsonObject.put("password","123456");
        //将json对象转成entity实体
        StringEntity stringEntity = new StringEntity(jsonObject.toString());
        //指定请求编码方式和数据格式
        stringEntity.setContentEncoding("utf-8");
        stringEntity.setContentType("application/json");
        //设置请求体
        httpPost.setEntity(stringEntity);
        //发送请求
        CloseableHttpResponse response = httpClient.execute(httpPost);

        //解析返回结果
        System.out.println("响应码为:"+response.getStatusLine().getStatusCode());
        HttpEntity entity = response.getEntity();
        System.out.println("请求数据为:"+EntityUtils.toString(entity));
    }

}

六、Wechat-小程序开发

6.1 介绍

https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zhCN

6.2 入门案例

操作步骤:

  • 了解小程序目录结构
  • 编写小程序代码
  • 编译小程序

小程序页面文件组成:前两个必须,后两个可选

主要是看,暂时不写。

七、Spring Cache

7.1 介绍

7.2 快速入门

1.导入maven坐标

2.配置数据源以及redis

3.具体使用

例一:

@CachePut(cacheNames=“userCache”,key=“abc”),这里的key里面写spring EL语言动态获取数据,简单点来说"#参数对象.属性名"就可以拿到对应属性值了。

如果使用Spring Cache缓存数据,key=userCache::abc。

多种方式获取:

向缓存中插入数据

示例二:通过测试发现,如果在缓存中查询到该数据,直接返回数据,方法不会被调用;如果缓存中不存在数据,那么通过查询获取后保存一份数据到缓存中。spring cache底层也是基于代理技术实现的。

示例三:根据key删除缓存中的一条数据

示例四:批量删除缓存中的数据

八、Spring Task

8.1 外卖业务

上诉两个业务中出现如下需求

  • 用户提交订单后,迟迟不付款(订单超时),我们需要一定时间后进行订单取消
  • 派送中的订单一致不点击完成该如何处理?我们需要在固定时间点自动完成(比如说每天凌晨对昨天处于派送中的订单进行自动完成)

8.2 介绍

定位:定时任务框架

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

应用场景:

8.3 cron表达式

例如:(注意周和日一般不同时出现)

cron在线生成器:(一般很少手写)

https://cron.qqe2.com/

8.4 入门案例

1.导入maven坐标(已存在)

2.启动类添加注解

3.新建task包创建定时任务类(类上加Component注解,方法上加上Scheduled注解)

九、WebSocket

9.1 外卖业务

实现如下两个业务

  • 来单提醒
  • 催单

9.2 介绍

术语解释:

  1. 长短连接:指的是生命周期的长短
  2. 请求响应模式:指的是只有客户端发起请求,服务端才会向客户端响应,请求结束,连接结束。
  3. 双向通信:第一次发起请求,响应后连接建立,只有主动结束或者其它方式结束才会断开连接。

使用场景:

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

9.3 入门案例

实现步骤:

maven坐标:

导入服务组件

package com.sky.websocket;

import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * 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();
            }
        }
    }

}

导入配置类,注册服务组件

package com.sky.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

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

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

}

十、Wechat-微信支付

10.1 介绍

10.2 入门程序

https://www.yuque.com/yotus/holgqk/hmyyw6sbhxdbyf9a#BxbIU

十一、Apache Echarts

11.1 介绍

https://echarts.apache.org/handbook/zh/get-started/

常见图表

十二、Apache POI

12.1 介绍

官网地址:https://poi.apache.org/apidocs/5.0/

应用场景

12.2 入门案例

导入Apache POI 的maven坐标

在测试包新建TestPOI类进行测试

package com.sky;

import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 使用POI操作Excel文件
 */
public class TestPOI {

    /**
     * 需求,使用POI在Excel文件中写入两列测试数据
     */
    @Test
    void write() throws Exception {
        //准备数据
        HashMap<String, Integer> map = new HashMap<>();
        map.put("张三",6000);
        map.put("李四",7000);
        map.put("王五",8000);

        //在内存中创建一个Excel文件
        XSSFWorkbook excel = new XSSFWorkbook();
        //在excel文件中创建一个sheet页
        XSSFSheet sheet = excel.createSheet("info");
        //在sheet页中创建一个行,第一行下标为0
        XSSFRow row = sheet.createRow(0);
        //在第一行添加第一个单元格,第一格下标为0,并赋值
        row.createCell(0).setCellValue("员工工资表");

        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        int[] index = {1,0}; // 使用数组对表格计数
        for (Map.Entry<String, Integer> entry : entries) {
            XSSFRow tabelRow = sheet.createRow(index[0]);
            tabelRow.createCell(index[1]).setCellValue(entry.getKey());
            index[1]++;
            tabelRow.createCell(index[1]).setCellValue(entry.getValue());
            index[1] = 0;
            index[0]++;
        }

        //通过输出流将内存中的excel写入文件中
        FileOutputStream fileOutputStream = new FileOutputStream("D:\\Code\\HDD\\info.xlsx");
        excel.write(fileOutputStream);

        //关闭文件资源
        excel.close();
        fileOutputStream.close();

    }


    /**
     * 通过POI读取excel中的内容,并打印
     */
    @Test
    public void read() throws Exception {
        FileInputStream fileInputStream = new FileInputStream("D:\\Code\\HDD\\info.xlsx");
        //读取磁盘上的excel文件
        XSSFWorkbook excel = new XSSFWorkbook(fileInputStream);
        //读取excel文件中的第一个sheet页
        XSSFSheet sheet = excel.getSheetAt(0);
        //读取sheet页中第一行第一列
        XSSFCell cell = sheet.getRow(0).getCell(0);
        System.out.println(cell);

        //读取循环读取table数据
        //获取最后一行的行号
        int lastRowNum = sheet.getLastRowNum();
        //循环打印输出
        for (int i = 1; i <= lastRowNum; i++) {
            XSSFRow row = sheet.getRow(i);
            int lastCellNum = row.getLastCellNum();
            for (int j = 0; j < lastCellNum; j++) {
                System.out.print(row.getCell(j).toString() + " ");
            }
            System.out.println();
        }

        //关闭文件资源
        excel.close();
        fileInputStream.close();
    }
}

十三、后端管理接口功能点

13.1 将员工表中的的密码进行加密存储

  • 将密码加密后存储,提高安全性
  • 加密方式:MD5

注:通过MD5加密只能使一个单项过程,不能反向算出密码,验证也只能将需要对比的字符串加密后进行比对。

使用:springframework已提供该加密工具类

  • import org.springframework.util.DigestUtils
  • password = DigestUtils.md5DigestAsHex(password.getBytes());

13.2 前后端分离开发

流程图

接口文档导入:apifox

13.3 员工新增

13.3.1 需求分析和设计

  • 产品原型分析出字段
  • 根据实际情况给字段加限制
  • 最后进行接口设计

13.3.2 代码开发

  • 创建数据库表
  • 创建对应的实体类ENTRY
  • 根据新增员工的接口设计对应的DTO
  • 没有数据响应体,不进行创建VO
  • 开发业务功能
    1. 属性拷贝:BeanUtils.copyProperties(DTO,VO)
    2. 补全其它基础字段:(下面是一些特殊字段的的处理)

默认密码可以设置为常量类,通过获取变量拿到,加密后补齐

状态可以设置为常量类,通过获取拿到,尽量不要在业务代码中写死一个字符串,一定要抽离出来。

  • 通过mybatis框架修改数据库

13.3.3 功能测试

  • 接口文档测试
  • 前后端联调测试

13.3.4 代码完善

存在的问题:

  • 录入的用户名已存在,抛出异常后没有处理
  • 新增员工的时候,创建id和修改人id设置为固定值

问题处理:

- 使用全局异常处理器捕获该异常
- 在拦截器中获取用户id,通过线程局部变量ThreadLocal将数据携带service层。一个请求一般是一个单独的线程进行处理直到结束。

知识点:

  1. Threadlocal为每个线程单独提供一份单独的空间,具有线程隔离效果,只有在线程内部才能获取到对应的值,线程外面则不能访问。
  2. 获取当前线程的id:Thread.currentThread().getId();
  3. ThreadLocal常用方法:

13.4 员工分页查询

业务规则:

根据页码展示员工信息

每页展示10条数据

分页查询时可以根据需要,输入员工姓名进行查询

13.4.1 需求分析和设计

  • 接口设计:请求方式+参数

13.4.2 代码开发

  • 根据分页查询接口设计对应的DTO
  • 所有的分页查询结果统一封装成PageResult对象
  • 开发业务功能

使用mybatis提供的分页插件PageHelper,调用方法设置当前页,以及每页展示的数据条数。

将查询的数据强转为Page类型,通过方法获取其中的总记录数、分页查询结果。

13.4.3 功能测试

  • 接口文档测试
  • 前后端联调测试

13.4.4 代码完善

存在的问题:

  • 前端页面展示的时间格式非理想状态

问题处理:

  • 方案一:在属性上加上@LocalDateFormat
  • 方案二:在WebMvcConfiguration中扩展Spring MVC消息转换器,对日期类型进行统一处理。

13.5 启用禁用员工账户

业务规则:

13.5.1 需求分析和设计

  • 接口设计:参数:路径+查询参数

13.5.2 代码开发

  • 接收路径参数status,和查询参数id

请求方式通过接口文档定义的使用post,而不是平常习惯使用put!!!

  • 业务代码开发

数据持久层传参使用对象传入,少用字段传入。

传统:新建对象,set方法设置,传参更新

新:使用builder注解链式补全字段,传参更新

数据持久层:使用动态sql进行全部字段的更新,方便复用!!!

13.6 编辑员工

13.6.1 需求分析和设计

  • 接口设计

13.6.2 代码开发

业务开发:

根据id查询员工信息:注意查出数据中,密码的保密!

修改员工信息:补全更新时间和更新用户id

13.7 导入分类管理的代码

业务规则:

13.6.1 接口设计

13.7 公共字段填充

13.7.1 问题分析

常用公共字段

13.7.2 实现思路


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

13.7.3 代码开发

  • 自定义注解
  • 定义切面类
  • 定义切入点表达式
  • 定义通知

13.8 菜品新增

业务规则:

菜品必须唯一

菜品必须属于某个分类下,不能单独存在

新增菜品的时候可以根据情况选择菜品的口味

每个菜品必须对应一张图片

13.8.1 需求分析和设计

接口设计:

  • 根据类型查询分类(可以复用员工分页查询代码)
  • 文件上传
  • 新增菜品

数据库设计:

  • 菜品表:和分类表是多对一,设计关联分类表的逻辑外键
  • 菜品口味表:和菜品表是多对多关系,所以一方加上逻辑外键就可以了

13.8.2 代码开发

文件上传开发

配置aliyun的四个属性注意:

- 命令规范:在java类中使用驼峰,在yml配置使用-分隔;
- 属性值:不希望直接配置到yml文件中,因为在开发环境中和生产环境下有可能地址和账号都不同,如果忘了修改,就容易出现问题。所以我们使用引用的方式提供配置环境。通过修改yml中引用,就可以直接切换所有的环境。
  1. 创建aliyun工具类:封装上传方法
  2. 创建aliyun属性配置类:用于获取配置中的属性值
  • @Component
  • @ConfigurationProperties(prefix = “sky.alioss”)
  1. 将aliyun工具类交给IOC容器管理,同时进行对其中的成员方法进行初始化。
  2. 通过依赖注入使用aliyun工具类

新增菜品开发

业务开发:

  1. 涉及了多张表的操作,需要在方法上开启事务@Transaction
  2. 还需要开启注解方式的事务管理:在启动类上添加@EnableTransactionManagement
  3. 向菜品表插入一条数据
  • 使用动态sql进行插入
  • 添加自动填充字段注解
  1. 向口味表插入多条数据
  • 判断插入数据是否为空:注意进行判空和集合长度>0
  • 在插入前获取菜品的id,开启获取菜品插入的自增主键。
  • 使用动态sql插入,使用

13.8.3 功能测试

字段过多,并且需要查询回显,使用前后端联调更便捷。

13.9 菜品分页查询

业务规则

13.9.1 需求分析和设计

接口设计:

分页查询+联表查询

13.9.2 代码开发

联表查询注意事项

  • sql可以优先写好,查询成功,再进行拼接where动态查询
  • 注意联表查询后返回的字段需要和实体类保持一致

13.9.3 功能测试

swugger测试

前后端联调测试

13.10 删除菜品

业务规则:

13.10.1 需求分析和设计

删除菜品参数:[1,2,3…]更加灵活适应。

13.10.2 代码开发

参数接收:

使用springmvc的注解解析"1,2,3,4"字符串封装成集合(@RequestParam)

删除操作关联的表:

dish/dish_flavor/setmeal_dish(套餐与菜品的关系表)

业务逻辑:

  • 判断删除菜品是否处于起售状态,如果是,抛出异常;
  • 判断删除菜品是否关联了套餐,如果是,抛出异常;
  • 删除菜品、菜品口味;

13.10.3 功能测试

1、打断点,前后端联调

2、优化如下代码:如果遍历非常多,发出的sql就会非常多!

13.11 修改菜品

13.11.1 需求分析和设计

查询回显部分

  1. 查询菜品基本信息:内部封装菜品口味数据
  2. 展示所有菜品分类(已实现)

修改部分:

  1. 图片重新上传(已实现)
  2. 更新菜品数据:类似于新增,不过id变成必须

13.11.2 代码开发

同步更新口味表:可能是新增、可能是追加、可能是修改…->难以判断,所以采用删除原来口味,将新的口味插入数据库表。

注意更新操作标签中逗号必须加上,忘了好几次!!!!

13.11.3 功能测试

前后端联查

13.12 套餐新增

13.12.1 需求分析和设计

业务规则:

  • 套餐名称唯一
  • 套餐必须属于某个分类
  • 套餐必须包含菜品
  • 名称、分类、价格、图片为必填项
  • 添加菜品窗口需要根据分类类型来展示菜品
  • 新增的套餐默认为停售状态

接口设计:

  • 根据类型查询分类(已完成)
  • 根据分类id查询菜品
  • 图片上传(已完成)
  • 新增套餐,同时需要保存套餐和菜品的关联关系

13.21.2 代码开发

  • 在开发前完成根据分类id查询菜品。
  • 新增套餐,同时需要保存套餐和菜品的关联关系

13.12.3 功能测试

前后端联调测试

13.13 套餐分页查询

13.13.1 需求分析和设计

业务规则

  • 根据页码进行分页展示
  • 每页展示10条数据
  • 可以根据需要,按照套餐名称、分类、售卖状态进行查询

13.13.2 代码开发

注意:

在接口文档上写了查询分类名称,要注意立联表查询!!!

13.13.3 功能测试

13.14 套餐删除

13.14.1 需求分析和设计

业务规则:

  • 可以一次删除一个套餐,也可以批量删除套餐
  • 起售中的套餐不能删除

13.14.2 代码开发

13.14.3 功能测试

13.15 套餐修改

13.15.1 需求分析和设计

接口设计:该功能涉及5个接口

  • 根据id查询套餐
  • 根据类型查询分类(已完成)
  • 根据分类id查询菜品(已完成)
  • 图片上传(已完成)
  • 修改套餐

13.15.2 代码开发

  • 可以在业务层进行数据封装
  • 也可以在数据持久层xml文件中进行数据封装

13.14.3 功能测试

13.14 套餐起售停售

13.14.1 需求分析和设计

业务规则:

  • 可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作
  • 起售的套餐可以展示在用户端,停售的套餐不能展示在用户端
  • 起售套餐时,如果套餐内包含停售的菜品,则不能起售

13.14.2 代码开发

主要sql主要查询的表为dish和setmeal_dish

13.14.3 功能测试

13.14 套餐删除

13.14.1 需求分析和设计

业务规则:

  • 可以一次删除一个套餐,也可以批量删除套餐
  • 起售中的套餐不能删除

13.14.2 代码开发

13.14.3 功能测试

13.14 店铺营业状态设置-Redis

13.14.1 需求分析和设计

理论上来说,查询营业状态可以使用同一个接口。

存储方式:

13.14.2 代码开发

controller:接口返回类型,因为data不是必须的,所以不用指定Result<>,使用Result就可以了

13.14.3 功能测试

-----------------------------------------------

十三、小程序接口功能点

13.15 微信登录

13.15.1 需求分析和设计

数据库字段设计:其中phone能否拿到取决于你的微信小程序资质,如果是由个人注册,无法拿到;如果是由企业资质注册,才可以拿到。

接口设计:/user/user/login

微信登录的流程:(熟悉)

小程序通过wx.login方式获取code(临时登录凭证,使用一次后失效)

小程序获取到code后向服务器发送登录请求,参数为code;

服务器调用 auth.code2Session 接口,换取用户唯一标识 OpenID 和 会话密钥 session_key。

服务器获取到OpenID之后,自动创建关联。也就是创建jwt令牌,相应给小程序;

小程序保存jwt令牌,后续每次向服务端发起业务请求,就会在请求头中携带jwt令牌;

服务器校验jwt令牌,成功后,返回业务数据。

拦截器定义:

和管理端类似,

区别:注册拦截器前,需要排除/login、/status。

13.15.2 代码开发

流程:

1、controller:拿到小程序请求,登录校验,生成jwt令牌,响应给小程序(用户id,openid、token)

2、service:登录校验

根据传递的临时登录凭证code,调用 auth.code2Session 接口,换取用户唯一标识 OpenID。参考以下网站,包括请求参数和响应数据。

https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html

将响应结果进行解析为json对象:

com.alibaba.fastjson.JSON

JSONObject jsonObject = JSON.parseObject(res);

    String openid = jsonObject.**getString**("openid");

查看获取的openid是否存在,如果不存抛出登录失败异常

查看根据openid用户在此系统中是否存在,如果不存在则自动创建新用户(只包含openid、create_time、id)。

注意:拆功能键新用户,id自增获取必须开启,因为返回给前端的数据中包含用户id。

13.15.3 功能测试

前后端联调测试

13.16 导入商品浏览功能

13.16.1 需求分析和设计

产品原型:

展示菜品:有口味数据,展示选择规格;无口味数据,展示+

点击选择规格:展示口味数据,用户自己选择数据

展示套餐:点击套餐图片,展示当前套餐下包含的菜品信息

接口设计:

  • 查询分类接口(原型左侧)
  • 根据分类id查询菜品(口味在查询菜品的时候直接关联就可以了)
  • 根据分类id查询套餐
  • 根据套餐id查询菜品

13.16.2 代码导入

修改之前的代码,用于复用:

DishMapper:

DishMapper.xml

DishServiceImpl

13.16.3 功能测试

小程序和后端联调,在此之前最好编译以下,因为加入了很多功能的代码和文件。

13.17 缓存菜品

13.15.1 需求分析和设计

**问题说明: **

用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。

结果:系统响应慢,用户体验差。

实现思路:

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

缓存分析逻辑

  1. 每个分类下的菜品保存一份数据
  • dish_1:菜品+分类id
  • value:将集合转换为字符串

  1. 数据库中菜品数据有变更时清理缓存数据

13.15.2 代码开发

缓存获取流程

关键点:类实现序列化接口

实现java.io.Serializable接口的类启用了类的可序列化的类的相关问题:

这个类的序列化和反序列化是哪两个类型数据的转换?

  1. 序列化是将对象的状态转换为字节流的过程。这样,对象的状态就可以被保存到文件、内存、数据库或者通过网络传输。
  2. 反序列化则是相反的过程,即从字节流中恢复对象的状态。

这两个过程涉及的数据类型转换主要是**对象(Object)与字节流(Byte Stream)**之间的转换。

这个序列化操作是哪个框架或者依赖提供的?

序列化操作所依赖的框架或库,实际上这是Java语言本身的一部分,并不需要额外的框架或依赖。Java提供的<font style="color:rgb(44, 44, 54);">java.io.ObjectOutputStream</font><font style="color:rgb(44, 44, 54);">java.io.ObjectInputStream</font>类分别用于执行对象的序列化和反序列化操作。只要一个类实现了<font style="color:rgb(44, 44, 54);">java.io.Serializable</font>接口(该接口本身没有任何方法,它只是一个标记接口),这个类的对象就可以通过上述两个类来进行序列化和反序列化操作。

缓存清理

  • 菜品修改操作,清理缓存数据
  • 菜品删除操作,清理缓存数据
  • 菜品起售,清理缓存数据
  • 菜品停售,清理缓存数据
  • 菜品新增,清理缓存数据

13.15.3 功能测试

小程序和后端联调测试

问题:菜品的起售和停售没有写。

现在进行解决:页面原型要求如下

controller

service

其它不用修改。

13.18 缓存套餐

13.18.1 需求分析和设计

实现思路

13.18.2 代码开发

13.18.3 功能测试

前、小程序、后端联调测试

13.19 添加购物车

13.19.1 需求分析和设计

这些冗余字段的加入,可以提高查询速度。

冗余字段必须比较稳定,不能大量改变!!

13.19.2 代码开发

添加购物车业务逻辑

  • 判断当前加入购物车中的商品是否已经存在(根据当前操作的用户、添加的套餐或添加的菜品,以及菜品口味来判断是否是同一商品
  • 如果已经存在,只需要将存在数据数量+1
  • 如果不存在,需要插入一条购物车数据(注意需要补全基础信息再进行插入)

13.19.3 功能测试

前后端联调

13.20 查看购物车

13.13.1 需求分析和设计

  • 查询购物车表的所有信息

13.13.2 代码开发

注意这里需要传入用户id查询商品数据,而不是所有数据!!!!

13.13.3 功能测试

13.21 导入地址簿功能代码

13.21.1 需求分析和设计

接口设计:

  • 所有地址列表查询
  • 设置默认地址
  • 新增收货地址
  • 修改收货地址
  • 根据id查询收货地址(容易被忽略,用于查询回显)
  • 删除收货地址
  • 查询默认地址(容易被忽略)

13.21.2 代码开发

注意设置默认地址的逻辑:默认地址只能有一个!!

13.21.3 功能测试

小程序、后端联调测试

13.22 用户下单业务

13.22.1 需求分析和设计

  • 商品和商品数量:购物车表
  • 订单总金额:购物车商品总额+餐盒(一个菜品一个1元)+配送(同一6元)
  • 用户、收获地址、手机号:地址簿表

数据库表的设计

order_detail:将购物车的数据挪到新的表,关联orders

13.22.2 代码开发

业务逻辑

  1. 处理各种业务异常(地址簿为空,购物车数据为空)
  2. 向订单表插入一条数据
  3. 向订单明细表插入n条数据
  4. 清空购物车数据
  5. 封装VO返回数据

13.22.3 功能测试

小程序、后端调试

13.23 用户订单支付

13.23.1 微信支付的介绍

微信支付产品

https://pay.weixin.qq.com/static/product/product_index.shtml

支付产品对应的场景:

  1. 商场扫码枪扫码
  2. H5页面支付
  3. 小程序直接点击支付
  4. 商家提供二维码,微信扫一扫支付
  5. 手机应用调用app支付
  6. 刷脸支付,快捷得很

微信支付接入流程:

微信小程序支付时序图:

商户系统调用微信下单接口:

小程序调起微信支付:

13.23.2 准备工作

问题:

  1. 商户系统调用微信后台,调用过程如何保证数据安全?
  2. 微信后台推送支付结果,如何调用到商户系统?我们的系统处于局域网内部,微信后台无法访问,所以我们要获取公网ip地址。

1.在微信支付平台下载平台证书和商户私钥文件

2.获取临时域名:内网渗透工具

:::info
https://dashboard.cpolar.com/login

:::

autotoken获取:

在cmd命令中,如果不能复制:

右键点标记,选中就可以复制了

13.23.3 代码导入

微信支付相关配置:

导入相关代码:略

需要添加的操作:

1.新增注解

2.新增查询语句

13.23.4 功能测试

13.24 用户查询历史订单

13.24.1 需求分析和设计

业务规则:

  • 分页查询历史订单
  • 可以根据订单状态查询
  • 展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)

接口设计:

  • 查询当前用户的所有订单以及订单详情数据

13.24.2 代码开发

Controller:注意@RequesParam的使用,以及使用java对象接受请求参数的时候,属性和前端传入的键名字相同,可以省略。

业务层:主要是封装数据。

13.24.3 功能测试

13.25 用户查询订单详情

13.25.1 需求分析和设计

接口设计:

根据单个订单id查询订单基础信息以及订单详情数据

13.25.2 代码开发

与历史订单开发类似,不过是返回一条数据,而历史订单返回所有数据。

13.25.3 功能测试

13.26 用户取消订单

13.26.1 需求分析和设计

业务规则:

  • 待支付和待接单状态下,用户可直接取消订单
  • 商家已接单状态下,用户取消订单需电话沟通商家
  • 派送中状态下,用户取消订单需电话沟通商家
  • 如果在待接单状态下取消订单,需要给用户退款
  • 取消订单后需要将订单状态修改为“已取消”

接口设计:

根据订单号修改订单状态和保存相关数据

13.26.2 代码开发

13.26.3 功能测试

13.27 用户再来一单

13.27.1 需求分析和设计

业务规则:

  • 再来一单就是将原订单中的商品重新加入到购物车中

接口设计

  • 根据订单关联的商品加入到购物车中

13.27.2 代码开发

13.27.3 功能测试

小程序、后端联调

-----------------------------------------------

十三、后端管理端接口功能点

13.28 订单搜索

13.28.1 需求分析和设计

  1. 设计DTO OrdersPageQueryDTO
  2. 分页查询订单数据
  3. 遍历分页查询结果,将结果转换为OrderVO 注意 OrderVO 的orderDishes字段需要做字符串拼接
  4. 返回结果

13.28.2 代码开发

业务层:

订单详情只需要拼接菜品名*数量字符串即可

订单详情无需全部返回

13.28.3 功能测试

前后端联调

13.29 订单模块其它接口

13.29.1 需求分析和设计

1.各个状态的订单数量统计

- 根据状态,分别查询出待接单、待派送、派送中的订单数量
- 将查询出的数据封装到orderStatisticsVO中响应

2.复用用户端【查询订单详情】接口

3.接单

- 修改订单状态为CONFIRMED即可

4.拒单

- 根据id查询订单
- 逻辑判断 订单只有存在且状态为2(待接单)才可以拒单
- 退款 (可以从答案中粘贴过来这部分代码,测试的时候可以注释掉)
- 根据订单id更新订单状态、拒单原因、取消时间

5.取消订单

- 根据id查询订单
- 退款(可以从答案中粘贴过来这部分代码,测试的时候可以注释掉)
- 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间

6.派送订单

- 根据id查询订单
- 判断状态是否为CONFIRMED状态
- 更新订单状态,状态转为派送中

7.完成订单

- 根据id查询订单
- 判断状态是否为DELIVERY_IN_PROGRESS(派送中)
- 更新订单状态,状态转为完成

13.29.2 代码开发

业务层注意事项:

1.各个状态的订单数量统计

- 根据状态查询订单数量
- 封装数据返回

2.复用用户端【查询订单详情】接口

3.接单

- 修改订单状态为CONFIRMED即可

4.拒单

- 根据id查询订单
- 逻辑判断 订单只有存在且状态为2(待接单)才可以拒单
- 退款 (可以从答案中粘贴过来这部分代码,测试的时候可以注释掉)
- 根据订单id更新订单状态、拒单原因、取消时间	

5.取消订单

- 根据id查询订单
- 退款(可以从答案中粘贴过来这部分代码,测试的时候可以注释掉)
- 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间

6.派送订单

- 根据id查询订单
- 判断状态是否为CONFIRMED状态
- 更新订单状态,状态转为派送中

7.完成订单

- 根据id查询订单
- 判断状态是否为DELIVERY_IN_PROGRESS(派送中)
- 更新订单状态,状态转为完成

13.29.3 功能测试

前后端联调测试

13.30 订单状态定时处理

13.30.1 需求分析和设计

后台处理:

13.30.2 代码开发

创建定时任务包,写定时任务方法

package com.sky.task;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.time.LocalDateTime;
import java.util.List;


/**
 * 订单模块定时任务
 */
@Component
@Slf4j
public class OrderTask {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 处理订单超时的订单
     */
    @Scheduled(cron = "0 * * * * *")
    public void processTimeOutOrder(){

        log.info("处理超时订单:{}", LocalDateTime.now());

        /* 获取当前时间-15min */
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);

        /* 查询:select * from orders where status = ? and order_time < (当前时间-15min) */
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT,time);

        /* 更新订单状态、取消原因、取消时间 */
        if(!CollectionUtils.isEmpty(ordersList)){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.CANCELLED);
                orders.setCancelReason("订单超时,自动取消");
                orders.setCancelTime(LocalDateTime.now());

                orderMapper.update(orders);
            }
        }
    }

    /**
     * 处理一直处于派送中的订单
     */
    @Scheduled(cron = "* * 1 * * ? ")
    public void processDeliveryOrder(){
        log.info("处理一直处于派送中的订单:{}",LocalDateTime.now());

        /* 获取当前时间(凌晨一点)-1h */
        LocalDateTime time = LocalDateTime.now().plusHours(-1);

        /* 查询:select * from orders where status = ? and order_time < (当前时间-1h) */
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);

        /* 更新订单状态为完成 */
        if(!CollectionUtils.isEmpty(ordersList)){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }
    }

}

13.30.3 功能测试

后台测试

13.30 来单提醒和催单提醒

13.30.1 需求分析和设计

菜单提醒

催单提醒:(发起是通过客户点击催单触发的)

接口:

  • 客户催单

13.30.2 代码开发

来单提醒:

1.依赖注入WebSocketServe对象

2.在支付成功后,调用方法向后台管理系统发送信息

客户催单:(新建接口)

通过订单id查询数据库订单是否存在

如果存在,给后台管理系统发送催单提醒

13.30.3 功能测试

13.31 数据统计

13.31.1 需求分析和设计

  1. 营业额统计

业务规则:

  1. 用户统计

注:每天用户总量=在此之前的所有新增用户。

  1. 订单统计

业务规则:

  • 订单完成率:当前时间段,完成订单数/所有订单数
  • 订单数列表:每天订单的数量
  • 订单总数:当前时间段,订单总数
  • 有效订单数:当前时间段,完成订单数量
  • 有效订单列表:每天完成订单的数量
  1. 销量排名Top10

产品原型

业务规则:

接口设计:

13.31.2 代码开发

  1. 营业额统计

根据接口设计对应的VO:

@Override
public TurnoverReportVO turnoverStatistics(LocalDate begin, LocalDate end) {
    //获取开始和结束的日期列表
    List<LocalDate> dateList = new ArrayList<>();
    dateList.add(begin);
    while (!begin.equals(end)) {
        //将当前日期新增一天,加入列表集合
        begin = begin.plusDays(1);
        dateList.add(begin);
    }
    // 通过日期列表查询每一天的营业额,并封装到一个列表中
    List<Double> turnovers = new ArrayList<>();
    for (LocalDate localDate : dateList) {
        //获取当天开始时刻,和当天最晚时刻
        LocalDateTime beginTime = LocalDateTime.of(localDate, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(localDate, LocalTime.MAX);
        HashMap<String, Object> map = new HashMap<>();
        map.put("begin", beginTime);
        map.put("end", endTime);
        map.put("status", Orders.COMPLETED);
        // select sum(amount) from orders where order_time > ? and order_time < ? and status = 5;
        Double turnover = orderMapper.sumByMap(map);
        turnover = turnover == null ? 0.0 : turnover;
        turnovers.add(turnover);
    }

    //根据数据响应格式,将对象转换成字符串
    String dateStr = StringUtils.join(dateList, ",");
    String turnoversStr = StringUtils.join(turnovers, ",");


    //封装数据返回给前端
    TurnoverReportVO turnoverReportVO = TurnoverReportVO.builder()
            .dateList(dateStr)
            .turnoverList(turnoversStr)
            .build();

    return turnoverReportVO;
}
  1. 用户统计

@Override
public UserReportVO userStatistics(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> userNumList = new ArrayList<>();
    List<Integer> userTotalList = new ArrayList<>();
    for (LocalDate localDate : dateList) {
        //获取当前开始时间和结束时间
        LocalDateTime beginDateTime = LocalDateTime.of(localDate, LocalTime.MIN);
        LocalDateTime endDateTime = LocalDateTime.of(localDate, LocalTime.MAX);

        //查询在此之前所有的用户(没有开始时间)
        Integer userTotal = userMapper.getByDate(null,endDateTime);
        userTotalList.add(userTotal);

        //查询当前新增的用户数量
        Integer userNum = userMapper.getByDate(beginDateTime, endDateTime);
        userNumList.add(userNum);
    }
    //将三个集合分别转换成字符串,封装到UserReportVO,返回给前端
    UserReportVO userReportVO = UserReportVO.builder()
            .dateList(StringUtils.join(dateList, ","))
            .newUserList(StringUtils.join(userNumList, ","))
            .totalUserList(StringUtils.join(userTotalList, ","))
            .build();
    return userReportVO;
}
  1. 订单统计

public OrderReportVO orderStatistics(LocalDate begin, LocalDate end) {
        //获取开始日期和结束日期之间的日期列表
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);
        while (!begin.equals(end)) {
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        //通过开始时间和结束时间获取每天的订单总数和有效订单数(status==5)
        List<Integer> orderCountList = new ArrayList<>();//订单列表
        List<Integer> validOrderCountList = new ArrayList<>();//有效订单列表
        Integer orderCountSum = 0;//订单数量总计
        Integer validOrderCountSum = 0;//有效订单总计
        Double orderCompletionRate = 0.0;
        for (LocalDate localDate : dateList) {
            //获取对当天的开始时刻和结束时刻
            LocalDateTime beginTime = LocalDateTime.of(localDate, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(localDate, LocalTime.MAX);
            //查询当前所有订单数量以及查询当天所有的有效订单数加入到集合
            Integer orderCount = orderMapper.getByDateAndStatus(beginTime, endTime, null);
            Integer validOrderCount = orderMapper.getByDateAndStatus(beginTime, endTime, Orders.COMPLETED);
            //将查询的数据加入集合并进行累加统计
            orderCountList.add(orderCount);
//            orderCountSum += orderCount;
            validOrderCountList.add(validOrderCount);
//            validOrderCountSum += validOrderCount;
        }
        //将查询的结果进行累加
        orderCountSum = orderCountList.stream().reduce(Integer::sum).get();
        validOrderCountSum = validOrderCountList.stream().reduce(Integer::sum).get();

        //计算订单完成率
        if (orderCountSum != 0){
            orderCompletionRate = (validOrderCountSum*1.0)/orderCountSum;
        }

        //封装成OrderReportVO集合返回给前端
        OrderReportVO orderReportVO = OrderReportVO.builder()
                .dateList(StringUtils.join(dateList, ","))
                .orderCompletionRate(orderCompletionRate)
                .orderCountList(StringUtils.join(orderCountList, ","))
                .totalOrderCount(orderCountSum)
                .validOrderCountList(StringUtils.join(validOrderCountList, ","))
                .validOrderCount(validOrderCountSum)
                .build();
        return orderReportVO;
    }
orderCountSum = orderCountList.stream().reduce(Integer::sum).get();
//集合的流式操作√
//方法引用Integer::sum √
//reduce详解 java进阶中
//get()解释
get() 方法并不是 Stream API 中的一部分,也不是流的终结操作。
orderCountList.stream().reduce(Integer::sum).get()中,
.reduce(Integer::sum) 返回的是一个 Optional<Integer> 对象,
因为 reduce 操作在流为空时不能提供一个结果值,
所以它使用 Optional 来包裹可能的结果。

  1. 销量排名Top10

    @Override
    public SalesTop10ReportVO Top10(LocalDate begin, LocalDate end) {
        //获取开始日期的开始时刻和结束日期的结束时刻
        LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);

//        //根据开始时间和结束时间以及状态查询orders和order_detail,返回商品名称和商品数量
//        List<Map<String,Integer>> mapList = orderMapper.getTop10Count(beginTime,endTime,Orders.COMPLETED);
//
//        //从List<Map>中取出商品品名称和销量,分别封装到列表中
        List<String> nameList = new ArrayList<>();
        List<Integer> numberList = new ArrayList<>();
//        for (Map<String, Integer> map : mapList) {
//            nameList.add(String.valueOf(map.get("name")));
//            numberList.add(String.valueOf(map.get("nameNum")));
//        }

        List<GoodsSalesDTO> goodsSalesDTOList = orderMapper.getTop10Count(beginTime, endTime, Orders.COMPLETED);
        for (GoodsSalesDTO goodsSalesDTO : goodsSalesDTOList) {
            nameList.add(goodsSalesDTO.getName());
            numberList.add(goodsSalesDTO.getNumber());
        }

        //将数据封装成json字符串返回给前端
        SalesTop10ReportVO salesTop10ReportVO = SalesTop10ReportVO.builder()
                .nameList(StringUtils.join(nameList, ","))
                .numberList(StringUtils.join(numberList, ","))
                .build();
        return salesTop10ReportVO;
    }
<select id="getTop10Count" resultType="com.sky.dto.GoodsSalesDTO">
    select order_detail.name name,sum(order_detail.number) number from orders left join order_detail on orders.id = order_detail.order_id
    <where>
        <if test="beginTime != null">and order_time &gt; #{beginTime}</if>
        <if test="endTime != null">and order_time &lt; #{endTime}</if>
        <if test="status != null">and status = #{status}</if>
    </where>
    group by order_detail.name
    order by number desc
</select>

13.31.3 功能测试

  1. 营业额统计
  2. 用户统计
  3. 订单统计
  4. 销量排名Top10

13.32 工作台

13.32.1 需求分析和设计

页面原型

接口设计:

  1. 今日数据

  1. 订单管理

  1. 菜品总览

  1. 套餐总览

  1. 订单信息

已存在

13.32.2 代码开发

直接导入,复用之前的接口

13.32.3 功能测试

13.33 导出运营数据Excel报表

13.33.1 需求分析和设计

产品原型

导出的excel格式

接口设计:

请求方式:Get

请求参数:导出最近30天数据,后台计算就可以了,所以不需要任何参数

返回数据:本质上是后台通过输出流向客户端浏览器写出文件

13.33.2 代码开发

  1. 设计Excel模版文件(包含excel样式)

  1. 查询近30天的运营数据
  2. 将查询的运营数据写入模版文件(对应的位置填充数据)
  3. 通过输出流将excel文件下载到客户端浏览器
/**
 * 导出运营数据报表
 * @param response
 */
@GetMapping("export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response) {//没有入参,但是需要获取一个浏览器输出流对象
      reportService.exportBusinessData(response);
}
@Override
    public void exportBusinessData(HttpServletResponse response){
    //获取开始日期和结束日期(昨天为截止时间)
    LocalDate endDate = LocalDate.now().minusDays(1);//获取昨天的日期
    LocalDate beginDate = LocalDate.now().minusDays(30);//获取30天之前的前一天的日期

    //查询总数据
    LocalDateTime beginTime = LocalDateTime.of(beginDate,LocalTime.MIN);
    LocalDateTime endTime = LocalDateTime.of(endDate,LocalTime.MAX);
    BusinessDataVO businessDataLast30 = workspaceService.getBusinessData(beginTime, endTime);

    // 查询明细数据
    List<LocalDate> dateList = new ArrayList<>();//日期列表
    List<BusinessDataVO> businessDataDetailList = new ArrayList<>();//运营数据明细表
    //由于已经确定是30填了,所以直接使用for循环30次,不用感觉很low,合适才是一切。
    for (int i = 0; i < 30; i++) {
        //将当前日期加入列表
        dateList.add(beginDate);
        //获取当天开始时刻和结束时刻
        LocalDateTime begin = LocalDateTime.of(beginDate,LocalTime.MIN);
        LocalDateTime end = LocalDateTime.of(beginDate,LocalTime.MAX);
        //将当天的运营数据加入明细表
        businessDataDetailList.add(workspaceService.getBusinessData(begin, end));
        beginDate = beginDate.plusDays(1);
    }

    try {
        //创建文件输入流:文件资源->内存(java程序中)
        InputStream in = this.getClass()
                .getClassLoader()
                .getResourceAsStream("template/businessDataReportTemplate.xlsx");
        XSSFWorkbook excel = new XSSFWorkbook(in);
        XSSFSheet sheet = excel.getSheet("Sheet1");


        insertToExcel(sheet, beginDate, endDate, businessDataLast30, dateList, businessDataDetailList);

        //通过输出流将excel文件下载到客户端浏览器
        ServletOutputStream out = response.getOutputStream();
        excel.write(out);

        //关闭资源
        in.close();
        excel.close();
        out.close();
    } catch (IOException e) {
        System.out.println(e);
    }
}

private static void insertToExcel(XSSFSheet sheet, LocalDate beginDate, LocalDate endDate, BusinessDataVO businessDataLast30, List<LocalDate> dateList, List<BusinessDataVO> businessDataDetailList) {
    //填充第二行
    sheet.getRow(1).getCell(1).setCellValue("时间:"+ beginDate +"至"+ endDate);
    //填充第四行
    XSSFRow row = sheet.getRow(3);
    row.getCell(2).setCellValue(businessDataLast30.getTurnover());
    row.getCell(4).setCellValue(businessDataLast30.getOrderCompletionRate());
    row.getCell(6).setCellValue(businessDataLast30.getNewUsers());
    //填充第五行
    row = sheet.getRow(4);
    row.getCell(2).setCellValue(businessDataLast30.getValidOrderCount());
    row.getCell(4).setCellValue(businessDataLast30.getUnitPrice());

    //填充明细表
    int[] index = {7,1};
    for (int i = 0; i < dateList.size(); i++) {
        row = sheet.getRow(index[0]);
        row.getCell(index[1]++).setCellValue(String.valueOf(dateList.get(i)));
        row.getCell(index[1]++).setCellValue(String.valueOf(businessDataDetailList.get(i).getTurnover()));
        row.getCell(index[1]++).setCellValue(String.valueOf(businessDataDetailList.get(i).getValidOrderCount()));
        row.getCell(index[1]++).setCellValue(String.valueOf(businessDataDetailList.get(i).getOrderCompletionRate()));
        row.getCell(index[1]++).setCellValue(String.valueOf(businessDataDetailList.get(i).getUnitPrice()));
        row.getCell(index[1]).setCellValue(String.valueOf(businessDataDetailList.get(i).getNewUsers()));
        index[0]++;
        index[1] = 1;
    }
}

13.34.3 功能测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值