项目大致完成,不过还有许多接口没有写,因此需要进行接口编写同时为了提高系统性能要进行项目优化。
目录
1.菜品的批量停售、起售
①功能预览
选择对应的菜品点击批量停售后,其售卖状态就改为停售,相反再次点击批量起售则改回来
②接口编写
思路分析:
控制器开发基本逻辑:当前端把基本的页面和ajax请求写好之后,我们开发对应的控制器来接收请求和参数,最后通过业务层和数据层处理完毕后返回给前端,这样就完成了一次请求和响应
当点击批量停售时,观察控制台捕获的ajax请求格式如下,这样我们就可以根据ajax请求来编写对应的控制器,对应的路径为dish/status/0 然后携带的参数为页面选择的菜品id,其应该用一个list集合来接收这样就可以完成一个或者多个菜品的批量状态修改
对应的控制器如下,通过@RequestParam来将多个参数封装到集合中
//批量停售菜品
@PostMapping("/status/0")
public Result<String> setStatus0(@RequestParam List<Long> ids){
return Result.success("修改成功");
}
先不管具体的处理逻辑,先进行断点调试看是否能接收到前端的参数,我们选择两个菜品同时点击批量停售:
这里观察到ids集合成功接收到2个菜品的id,因此参数接收没有问题
随后进行逻辑处理,即查询对应菜品后修改其状态为0即可(前端逻辑0表示停售,1表示起售)
//批量停售菜品
@PostMapping("/status/0")
public Result<String> setStatus0(@RequestParam List<Long> ids){
UpdateWrapper<Dish> wrapper = new UpdateWrapper<>();
wrapper.in("id",ids);
wrapper.set("status",0);
dishService.update(wrapper);
return Result.success("修改成功");
}
同理批量起售,则修改状态为1即可
// //批量启售菜品
@PostMapping("/status/1")
public Result<String> setStatus1(@RequestParam List<Long> ids){
UpdateWrapper<Dish> wrapper = new UpdateWrapper<>();
wrapper.in("id",ids);
wrapper.set("status",1);
dishService.update(wrapper);
return Result.success("修改成功");
}
2.套餐的批量停售、起售,修改和保存套餐
理解了菜品的批量停售起售,套餐也是同理,只需要修改泛型即可(咱们直接CV)
//批量停售套餐
@PostMapping("/status/0")
public Result<String> setStatus0(@RequestParam List<Long> ids){
UpdateWrapper<Setmeal> wrapper = new UpdateWrapper<>();
wrapper.in("id",ids);
wrapper.set("status",0);
setmealService.update(wrapper);
return Result.success("修改成功");
}
//批量启售套餐
@PostMapping("/status/1")
public Result<String> setStatus1(@RequestParam List<Long> ids){
UpdateWrapper<Setmeal> wrapper = new UpdateWrapper<>();
wrapper.in("id",ids);
wrapper.set("status",1);
setmealService.update(wrapper);
return Result.success("修改成功");
}
测试一下,状态成功修改
接下来就是套餐的修改和保存:
①功能预览:
先修改对应套餐,然后点击保存
②接口开发
套餐修改
修改对应的套餐,首先得展示对应的套餐,即完成一个套餐查询,这里发现此表并不是简单的一个套餐类,里面包含着菜品,因此需要用DTO来进行数据传输,展示所有数据
//查询对应id的套餐
@GetMapping("/{id}")
public Result<SetmealDto> findByid(@PathVariable Long id){
SetmealDto setmealDto = setmealService.getByIdWithDish(id);
return Result.success(setmealDto);
}
这里多表查询在业务层完成,调用了getByIdWithDish接口,这样就能展示所有数据
//查询两表数据
@Override
public SetmealDto getByIdWithDish(Long id) {
//从套餐表查询菜品基本信息
Setmeal setmeal = this.getById(id);
//拷贝给DTO
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(setmeal,setmealDto);
//查询当前套餐中的菜品信息
LambdaQueryWrapper<SetmealDish> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SetmealDish::getSetmealId,setmeal.getId());
List<SetmealDish> dishes = setmealDishService.list(wrapper);
//为dto的菜品属性设置值
setmealDto.setSetmealDishes(dishes);
return setmealDto;
}
套餐保存
保存对应修改后的套餐,同理也需要进行多表修改,在业务层完成逻辑处理
//保存修改套餐对应信息
@PutMapping
public Result<String> update(@RequestBody SetmealDto setmealDto){
//在业务层完成对套餐和套餐菜品表的处理
setmealService.updateWithDish(setmealDto);
return Result.success("修改成功");
}
//修改两表数据
@Transactional
@Override
public void updateWithDish(SetmealDto setmealDto) {
//修改套餐表
this.updateById(setmealDto);
//先清理套餐菜品表
LambdaQueryWrapper<SetmealDish> wrapper = new LambdaQueryWrapper();
wrapper.eq(SetmealDish::getSetmealId,setmealDto.getId());
setmealDishService.remove(wrapper);
//更新套餐菜品表
List<SetmealDish> dishes= setmealDto.getSetmealDishes();
dishes = dishes.stream().map((item)-> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(dishes);
}
另外声明一下这里添加菜品是调用了菜品的list接口来展示所有的菜品
//查询指定菜品
@GetMapping("/list")
public Result<List<Dish>> list(Dish dish){
//条件构造器
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper();
//添加查询条件
wrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//当菜品停售后不显示
wrapper.eq(Dish::getStatus,1);
//添加排序条件
wrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(wrapper);
return Result.success(list);
}
同时在用户端也调用的此接口,这些套餐的展示通过list控制器实现,当停售套餐时也需要展示只起售的套餐,而停售的套餐不展示,因此上面的控制器需要优化
3.菜品套餐停售与启售之间关联
思路分析:
光简单的去批量停售、启售菜品和套餐是不行的,因为套餐包含着菜品,当管理端停售了菜品,那么对应的套餐也应该自动停售,相反当启售套餐时,对应的菜品有些正在停售则要先启售菜品才能启售套餐,同时为了系统更加智能化,当启售、停售菜品后套餐也会跟着启售、停售(这里会进行逻辑判断套餐中的所有菜品是否属于启售才能启售此套餐)
分类:
菜品的停售和启售与套餐的关系
1.当菜品停售时,如果有套餐包含了此菜品,也应该一起停售,没有则直接停售
2.当菜品启售时,如果此菜品没有在任何套餐中,直接启售,若在套餐中,也不能直接将套餐一起启售,需要先判断套餐中的其他菜品是否都启售,当所有菜品启售时套餐才能随之启售
控制器编写:
//批量停售菜品
@PostMapping("/status/0")
public Result<String> setStatus0(@RequestParam List<Long> ids){
UpdateWrapper<Dish> wrapper = new UpdateWrapper<>();
UpdateWrapper<Setmeal> setmealwrapper = new UpdateWrapper<>();
//先查询对应菜品是否在套餐中
//查询所有套餐中的所有菜品
LambdaQueryWrapper<SetmealDish> stemealDishwapper = new LambdaQueryWrapper<>();
stemealDishwapper.in(SetmealDish::getDishId,ids);
//得到本次存在要停售菜品的集合
List<SetmealDish> list = setmealDishService.list(stemealDishwapper);
if (list!=null && list.size()!=0){ //存在套餐包含停售菜品
//遍历集合获取对应的setmeal_id
List<Long> collect = list.stream().map(SetmealDish::getSetmealId).collect(Collectors.toList());
//停售对应的套餐和菜品
//套餐 ,要传入查询出来的list集合
setmealwrapper.in("id",collect);
setmealwrapper.set("status",0);
setmealService.update(setmealwrapper);
//菜品
wrapper.in("id",ids);
wrapper.set("status",0);
dishService.update(wrapper);
}else {
//没有,停售菜品
wrapper.in("id",ids);
wrapper.set("status",0);
dishService.update(wrapper);
}
return Result.success("修改成功");
}
//批量启售菜品
@PostMapping("/status/1")
public Result<String> setStatus1(@RequestParam List<Long> ids){
UpdateWrapper<Dish> wrapper = new UpdateWrapper<>();
UpdateWrapper<Setmeal> setmealwrapper = new UpdateWrapper<>();
//获取菜品所属的套餐集合
LambdaQueryWrapper<SetmealDish> stemealDishwapper = new LambdaQueryWrapper<>();
stemealDishwapper.in(SetmealDish::getDishId,ids);
List<SetmealDish> setmeallist = setmealDishService.list(stemealDishwapper);
//先查询启售菜品中是否有对应套餐
if (setmeallist!=null && setmeallist.size()>0){ //套餐中有此菜品
//得到菜品id的集合
List<Long> dishcollect = setmeallist.stream().map(SetmealDish::getDishId).collect(Collectors.toList());
//得到套餐id的集合
List<Long> setmealcollect = setmeallist.stream().map(SetmealDish::getSetmealId).collect(Collectors.toList());
//获取停售菜品的状态集合
LambdaQueryWrapper<Dish> wrapper2 = new LambdaQueryWrapper<>();
wrapper2.in(Dish::getId,dishcollect);
wrapper2.eq(Dish::getStatus,0);
//得到停售的所有菜品
List<Dish> dishlist = dishService.list(wrapper2);
if (dishlist!=null && dishlist.size()>0){
//有停售的菜品,不能将套餐启售,只启售本菜品
wrapper.in("id",ids);
wrapper.set("status",1);
dishService.update(wrapper);
//当此菜品为该套餐最后一个停售的菜品时,也将改变套餐的启售状态
//判断此套餐中所有菜品的状态
LambdaQueryWrapper<SetmealDish> wrapper1 = new LambdaQueryWrapper<>();
wrapper1.in(SetmealDish::getSetmealId,setmealcollect);
//收集套餐中的菜品
List<SetmealDish> dishlist1 = setmealDishService.list(wrapper1);
//获取菜品id
List<Long> dishcollectlast = dishlist1.stream().map(SetmealDish::getDishId).collect(Collectors.toList());
LambdaQueryWrapper<Dish> wrapper3 = new LambdaQueryWrapper<>();
wrapper3.in(Dish::getId,dishcollectlast);
wrapper3.eq(Dish::getStatus,0);
List<Dish> dishlistlast = dishService.list(wrapper3);
if ( dishlistlast.size()==0){ //修改套餐状态
setmealwrapper.in("id",setmealcollect);
setmealwrapper.set("status",1);
setmealService.update(setmealwrapper);
}
}else { //否则,改套餐的所有菜品已经起售,可以进行套餐和菜品启售
//套餐 ,要传入查询出来的collect集合
setmealwrapper.in("id",setmealcollect);
setmealwrapper.set("status",1);
setmealService.update(setmealwrapper);
//菜品
wrapper.in("id",ids);
wrapper.set("status",1);
dishService.update(wrapper);
}
}else {
//如果此菜品没有包含在套餐中直接启售菜品
wrapper.in("id",ids);
wrapper.set("status",1);
dishService.update(wrapper);
}
return Result.success("修改成功");
}
停售启售与菜品的联系
1.当套餐停售时,菜品的停售起售与其无关,因为套餐的停售并不影响菜品
2.当套餐启售时,如果套餐中有菜品停售,则对应的套餐不能启售,应该先启售对应菜品后在对套餐进行启售(由于菜品启售会自动启售套餐,所以当起售完对应套餐中所有的菜品后自动启售),如果没有菜品停售则直接启售
控制器编写:
//批量停售套餐
@PostMapping("/status/0")
public Result<String> setStatus0(@RequestParam List<Long> ids){
UpdateWrapper<Setmeal> wrapper = new UpdateWrapper<>();
wrapper.in("id",ids);
wrapper.set("status",0);
setmealService.update(wrapper);
return Result.success("修改成功");
}
//批量启售套餐
@PostMapping("/status/1")
public Result<String> setStatus1(@RequestParam List<Long> ids){
//当套餐启售时,如果套餐中有菜品停售,则对应的套餐不能启售,应该先启售对应菜品后在对套餐进行启售
//先查询套餐中的所有菜品
LambdaQueryWrapper<SetmealDish> setmealdishwrapper = new LambdaQueryWrapper<>();
setmealdishwrapper.in(SetmealDish::getSetmealId,ids);
List<SetmealDish> list = setmealDishService.list(setmealdishwrapper);
//收集所有菜品的id
List<Long> collect = list.stream().map(SetmealDish::getDishId).collect(Collectors.toList());
LambdaQueryWrapper<Dish> dishwapper = new LambdaQueryWrapper<>();
dishwapper.eq(Dish::getStatus,0);
dishwapper.in(Dish::getId,collect);
//通过dish表得到可能含有停售菜品的集合,进行判断
List<Dish> dishList = dishService.list(dishwapper);
//判断这些菜品是否停售
if (dishList !=null && dishList.size()>0){
//停售,则不能启售此套餐
throw new CustomException("请先启售对应的菜品,才能启售套餐");
}else {
//否则,直接启售套餐
UpdateWrapper<Setmeal> wrapper = new UpdateWrapper<>();
wrapper.in("id",ids);
wrapper.set("status",1);
setmealService.update(wrapper);
}
return Result.success("修改成功");
}
功能测试:
停售2个菜品,这两个菜品在套餐麻辣鲜中
观察对应的套餐,也改为停售
启售该套餐,由于里面菜品停售,提示无法启售
我们依次启售这两个菜品,同时观察对应套餐状态
当启售第一个菜品时,对应套餐没有变化
启售第二个菜品时,由于套餐所有的菜品都已经起售,套餐自动启售
这里的逻辑就是基于,菜品控制器中的启售接口(里面判断了当前套餐中菜品的状态,和最后一个停售的菜品启售时的操作,同时修改了套餐状态,完成菜品和套餐的同步启售,如果不想影响套餐状态去掉对应的设置状态的代码即可,)
4.订单页面的展示,查询对应时间订单,派送订单
订单的展示是基于用户下单后,在order表中生成一行数据,因此需要进行分页查询,同时由于这里可以查询对应时间内的订单,因此也要给接收时间的参数。
//订单列表
@GetMapping("/page")
public Result<Page> page(Integer page, Integer pageSize, String number,
//这里接收前端日期需要进行格式转换(string->Date)才能接收
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date beginTime,
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime
) {
//构造分页器 页码, 每页数
Page pageInfo = new Page(page, pageSize);
//构造过滤条件
LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper();
wrapper.eq(number != null, Orders::getNumber, number);
//筛选对应时间段
wrapper.between(beginTime != null && endTime != null, Orders::getOrderTime, beginTime, endTime);
wrapper.orderByDesc(Orders::getOrderTime);
ordersService.page(pageInfo, wrapper);
return Result.success(pageInfo);
}
成功显示
派送订单 (当用户下单后默认已支付即状态都为正在派送)
观察前台的代码发现,不同的status对应不同的状态,因此只需要修改status即可
操作对应的js
//修改订单的状态为完成
@PutMapping
public Result<String> setStatus(@RequestBody Orders orders) {
Long id = orders.getId();
//这里不知道为什么页面显示状态默认为已派送(用户移动端下单状态为1,之后插入订单数据即已经支付状态为2),status应该为2应该为正在派送,但传过来的为3所以减1
Integer status = orders.getStatus() - 1;
UpdateWrapper<Orders> wrapper = new UpdateWrapper<>();
wrapper.eq("id", id);
//修改订单的状态
if (status == 2) {
wrapper.set("status", 3);
} else if (status == 3) {
wrapper.set("status", 4);
} else {
//否则为取消
wrapper.set("status", 5);
}
ordersService.update(wrapper);
return Result.success("修改成功");
}
5.用户端,退出登录,查询最新订单,历史订单,再来一单
效果展示
个人中心可以查看最新订单,历史订单(里面可以再来一单),退出登录等功能
首先是退出登录,很简单清除对应session即可
//用户退出接口
@PostMapping("/loginout")
public Result<String> loginout(HttpServletRequest request){
//获取session域并清除对应session
request.getSession().removeAttribute("user");
return Result.success("退出成功");
}
这里需要将用户退出登录的接口路径进行过滤,这样才能完成退出的功能
最新订单与历史订单(历史订单与其共用一个接口)
展示出最新的订单,首先得查询出该用户的所有订单,同时添加过滤条件,根据时间进行降序排序即可,这里默认展示最新订单是取的数组第一个订单(order[0]),因此要进行降序排序
@GetMapping("/userPage")
public Result<Page> pageUser(Integer page, Integer pageSize){
//构造分页器 页码, 每页数
Page pageInfo = new Page(page, pageSize);
//构造过滤条件
LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper();
//获取当前登录的用户
Long userid = BaseContext.getCurrentId();
//添加过滤条件
wrapper.eq(Orders::getUserId,userid);
wrapper.orderByDesc(Orders::getOrderTime);
//执行条件查询
ordersService.page(pageInfo,wrapper);
return Result.success(pageInfo);
}
这里测试发现,很奇怪并没有展示所有的数据,观察发现需要进行dto封装展示多表数据
优化:
@Data
public class OrdersDto extends Orders {
private String userName;
private String phone;
private String address;
private String consignee;
private List<OrderDetail> orderDetails;
}
//光查询订单返回前端不足以展示所以的数据,因此需要封装一个dto来进行数据传输
@GetMapping("/userPage")
public Result<Page> pageUser(Integer page, Integer pageSize) {
//构造分页器 页码, 每页数
Page pageInfo = new Page(page, pageSize);
//将分页查询结果进行对象拷贝,给分页对象添加更多的数据
Page<OrdersDto> dtoPage = new Page<>();
//构造过滤条件
LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper();
//获取当前登录的用户
Long userid = BaseContext.getCurrentId();
//添加过滤条件
wrapper.eq(Orders::getUserId, userid);
wrapper.orderByDesc(Orders::getOrderTime);
//执行条件查询
ordersService.page(pageInfo, wrapper);
//对象拷贝 //忽略records,泛型不一致records返回list集合
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Orders> records = pageInfo.getRecords();
List<OrdersDto> list = records.stream().map((item)->{
OrdersDto ordersDto = new OrdersDto();
//先将分页属性拷贝给dto
BeanUtils.copyProperties(item,ordersDto);
//查询订单id
Long id = item.getId();
//通过order表的id获取订单细节表中的name和number
LambdaQueryWrapper<OrderDetail> detailwrapper = new LambdaQueryWrapper<>();
//这里id和order_id是两表联系的关键,因此通过查询orderid来获取订单细节
detailwrapper.eq(OrderDetail::getOrderId,id);
List<OrderDetail> detailList = orderDetailService.list(detailwrapper);
if (detailList!=null){
//将订单细节封装dto中的List集合里
ordersDto.setOrderDetails(detailList);
}
//返回dto
return ordersDto;
}).collect(Collectors.toList());
//将返回的集合赋值给dto对象
dtoPage.setRecords(list);
return Result.success(dtoPage);
}
再来一单
功能预览 :历史订单中有再来一单,点击后跳转至购物车页面
观察对应ajax请求
控制器编写:
需求分析:1.先通过订单id查询出对应的订单明细
2.清空购物车的菜品,同时为购物车添加查询出来的菜品
//再来一单,通过订单id获取订单和对应订单明细,随后为其设置属性并保存到表中
@PostMapping("/again")
public Result<String> again(@RequestBody Orders order){
//获取订单id
Long id = order.getId();
//通过订单id查询对应菜品
LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderDetail::getOrderId,id);
List<OrderDetail> list = orderDetailService.list(wrapper);
//先清空购物车中的菜品
//获取当前用户id
Long userid = BaseContext.getCurrentId();
//获取购物车中的菜品和套餐
LambdaQueryWrapper<ShoppingCart> shoppingwrapper = new LambdaQueryWrapper();
shoppingwrapper.eq(ShoppingCart::getUserId,userid);
//清空购物车
shoppingCartService.remove(shoppingwrapper);
//将订单明细中的数据插入到购物车中
List<ShoppingCart> shoppingCartList = list.stream().map((item) -> {
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setName(item.getName());
shoppingCart.setImage(item.getImage());
shoppingCart.setUserId(BaseContext.getCurrentId());
shoppingCart.setDishId(item.getDishId());
shoppingCart.setSetmealId(item.getSetmealId());
shoppingCart.setDishFlavor(item.getDishFlavor());
shoppingCart.setNumber(item.getNumber());
shoppingCart.setAmount(item.getAmount());
return shoppingCart;
}).collect(Collectors.toList());
//将数据保存到购物车中
shoppingCartService.saveBatch(shoppingCartList);
return Result.success("保存成功");
}
功能测试:
点击15号购买的套餐
观察购物车展示了本次订单选购的套餐
6.缓存优化-redis
通过redis缓存,来提高系统性能,用户请求先查询内存而不是数据库
1.搭建对应环境
导入maven坐标
<!-- 导入redis缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redis连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--spring-cahce-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
配置yml文件(这里需要开启redis服务)
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
配置类的编写:
package com.lyl.Config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@EnableCaching //开启缓存
@Configuration //配置类
public class RedisConfig extends CachingConfigurerSupport {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
@SuppressWarnings("all")
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
2.缓存验证码
基本思路
实现:usercontroller中
3.缓存菜品
基本思路
//查询指定菜品
@GetMapping("/list")
public Result<List<DishDto>> list(Dish dish){
List<DishDto> dtolist = null;
//从redis获取缓存数据(缓存多钟菜品,而不是将所有菜品一起缓存) ,key通过分类id区分
String key = "dish_" + dish.getCategoryId() + "_" +dish.getStatus();
dtolist = (List<DishDto>) redisTemplate.opsForValue().get(key);
if (dtolist!=null){ // 存在则查询缓存
return Result.success(dtolist);
}else { // 否则查询数据库
//条件构造器
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper();
//添加查询条件
wrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//当菜品停售后不显示
wrapper.eq(Dish::getStatus,1);
//添加排序条件
wrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(wrapper);
dtolist = list.stream().map((item)->{
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
//当前菜品id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishId);
//当前菜品口味集合
List<DishFlavor> dishFlavorList = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
//当查询完毕后,将查询结果加入redis缓存,并设置缓存时间
redisTemplate.opsForValue().set(key,dtolist,60, TimeUnit.MINUTES);
return Result.success(dtolist);
}
}
为了保证数据库和缓存的数据一致,需要在修改和保存删除接口中,每次完成后清除对应缓存,这样下次查询数据会查询数据库的数据,从而更新数据一致
spring-cache框架
由于使用boot整合的redis,灵活性不好,而且操作嵌入在代码中,耦合度太高,所以使用spring-cache来实现redis缓存,通过注解配置同时功能更加强大。
<!--spring-cahce-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
使用:
4.缓存套餐
在list接口中加入缓存注解
测试后报错,java对象需要实现序列化接口才能在redis中缓存
随后在增删改接口中,加入清理缓存注解 、为true表示删除此分类缓存下的所有key
测试查看reids,成功缓存!
这里当套餐停售、启售时也需要更新缓存
同时给分类控制器也加上缓存
当然还有很多接口都需要进行缓存,比如分页控制器、订单购物车等等,这里就不展示了
7.主从复制和读写分离
为了减轻数据库服务器的压力,让读写分离,当主库更新时从库同步其数据即主从复制
1.主从复制
首先在linux系统下通过docker创建2个mysql容器,他们的端口为主库3301和从库3302
观察navicat也连接成功,连接的主机ip地址为虚拟机分配的ip
随后修改数据库权限并为主库和从库添加日志。
[mysqld]
## 同一局域网内注意要唯一
server-id=100
## 开启二进制日志功能,可以随便取(关键)
log-bin=master-bin
binlog-format=ROW // 二级制日志格式,有三种 row,statement,mixed
binlog-do-db=数据库名 //同步的数据库名称,如果不配置,表示同步所有的库
配置从库,启动主从复制
查看对应状态,完成主从复制
在master主库中新建test数据库,观察从库也进行了复制
2.读写分离
使用框架来完成主从复制
yml中配置:配置多个数据源,当执行sql时会进行读写分离,进入不同数据库
spring:
shardingsphere:
datasource:
# 定义数据源的名字
names:
master,slave
# 主数据源
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.17.0.2:3306/rw?characterEncoding=utf-8
username: root
password: root
# 从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.17.0.3:3306/rw?characterEncoding=utf-8
username: root
password: root
masterslave:
# 读写分离配置
load-balance-algorithm-type: round_robin #轮询
# 最终的数据源名称
name: dataSource
# 主库数据源名称
master-data-source-name: master
# 从库数据源名称列表,多个逗号分隔
slave-data-source-names: slave
props:
sql:
show: true #开启SQL显示,默认false
main:
allow-bean-definition-overriding: true
启动项目使用postman测试
先进行查询操作。
观察控制台可知,查询走的是slave从库的sql
在进行新增操作
观察控制台可知,新增操作走的是master主库
查看数据库从库的数据也进行了主从复制,至此测试成功
8.nginx反向代理
1.安装使用
使用docker拉取nginx,并在宿主机中挂载配置文件
先在宿主机创建对应配置文件
对应的功能:
容器中的nginx.conf文件和conf.d文件夹复制到宿主机
删除之前的nginx并启动run新的nginx
测试成功
2.配置文件(conf/nginx.conf)
我们cat此文件,发现了三个模块
3.nginx的应用
部署静态资源
反向代理
配置
负载均衡
配置:
9.前后端分离开发
后端接口文档生成
配置类:
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
/**
* 扩展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);
}
@Bean
public Docket createRestApi() {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//扫描controller包生成接口文档
.apis(RequestHandlerSelectors.basePackage("com.lyl.Web.Controller"))
.paths(PathSelectors.any())
.build();
}
//接口文档标题
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("吃货外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
过滤对应的接口(不登录也能访问)
启动项目,测试
访问文档页面,成功
我们可以查看每个接口的具体请求方式,参数,路径,响应参数等等
项目部署:
至此整个项目的学习正式结束!感谢黑马