Part04 ☀️
Part04 ☀️
课程内容
- 菜品模块剩余的禁用和删除 ✏️
- 套餐模块和移动端代码导入 ✏️
- 优化 🍐
- Swaager人工智能插件 🍐
- Redis 🍐
- Nginx 🍐
1. 菜品启用禁用和删除(1-2) ✏️
1.1 菜品启用禁用 ✏️
菜品启用禁用
1. 点击菜品列表的中的禁用或者启用 按钮,弹出对话框进行禁用或者启用

2. 点击停售或者批量停售,观察 Network 中的请求

- 其中0表示停售,1表示起售
- ids后面接受的是菜品id
代码操作
步鄹
- 依据分析 Network 中的请求,在 DishController 中定义 updateStatus 方法
- 通过菜品 Id 获得所有的菜品(有可能是批量禁用和启用)
- 通过 for循环遍历,设置状态值
- 调用 updateBatchById 批量进行修改
- 观察控制台 sql 语句的输出
- 重启服务器,登陆,进行测试
从上述的Network图可以得出: 👇
- 使用路径参数接收status状态
- 使用
List<Long>
接收ids
DishController类
@PostMapping("/status/{status}")
public R<String> updateStatus(@PathVariable("status") Integer status,
@RequestParam("ids") List<Long> ids){
log.info("status:{},停售起售的ids:{}",status,ids);
//通过ids批量获取dish 菜品
List<Dish> dishes = dishService.listByIds(ids);
//遍历菜品,修改其状态
for (Dish dish : dishes) {
dish.setStatus(status);
}
//批量修改
boolean b = dishService.updateBatchById(dishes);
//返回结果
return b ?R.success("操作成功"):R.error("操作失败");
}
1. 操作视图

2. idea控制台sql

1.2 菜品删除 ✏️
删除
1. 点击菜品列表的中的删除 按钮,弹出对话框进行删除或者批量删除

2. 点击删除或者批量删除,观察 Network 中的请求

- ids后面接受的是菜品id
代码操作
步鄹
- 依据分析 Network 中的请求,在 DishController 中定义 delete 方法
- 通过菜品 Id 删除菜品(有可能是批量禁用和启用)
- 重启服务器,登陆,进行测试
- 观察控制台 sql 语句的输出
DishController
@DeleteMapping
public R<String> deleteByIds(@RequestParam("ids") List<Long> ids){
log.info("deleteByIds:ids:{}",ids);
boolean b = dishService.removeByIdsWithFlavors(ids);
return b?R.success("删除成功"):R.error("删除失败");
}
DishService
boolean removeByIdsWithFlavors(List<Long> ids);
DishServiceImpl
//注入套餐菜品服务,可以对中间表进行增删改查
@Autowired
SetmealDishService setmealDishService;
@Override
@Transactional
public boolean removeByIdsWithFlavors(List<Long> ids) {
//1. 先判断是否在售,如果在售不能删除
LambdaQueryWrapper<Dish> slqw = new LambdaQueryWrapper<>();
slqw.in(Dish::getId,ids);
List<Dish> list = list(slqw);
//2. 遍历集合,判断是否在售,如果在售,则不能删除,抛出异常
for (Dish dish : list) {
if (dish.getStatus()==1){
throw new CustomException("此菜品正在热卖中,,不能删除");
}
}
//3.再次判断是否在套餐中,是否是,则删除不了,如果不在可以删除?
LambdaQueryWrapper<SetmealDish> smdlqw = new LambdaQueryWrapper<>();
smdlqw.in(SetmealDish::getDishId,ids);
int count = setmealDishService.count(smdlqw);
if (count>0){
throw new CustomException("此菜品在套餐中,不能删除;");
}
//4. 先删除菜品基本信息
boolean b = removeByIds(ids);
//5. 删除菜品口味信息
LambdaUpdateWrapper<DishFlavor> luw = Wrappers.lambdaUpdate();
luw.in(DishFlavor::getDishId,ids);
boolean remove = dishFlavorService.remove(luw);
//6. 返回结果
return b&&remove;
}
1. 点击删除或者批量删除


在起售状态不能删除

套餐中包含,也不能删除,以免套餐少菜!

删除成功
2. 观察idea的控制台sql语句

