文章目录
- 开源项目-sky-take-out
- 一、git
- 二、nginx反向代理机制
- 三、Knife4j
- 四、Redis
- 五、HttpClient
- 六、Wechat-小程序开发
- 七、Spring Cache
- 八、Spring Task
- 九、WebSocket
- 十、Wechat-微信支付
- 十一、Apache Echarts
- 十二、Apache POI
- 十三、后端管理接口功能点
- -----------------------------------------------
- 十三、小程序接口功能点
- -----------------------------------------------
- 十三、后端管理端接口功能点
开源项目-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
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在线生成器:(一般很少手写)
8.4 入门案例
1.导入maven坐标(已存在)
2.启动类添加注解
3.新建task包创建定时任务类(类上加Component注解,方法上加上Scheduled注解)
九、WebSocket
9.1 外卖业务
实现如下两个业务
- 来单提醒
- 催单
9.2 介绍
术语解释:
- 长短连接:指的是生命周期的长短
- 请求响应模式:指的是只有客户端发起请求,服务端才会向客户端响应,请求结束,连接结束。
- 双向通信:第一次发起请求,响应后连接建立,只有主动结束或者其它方式结束才会断开连接。
使用场景:
- 视频弹幕
- 网页聊天
- 体育实况更新
- 股票寂静报价实时更新
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
- 开发业务功能
- 属性拷贝:BeanUtils.copyProperties(DTO,VO)
- 补全其它基础字段:(下面是一些特殊字段的的处理)
默认密码可以设置为常量类,通过获取变量拿到,加密后补齐
状态可以设置为常量类,通过获取拿到,尽量不要在业务代码中写死一个字符串,一定要抽离出来。
- 通过mybatis框架修改数据库
13.3.3 功能测试
- 接口文档测试
- 前后端联调测试
13.3.4 代码完善
存在的问题:
- 录入的用户名已存在,抛出异常后没有处理
- 新增员工的时候,创建id和修改人id设置为固定值
问题处理:
- 使用全局异常处理器捕获该异常
- 在拦截器中获取用户id,通过线程局部变量ThreadLocal将数据携带service层。一个请求一般是一个单独的线程进行处理直到结束。
知识点:
- Threadlocal为每个线程单独提供一份单独的空间,具有线程隔离效果,只有在线程内部才能获取到对应的值,线程外面则不能访问。
- 获取当前线程的id:Thread.currentThread().getId();
- 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中引用,就可以直接切换所有的环境。
- 创建aliyun工具类:封装上传方法
- 创建aliyun属性配置类:用于获取配置中的属性值
- @Component
- @ConfigurationProperties(prefix = “sky.alioss”)
- 将aliyun工具类交给IOC容器管理,同时进行对其中的成员方法进行初始化。
- 通过依赖注入使用aliyun工具类
新增菜品开发
业务开发:
- 涉及了多张表的操作,需要在方法上开启事务@Transaction
- 还需要开启注解方式的事务管理:在启动类上添加@EnableTransactionManagement
- 向菜品表插入一条数据
- 使用动态sql进行插入
- 添加自动填充字段注解
- 向口味表插入多条数据
- 判断插入数据是否为空:注意进行判空和集合长度>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 需求分析和设计
查询回显部分
- 查询菜品基本信息:内部封装菜品口味数据
- 展示所有菜品分类(已实现)
修改部分:
- 图片重新上传(已实现)
- 更新菜品数据:类似于新增,不过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来缓存菜品数据,减少数据库查询操作。
缓存分析逻辑
- 每个分类下的菜品保存一份数据
- dish_1:菜品+分类id
- value:将集合转换为字符串
- 数据库中菜品数据有变更时清理缓存数据
13.15.2 代码开发
缓存获取流程
关键点:类实现序列化接口
实现java.io.Serializable接口的类启用了类的可序列化的类的相关问题:
这个类的序列化和反序列化是哪两个类型数据的转换?
- 序列化是将对象的状态转换为字节流的过程。这样,对象的状态就可以被保存到文件、内存、数据库或者通过网络传输。
- 反序列化则是相反的过程,即从字节流中恢复对象的状态。
这两个过程涉及的数据类型转换主要是**对象(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 代码开发
业务逻辑
- 处理各种业务异常(地址簿为空,购物车数据为空)
- 向订单表插入一条数据
- 向订单明细表插入n条数据
- 清空购物车数据
- 封装VO返回数据
13.22.3 功能测试
小程序、后端调试
13.23 用户订单支付
13.23.1 微信支付的介绍
微信支付产品
https://pay.weixin.qq.com/static/product/product_index.shtml
支付产品对应的场景:
- 商场扫码枪扫码
- H5页面支付
- 小程序直接点击支付
- 商家提供二维码,微信扫一扫支付
- 手机应用调用app支付
- 刷脸支付,快捷得很
微信支付接入流程:
微信小程序支付时序图:
商户系统调用微信下单接口:
小程序调起微信支付:
13.23.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 需求分析和设计
- 设计DTO OrdersPageQueryDTO
- 分页查询订单数据
- 遍历分页查询结果,将结果转换为OrderVO 注意 OrderVO 的orderDishes字段需要做字符串拼接
- 返回结果
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 需求分析和设计
- 营业额统计
业务规则:
- 用户统计
注:每天用户总量=在此之前的所有新增用户。
- 订单统计
业务规则:
- 订单完成率:当前时间段,完成订单数/所有订单数
- 订单数列表:每天订单的数量
- 订单总数:当前时间段,订单总数
- 有效订单数:当前时间段,完成订单数量
- 有效订单列表:每天完成订单的数量
- 销量排名Top10
产品原型
业务规则:
接口设计:
13.31.2 代码开发
- 营业额统计
根据接口设计对应的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;
}
- 用户统计
@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;
}
- 订单统计
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 来包裹可能的结果。
- 销量排名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 > #{beginTime}</if>
<if test="endTime != null">and order_time < #{endTime}</if>
<if test="status != null">and status = #{status}</if>
</where>
group by order_detail.name
order by number desc
</select>
13.31.3 功能测试
- 营业额统计
- 用户统计
- 订单统计
- 销量排名Top10
13.32 工作台
13.32.1 需求分析和设计
页面原型
接口设计:
- 今日数据
- 订单管理
- 菜品总览
- 套餐总览
- 订单信息
已存在
13.32.2 代码开发
直接导入,复用之前的接口
13.32.3 功能测试
13.33 导出运营数据Excel报表
13.33.1 需求分析和设计
产品原型
导出的excel格式
接口设计:
请求方式:Get
请求参数:导出最近30天数据,后台计算就可以了,所以不需要任何参数
返回数据:本质上是后台通过输出流向客户端浏览器写出文件
13.33.2 代码开发
- 设计Excel模版文件(包含excel样式)
- 查询近30天的运营数据
- 将查询的运营数据写入模版文件(对应的位置填充数据)
- 通过输出流将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 功能测试
略