🔥 本文由 程序喵正在路上 原创,CSDN首发!
💖 系列专栏:苍穹外卖项目实战
🌠 首发时间:2024年5月9日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
目录
缓存菜品
问题说明
用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力也会随之增大,造成用户体验较差。
实现思路
通过 Redis 来缓存菜品数据,减少数据库查询操作。
缓存逻辑分析:
-
每个分类下的菜品保存一份缓存数据
-
数据库中菜品数据有变更时清理缓存数据
代码开发
缓存菜品数据
通过调试,我们知道是通过 list 这个路径来查询菜品的
如果要缓存菜品,那么就需要修改用户端接口 DishController 中的 list 方法,在其中加入缓存处理逻辑:
package com.sky.controller.user;
import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
//构造redis缓存key, 规则为:dish_分类id
String key = "dish_" + categoryId;
//查询redis中是否已经有缓存数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
//如果存在缓存数据, 直接返回给前端
if (list != null && list.size() > 0) {
return Result.success(list);
}
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
list = dishService.listWithFlavor(dish);
//还没有缓存, 需要将查询到的数据载入缓存
redisTemplate.opsForValue().set(key, list);
return Result.success(list);
}
}
加入缓存后,当我们点击已经点击过的菜单时,就不会再对数据库进行查询
清理缓存数据
当数据库中菜品数据有变更时,我们需要及时清理缓存数据,不然会造成数据不一致的问题。
因此,我们需要修改管理端接口 DishController 的相关方法,加入清理缓存的逻辑,其中需要改造的方法有:
- 新增菜品
- 修改菜品
- 批量删除菜品
- 起售、停售菜品
首先,我们需要定义一个清理缓存数据的私有方法:
/**
* 清理缓存数据
*
* @param pattern
*/
private void cleanCache(String pattern) {
Set keys = redisTemplate.keys(pattern); //获取符合要求的所有key
redisTemplate.delete(keys);
}
然后在需要修改的方法中调用这个方法,可以减少代码量:
-
新增菜品
/** * 新增菜品 * * @param dishDTO * @return */ @PostMapping @ApiOperation("新增菜品") public Result<String> save(@RequestBody DishDTO dishDTO) { log.info("新增菜品:{}", dishDTO); dishService.saveWithFlavor(dishDTO); //由于新增菜品只会影响某一个分类, 所以只需要清理某个key的数据 Long categoryId = dishDTO.getCategoryId(); //获取分类id String key = "dish_" + categoryId; //构造key cleanCache(key); return Result.success(); }
在另外三个方法中,如果要精确删除某个分类的缓存数据,是有些复杂的,同时也会设计对数据库的查询操作,这是得不偿失的;而且这些操作不是经常性的,所以我们直接将所以菜品的缓存数据删除。
-
修改菜品
/** * 修改菜品 * * @param dishDTO * @return */ @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO) { log.info("修改菜品:{}", dishDTO); dishService.updateWithFlavor(dishDTO); //删除所有菜品的缓存数据 cleanCache("dish_*"); return Result.success(); }
-
批量删除菜品
/** * 菜品批量删除 * * @param ids * @return */ @DeleteMapping @ApiOperation("菜品批量删除") public Result delete(@RequestParam List<Long> ids) { log.info("菜品批量删除:{}", ids); dishService.deleteBatch(ids); //删除所有菜品的缓存数据 cleanCache("dish_*"); return Result.success(); }
-
起售、停售菜品
/** * 菜品启售停售 * * @param status * @return */ @PostMapping("/status/{status}") @ApiOperation("菜品启售停售") public Result startOrStop(@PathVariable Integer status, Long id) { log.info("菜品启售停售:{}", status); dishService.startOrStop(status, id); //删除所有菜品的缓存数据 cleanCache("dish_*"); return Result.success(); }
功能测试
可以通过如下方式进行测试:
- 查看控制台 sql
- 前后端联调
- 查看 Redis 中的缓存数据
我们来简单测试一下,启动项目,来到小程序端测试菜品这里:
然后,我们在管理端停售测试菜品:
再回到小程序端加载看看,可以看到已经没有了:
再将其启售,又可以看到了:
缓存套餐
Spring Cache
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
- EHCache
- Caffeine
- Redis
Spring Cache 所需要导入的 maven 坐标:
常用注解:
@EnableCaching
没啥好说,直接加项目启动类上
@CachePut
来看下面这个方法,这是一个简单的保存用户的方法,我们要实现在添加完用户后,将该用户的信息添加到缓存中,方便我们查询。
@PostMapping
@CachePut(cacheNames = "userCache", key = "#user.id") //user为方法的形参
// @CachePut(cacheNames = "userCache", key = "#result.id") //result表示方法的返回值, 在当前方法即为一个User对象
// @CachePut(cacheNames = "userCache", key = "#p0.id") //p0表示方法的第一个形参, 依此类推, p1表示第二个形参
// @CachePut(cacheNames = "userCache", key = "#a0.id") //a0表示方法的第一个形参, 同上
// @CachePut(cacheNames = "userCache", key = "#root.args[0].id") //同上
public User save(@RequestBody User user) {
userMapper.insert(user);
return user;
}
当使用 Spring 的缓存注解时,cacheNames
和 key
是两个常用的参数,它们可以用来更精确地控制缓存的行为。
-
cacheNames
参数:cacheNames
参数用于指定一个或多个缓存的名称,将当前方法的返回值存储到指定名称的缓存中。可以使用逗号分隔多个缓存名称,如果缓存不存在,则会自动创建。这样做的好处是可以根据不同的业务场景将数据存储到不同的缓存中,以便更好地管理和维护。- 在上面的方法中,方法的返回值将被存储在名为 “userCache” 的缓存中。
-
key
参数:-
key
参数用于指定缓存的键,以便唯一标识缓存中的数据。默认情况下,Spring 会使用方法的参数作为缓存的键,但是有时候方法的参数并不足以唯一标识缓存数据,这时就需要使用key
参数来自定义缓存的键。key
参数支持 Spring 的 SpEL 表达式语言,可以根据方法的参数、返回值等动态生成缓存的键,具体可以用哪些可以查看注解的源码。例如:@CachePut(cacheNames = "userCache", key = "#user.id")
在上面这个例子中,根据方法的参数
user
中的属性id
来生成缓存的键,确保了同样的id
对应着同样的缓存数据。@CachePut(cacheNames = "userCache", key = "#result.id")
在上面这个例子中,
result
表示为方法的返回值,根据result
中的属性id
来生成缓存的键,确保了同样的id
对应着同样的缓存数据。
-
-
缓存的键:
-
cacheNames
参数和key
参数会被用于生成缓存的键,也就是 redis 中的 key,其形式为cacheNames::key
,它在数据库中会生成一种树形的文件结构。比如,当我们添加了三个用户后,redis 数据库中就会多出三个缓存数据,它们的键即为:
-
通过合理地使用 cacheNames
和 key
参数,可以更灵活地控制缓存的存储和检索行为,提高系统的性能和可维护性。
@Cacheable
@GetMapping
@Cacheable(cacheNames = "userCache", key = "#id")
public User getById(Long id) {
User user = userMapper.getById(id);
return user;
}
@Cacheable
注解的参数的使用方法同 @CachePut
@CacheEvict
@DeleteMapping
@CacheEvict(cacheNames = "userCache", key = "#id") //只删除id所表示的键
public void deleteById(Long id) {
userMapper.deleteById(id);
}
@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache", allEntries = true) //删除全部的键
public void deleteAll() {
userMapper.deleteAll();
}
参数的使用方法同上
实现思路
具体的实现思路如下:
- 导入 Spring Cache 和 Redis 相关 maven 坐标
- 在启动类上加入
@EnableCaching
注解,开启缓存注解功能 - 在用户端接口 SetmealController 的 list 方法上加入
@Cacheable
注解 - 在管理端接口 SetmealController 的 save、delete、update、startOrStop 等方法上加入
CacheEvict
注解
代码开发
功能测试
通过前后端联调方式来进行测试,同时观察 redis 中缓存的套餐数据。
到了这里,你也可以自行使用 Spring Cache 来修改缓存菜品的代码。
添加购物车
需求分析和设计
产品原型:
接口设计:
- 请求方式:
POST
- 请求路径:
/user/shoppingCart/add
- 请求参数:套餐id、菜品id、口味
- 返回结果:
code
、data
、msg
数据库设计:
- 作用:暂时存放所选商品的地方
- 选的什么商品
- 每个商品都买了几个
- 不同用户的购物车需要区分开
数据库设计(shopping_cart表):
代码开发
根据添加购物车接口的参数设计 DTO:
根据添加购物车接口创建 ShoppingCartController,在其中创建 add 方法:
import com.sky.dto.ShoppingCartDTO;
import com.sky.result.Result;
import com.sky.service.ShoppingCartService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user/shoppingCart")
@Api(tags = "C端-购物车接口")
@Slf4j
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
/**
* 添加购物车
*
* @param shoppingCartDTO
* @return
*/
@PostMapping("/add")
@ApiOperation("添加购物车")
public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO) {
log.info("添加购物车, 商品信息为:{}", shoppingCartDTO);
shoppingCartService.addShoppingCart(shoppingCartDTO);
return Result.success();
}
}
创建 ShoppingCartService 接口,声明 addShoppingCart 方法:
import com.sky.dto.ShoppingCartDTO;
public interface ShoppingCartService {
/**
* 添加购物车
*
* @param shoppingCartDTO
*/
void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}
创建 ShoppingCartServiceImpl 实现类,并实现 addShoppingCart 方法:
import com.sky.context.BaseContext;
import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.ShoppingCart;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;
/**
* 添加购物车
*
* @param shoppingCartDTO
*/
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart); //拷贝对象属性
//设置购物车用户id, 只能查询自己的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());
//判断要添加商品是否在购物车中
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
if (shoppingCartList != null && shoppingCartList.size() == 1) {
//如果要添加的商品已经存在购物车中, 将数量加1即可
shoppingCart = shoppingCartList.get(0);
shoppingCart.setNumber(shoppingCart.getNumber() + 1);
shoppingCartMapper.updateNumberById(shoppingCart);
} else {
//不存在, 就添加到购物车中
Long dishId = shoppingCartDTO.getDishId();
if (dishId != null) { //要添加到购物车的是菜品
Dish dish = dishMapper.getById(dishId);
shoppingCart.setName(dish.getName());
shoppingCart.setImage(dish.getImage());
shoppingCart.setAmount(dish.getPrice());
} else { //不是菜品, 就是套餐
Setmeal setmeal = setmealMapper.getById(shoppingCartDTO.getSetmealId());
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
}
}
创建 ShoppingCartMapper 接口:
import com.sky.entity.ShoppingCart;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@Mapper
public interface ShoppingCartMapper {
/**
* 条件查询
*
* @param shoppingCart
* @return
*/
List<ShoppingCart> list(ShoppingCart shoppingCart);
/**
* 更新商品数量
*
* @param shoppingCart
*/
@Update("update shopping_cart set number = #{number} where id = #{id}")
void updateNumberById(ShoppingCart shoppingCart);
/**
* 插入购物车数据
*
* @param shoppingCart
*/
@Insert("insert into shopping_cart (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time) " +
"values (#{name}, #{image}, #{userId}, #{dishId}, #{setmealId}, #{dishFlavor}, #{number}, #{amount}, #{createTime})")
void insert(ShoppingCart shoppingCart);
}
创建 ShoppingCartMapper.xml 并配置 SQL:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.ShoppingCartMapper">
<!-- 条件查询-->
<select id="list" resultType="com.sky.entity.ShoppingCart">
select * from shopping_cart
<where>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="dishId != null">
and dish_id = #{dishId}
</if>
<if test="setmealId != null">
and setmeal_id = #{setmealId}
</if>
<if test="dishFlavor != null">
and dish_flavor = #{dishFlavor}
</if>
</where>
order by create_time desc
</select>
</mapper>
功能测试
可以通过如下方式进行测试:
- 查看控制台 sql
- Swagger 接口文档测试
- 前后端联调
启动服务,小程序端随便添加几个商品,有数据即可:
查看购物车
需求分析和设计
产品原型:
接口设计:
代码开发
在 ShoppingCartController 中创建查看购物车的方法:
/**
* 查看购物车
*
* @return
*/
@GetMapping("/list")
@ApiOperation("查看购物车")
public Result<List<ShoppingCart>> list() {
return Result.success(shoppingCartService.showShoppingCart());
}
在 ShoppingCartService 接口中声明查看购物车的方法:
/**
* 查看购物车
*
* @return
*/
List<ShoppingCart> showShoppingCart();
在 ShoppingCartServiceImpl 中实现查看购物车的方法:
/**
* 查看购物车
*
* @return
*/
public List<ShoppingCart> showShoppingCart() {
ShoppingCart shoppingCart = ShoppingCart.builder()
.userId(BaseContext.getCurrentId())
.build();
return shoppingCartMapper.list(shoppingCart);
}
功能测试
可以通过接口文档进行测试,最后完成前后端联调测试即可
清空购物车
需求分析和设计
产品原型:
接口设计:
代码开发
在 ShoppingCartController 中创建清空购物车的方法:
/**
* 清空购物车
*
* @return
*/
@DeleteMapping("/clean")
@ApiOperation("清空购物车")
public Result<String> clean() {
shoppingCartService.cleanShoppingCart();
return Result.success();
}
在 ShoppingCartService 接口中声明清空购物车的方法:
/**
* 清空购物车
*/
void cleanShoppingCart();
在 ShoppingCartServiceImpl 中实现清空购物车的方法:
/**
* 清空购物车
*/
public void cleanShoppingCart() {
shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId());
}
在 ShoppingCartMapper 接口中创建删除购物车数据的方法:
/**
* 根据用户id删除购物车数据
*
* @param userId
*/
@Delete("delete from shopping_cart where user_id = #{userId};")
void deleteByUserId(Long userId);
功能测试
可以通过接口文档进行测试,最后完成前后端联调测试即可
删除购物车中一个商品
需求分析和设计
产品原型:
接口设计:
代码开发
在 ShoppingCartController 中创建删除购物车中一个商品的方法:
/**
* 删除购物车中一个商品
*
* @param shoppingCartDTO
* @return
*/
@PostMapping("/sub")
@ApiOperation("删除购物车中一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO) {
log.info("删除购物车中一个商品:{}", shoppingCartDTO);
shoppingCartService.subShoppingCart(shoppingCartDTO);
return Result.success();
}
在 ShoppingCartService 接口中声明删除购物车中一个商品的方法:
/**
* 删除购物车中一个商品
*
* @param shoppingCartDTO
*/
void subShoppingCart(ShoppingCartDTO shoppingCartDTO);
在 ShoppingCartServiceImpl 中实现删除购物车中一个商品的方法:
/**
* 删除购物车中一个商品
*
* @param shoppingCartDTO
*/
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart); //拷贝对象属性
//设置查询条件, 查询当前登录用户的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
if (list != null && list.size() > 0) {
shoppingCart = list.get(0);
Integer number = shoppingCart.getNumber();
if (number == 1) {
//当前商品在购物车中的份数为1, 直接删除
shoppingCartMapper.deleteById(shoppingCart.getId());
} else {
//不为1, 减1
shoppingCart.setNumber(shoppingCart.getNumber() - 1);
shoppingCartMapper.updateNumberById(shoppingCart);
}
}
}
在 ShoppingCartMapper 接口中创建根据 id 删除购物车数据的方法:
/**
* 根据id删除购物车数据
*
* @param id
*/
@Delete("delete from shopping_cart where id = #{id};")
void deleteById(Long id);
功能测试
可以通过接口文档进行测试,最后完成前后端联调测试即可