总结
课堂作业
- 根据上述提示,完成菜品启用禁用和菜品删除🎤
2. 套餐模块和移动端代码导入(3-4)
套餐模块和移动端代码导入
1. 菜品和套餐业务基本一样,同学们可以用自行书写,本节课直接导入

1. 移动端的菜品和套餐展示已经写好了,只需要导入下列数据,就可以正常展示

这些文件其实只有一个类的架子,没有具体代码,需要后续完成...
代码操作
需要提前在DishController增加如下方法,新增套餐需要用到
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品的id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
//SQL:select * from dish_flavor where dish_id = ?
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
1. 套餐禁用和启用操作
2,套餐修改之回显

3,套餐修改之修改
4,套餐修改之删除
执行下列语句,往订单表中插入10条假数据
如果有假数据,可以不导入
INSERT INTO orders (id, number, status, user_id, address_book_id, order_time, checkout_time, pay_method, amount, remark, phone, address, user_name, consignee)
VALUES
-- 示例数据
(1, 'ORDER001', 1, 1001, 2001, NOW(), NOW(), 1, 100.00, '测试订单', '1234567890', '北京市海淀区', '张三', '张三'),
(2, 'ORDER002', 2, 1002, 2002, NOW(), NOW(), 2, 200.00, NULL, '9876543210', '上海市浦东新区', '李四', '李四'),
(3, 'ORDER003', 3, 1003, 2003, NOW(), NOW(), 1, 300.00, '加急', '13333333333', '广州市天河区', '王五', '王五'),
(4, 'ORDER004', 4, 1004, 2004, NOW(), NOW(), 2, 400.00, '无特殊要求', '15555555555', '深圳市南山区', '赵六', '赵六'),
(5, 'ORDER005', 5, 1005, 2005, NOW(), NOW(), 1, 500.00, '取消订单', '17777777777', '杭州市西湖区', '孙七', '孙七'),
(6, 'ORDER006', 1, 1006, 2006, NOW(), NOW(), 2, 600.00, '请尽快处理', '19999999999', '成都市武侯区', '周八', '周八'),
(7, 'ORDER007', 2, 1007, 2007, NOW(), NOW(), 1, 700.00, NULL, '16666666666', '南京市秦淮区', '吴九', '吴九'),
(8, 'ORDER008', 3, 1008, 2008, NOW(), NOW(), 2, 800.00, '请送到门口', '15888888888', '合肥市蜀山区', '郑十', '郑十'),
(9, 'ORDER009', 4, 1009, 2009, NOW(), NOW(), 1, 900.00, '', '13999999999', '郑州市金水区', '冯十一', '冯十一'),
(10, 'ORDER010', 5, 1010, 2010, NOW(), NOW(), 2, 1000.00, '谢谢', '13666666666', '武汉市江汉区', '萧十二', '萧十二');

移动端不是本次的内容,为了展示效果,需要将下列地址放到过滤器的白名单中,以防拦截
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",//后台的前端资源css js html
"/front/**",//移动端的前端资源css js html
"/category/**",
"/dish/**",
"/setmeal/**",
"/common/**"
};

效果如下:
图片不显示是因为图片错误,可以在管理端重新上传即可
总结
课堂作业
- 根据上述提示,完成代码导入,并且观察效果,理解项目业务需求🎤
3. 优化项目(5-6)
3.1 Swagger 🍐
Swagger
1️⃣ 介绍
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(https://swagger.io/)。

它的主要作用是:
- 使得前后端分离开发更加方便,有利于团队协作
- 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
- 功能测试
Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!
目前,一般都使用knife4j框架。
2️⃣ 使用步骤 👇
导入 knife4j 的maven坐标
在pom.xml中添加依赖
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency>

在配置类中加入 knife4j 相关配置并且在类上添加2个注解
WebMvcConfig.java
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Bean
public Docket docket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("内蒙古电子信息外卖项目接口文档")
.version("1.0")
.description("内蒙古电子信息外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}

设置静态资源映射添加2条,注意代码阴影部分,否则接口文档页面无法访问
WebMvcConfig.java
/** * 设置静态资源映射 * @param registry */ @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { log.info("开始进行静态资源映射..."); registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/"); registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/"); registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); }
往过滤器中,在原有的不需要处理的请求路径中,再增加如下链接:
"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"

3️⃣ 常用注解 👇
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
注解 | 说明 |
---|---|
@Api | 用在类上,例如Controller,表示对类的说明 |
@ApiModel | 用在类上,例如entity、DTO、VO |
@ApiModelProperty | 用在属性上,描述属性信息 |
@ApiOperation | 用在方法上,例如Controller的方法,说明方法的用途、作用 |
接下来,使用上述注解,生成可读性更好的接口文档
@RestController
@RequestMapping("/category")
@Slf4j
@Api(tags = "分类管理")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 新增分类
* @param category
* @return
*/
@PostMapping
@ApiOperation("新增分类")
public R<String> save(@RequestBody Category category){
log.info("category:{}",category);
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
category.setCreateUser(1L);
category.setUpdateUser(1L);
categoryService.save(category);
return R.success("新增分类成功");
}
启动服务:访问http://localhost:8080/doc.html

书写这些是不是很繁琐呀!!,可以使用AI插件哦!!!
3.2 Redis操作
SpringCache和Redis操作
前面我们已经实现了移动端菜品查看功能,对应的服务端方法为 DishController 的 list 方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发 的情况下,频繁查询数据库会导致系统性能下降 ,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。(如下图)

SpringCache是一个框架,实现了基于注解的缓存功能 ,只需要简单地加一个注解,就能实现缓存功能,大大简化我们在业务中操作缓存的代码。
Spring Cache 只是提供了一层抽象,底层可以切换不同的 cache 实现。具体就是通过CacheManager 接口 来统一不同的缓存技术。CacheManager 是 Spring 提供的各种缓存技术抽象接口。
针对不同的缓存技术需要实现不同的 CacheManager:
CacheManager | 描述 |
---|---|
EhCacheCacheManager | 使用 EhCache 作为缓存技术 |
GuavaCacheManager | 使用 Google 的 GuavaCache 作为缓存技术 |
RedisCacheManager | 使用 Redis 作为缓存技术 |
在 SpringCache 中提供了很多缓存操作的注解,常见的是以下的几个:
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能 |
@Cacheable | 在方法执行前 spring 先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
在 spring boot 项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching
开启缓存支持即可。
例如,使用 Redis 作为缓存技术,只需要导入 Spring data Redis 的 maven 坐标即可。
代码操作
由于 SpringCache 的基本功能是 Spring 核心(spring-context)中提供的,所以目前我们进行简单的 SpringCache 测试,是可以不用额外引入其他依赖的。 可以检查是否导入了下列依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
1). 引导类上加@EnableCaching
在引导类上加该注解,就代表当前项目开启缓存注解功能。
在 DishController 的 list 方法上加入@Cacheable
注解
在进行数据查询时,我们需要根据分类 ID 和菜品的状态进行查询,所以我们在缓存数据时,可以将菜品分类 ID 和菜品状态组合起来作为key,如: 1627182182_1 (1627182182 为分类 ID,1 为状态)。
@GetMapping("/list")
@Cacheable(value = "dishCache",key = "#dish.categoryId + '_' + #dish.status")
public R<List<DishDto>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品的id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
//SQL:select * from dish_flavor where dish_id = ?
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
为了保证数据库中数据与缓存数据的一致性,在我们添加菜品或者删除菜品数据之后,需要清空当前菜品缓存的全部数据。那么@CacheEvict
注解如何清除某一份缓存下所有的数据呢,这里我们可以指定@CacheEvict
中的一个属性 allEnties,将其设置为 true 即可。
@PostMapping
@CacheEvict(value = "dishCache",allEntries = true) //清除dishCache名称下,所有的缓存数据
public R<String> save(@RequestBody DishDto dishDto) {
log.info("新增菜品,菜品信息:{}", dishDto);
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}


上述确实能解决数据库频繁查询的问题,但是如果idea重启,缓存就消失了,下次启动idea得重新构建缓存,有没有一种缓存技术可以直接避免这种情况,提高缓存的可用性尼?
使用redis非关系型数据库即可
- 安装Redis,在非中文的路径下,解压焱哥发在微信群里的压缩包,点击运行startredis.bat 即可

点击运行startredis.bat 即可
- 在项目的pom.xml中添加redis依赖,点击右上角的刷新
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.5</version>
</dependency>
- 在项目的yml文件中添加redis链接配置
spring:
redis:
host: localhost
port: 6379
database: 0


反复点击发现,不会查询数据库

通过redis可视化软件,可以查看已经存入的数据,且关闭idea数据依然存在