目录

一、缓存菜品模块(代码实现redis缓存)

1、问题分析

2、缓存菜品数据

3、清理缓存数据

为什么在管理员端对缓存数据进行清理?

二、缓存套餐模块(注解实现redis缓存)

1、Spring Cache框架

(1)常用注解

2、用注解方式实现缓存套餐功能

三、购物车模块

1、添加购物车 - POST接口

(1)需求分析

像name、image、amount这些属于冗余字段,为什么要设置冗余字段?

(2)代码开发

2、查看购物车 - GET接口

3、清空购物车 - DELETE接口

4、删除购物车中的一个商品 - POST接口


一、缓存菜品模块(代码实现redis缓存)

1、问题分析

  • 用户端小程序展示菜品数据都是通过查询数据库获得,如果用户访问量大,数据库访问压力就会随之增大
  • 因此我们通过 Redis 缓存菜品数据,可以减少数据库查询操作,提高效率

2、缓存菜品数据

  • 在DishController中加入查询Redis是否存在缓存的判断逻辑
  • 若Redis存在缓存数据,直接返回,否则查询数据库并存入Redis
@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) {
        //1.构造redis中的key,规则:dish_分类id
        String key = "dish_" + categoryId;

        //2.查询redis是否存在菜品数据
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
        if(list != null && list.size() > 0){
            //3.如果存在,直接返回,无需查询数据库
            return Result.success(list);
        }

        //3.如果不存在,查询数据库,将查询的数据存入redis
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

        list = dishService.listWithFlavor(dish);
        redisTemplate.opsForValue().set(key,list);

        return Result.success(list);
    }

}

3、清理缓存数据

如果没有清理redis缓存,以下功能会出现问题:

  • 新增菜品:管理员虽然新增了菜品,但由于redis内旧数据未清理,用户仍然只能看到以前的菜品数据,看不到新增的菜品(controller层查询redis是否有对应数据,如果查到了直接在controller层返回,根本不会去数据库拿新增的数据)
  • 删除菜品:管理员虽然在后端删除了菜品,但由于redis内旧数据未清理,用户仍然还能看到被删除的菜品数据
  • 修改菜品:管理员虽然修改了菜品,但由于redis内旧数据未清理,用户看到的菜品数据依旧是未更新之前的
  • 菜品起售停售:管理员虽然更改了菜品状态,但由于redis内旧数据未清理,用户看到的菜品状态依旧是未更新之前的

所以我们需要在Admin端对缓存数据进行清理

为什么在管理员端对缓存数据进行清理?

因为管理员端需要清理缓存后,再进行增删改,这样用户端才能看到最新数据

下面是增加清理缓存功能的Admin - DishController

/**
 * 菜品管理
 */
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {

    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 新增菜品
     * @param dishDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO){
        log.info("新增菜品:{}",dishDTO);
        dishService.saveWithFlavor(dishDTO);

        //清理缓存数据
        String key = "dish_" + dishDTO.getCategoryId();
        cleanCache(key);

        return Result.success();
    }

    /**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("菜品分页查询")
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
        log.info("菜品分页查询:{}",dishPageQueryDTO);
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);
    }

    /**
     * 批量删除菜品
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除菜品")
    public Result detele(@RequestParam List<Long> ids){
        log.info("批量删除:{}",ids);
        dishService.deleteBatch(ids);

        //将所有菜品缓存数据清除,所有以dish_开头的key
        cleanCache("dish_*");

        return Result.success();
    }

    /**
     * 根据id查询菜品
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询菜品")
    public Result<DishVO> getById(@PathVariable Long id){
        log.info("根据id查询菜品:{}",id);
        DishVO dishVO = dishService.getByIdWithFlavor(id);
        return Result.success(dishVO);
    }

    /**
     * 修改菜品
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO){
        log.info("修改菜品:{}",dishDTO);
        dishService.updateWithFlavor(dishDTO);

        //将所有菜品缓存数据清除,所有以dish_开头的key
        cleanCache("dish_*");

        return Result.success();
    }

    /**
     * 菜品起售停售
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("起售停售菜品")
    public Result startOrStop(@PathVariable Integer status, Long id){
        log.info("起售停售菜品:{},{}",status,id);
        dishService.startOrStop(status,id);

        //将所有菜品缓存数据清除,所有以dish_开头的key
        cleanCache("dish_*");

        return Result.success();
    }

    /**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<Dish>> list(Long categoryId){
        List<Dish> list = dishService.getByCategoryId(categoryId);
        return Result.success(list);
    }

    /**
     * 清除缓存数据
     * @param pattern
     */
    private void cleanCache(String pattern){
        Set keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }
}

这样,当我们在后端修改、新增、删除、改变菜品状态时,用户端也能将数据及时更新

二、缓存套餐模块(注解实现redis缓存)

1、Spring Cache框架

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:EHCache、Caffeine、Redis

(1)常用注解

注解 说明 位置
@EnableCaching 开启缓存注解功能 放启动类上
@Cacheable

在方法执行前先查询缓存中是否有数据,如果有数据则直接返回缓存数据

如果没有缓存数据,调用方法并将方法返回值放到缓存中(既能取数据,又能放缓存)

放方法上
@CachePut 将方法的返回值放到缓存中(只能放缓存) 放方法上
@CacheEvict 将一条或多条数据从缓存中删除 放方法上








 

2、用注解方式实现缓存套餐功能

步骤:

  1. 导入Spring Cache和Redis相关maven坐标
  2. 在启动类上加入@EnableCaching注解,开启缓存注解功能
  3. 在用户端接口SetmealController的 list 方法上加入@Cacheable注解
  4. 在管理端接口SetmealController的 save、delete、update、startOrstop等方法上加入CacheEvict注解

(1)导入Spring Cache和Redis的maven

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

(2)在启动类上加入@EnableCaching注解

(3)用户端接口SetmealController的 list 方法上加入@Cacheable注解

为什么只在【根据分类id查询套餐】方法上加Cache注解?

  • 因为该方法是基础查询,用户可能会频繁访问查询套餐,因此需要在该方法上加入Cache注解,而下面【根据套餐id查询菜品】的功能,相对于该方法的访问就没有那么频繁,因此不加Cache注解
    /**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    //查询前先在redis中查是否有相应数据,如果有,直接返回;如果没有,再从数据库查,查完存入redis缓存
    @Cacheable(cacheNames = "setmealCache",key = "#categoryId") //生成key:setmealCache::100
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);

        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }

(4)管理端接口SetmealController的 save、delete、update、startOrstop等方法上加入CacheEvict注解

【1】save

为什么key是setmealDTO.categoryId而不是setmealDTO.id?

  • 因为新增套餐前,需要把该套餐【所属分类下的所有套餐】缓存全部删除,否则新增后,用户界面读取的依旧是缓存内的旧数据
  • 比如:缓存内 → 分类A[套餐1,套餐2,套餐3],现在管理员新增套餐4,需要先将分类A下的套餐1、2、3缓存数据清理掉,再新增套餐4,此时用户查询分类A下的套餐,后端发现缓存中并没有分类A套餐信息,故从数据库中查询出数据 分类A[套餐1,套餐2,套餐3,套餐4],并将其存入redis缓存,这样用户端就能看到新增的套餐4
  • 如果key = setmealDTO.id会怎么样?
  • 比如新增套餐4,新增前删除缓存,key为套餐id,即删除【key=套餐4的id】这条缓存,问题是删除的时候套餐4还没加进来,哪里来的缓存?因此我们删除时应该采用分类id,这样删除的是该分类下的所有菜品缓存
    /**
     * 新增套餐
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")
    public Result save(@RequestBody SetmealDTO setmealDTO){
        log.info("新增套餐:{}",setmealDTO);
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }

【2】delete

    /**
     * 批量删除套餐
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result delete(@RequestParam List<Long> ids){
        log.info("批量删除:{}",ids);
        setmealService.deleteBatch(ids);
        return Result.success();
    }

【3】update

    /**
     * 修改套餐
     * @param setmealDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result update(@RequestBody SetmealDTO setmealDTO){
        log.info("修改套餐信息:{}",setmealDTO);
        setmealService.update(setmealDTO);
        return Result.success();
    }

【4】startOrstop

    /**
     * 起售停售套餐
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("起售停售套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result startOrStop(@PathVariable Integer status,Long id){
        log.info("起售停售套餐:{},{}",status,id);
        setmealService.startOrStop(status,id);
        return Result.success();
    }

三、购物车模块

1、添加购物车 - POST接口

(1)需求分析

像name、image、amount这些属于冗余字段,为什么要设置冗余字段?

设置冗余字段主要是为了用空间换时间,提升查询性能

冗余字段要求:不频繁改变、相对稳定

(2)代码开发

【1】controller层

    /**
     * 添加购物车
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/add")
    @ApiOperation("添加购物车")
    public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("添加购物车:{}",shoppingCartDTO);
        shoppingCartService.addShoppingCart(shoppingCartDTO);
        return Result.success();
    }

【2】service层

将菜品/套餐加入购物车时,需要进行以下判断:

  • 查询购物车中是否存在该菜品/套餐
    • DTO中只含dish_id菜品id、setmeal_id套餐id、dish_flavor口味表,但我们查询时需要通过【user_id】+【菜品/套餐id】,因此我们用实体类存数据,并用线程获取当前userid绑定在实体类中
    • 将该实体类传入mapper层进行条件查询
  • 如果购物车中存在该菜品/套餐
    • 获取该菜品/套餐数量,然后+1,更新这条购物车记录
  • 如果购物车中不存在该菜品/套餐
    • 判断一下添加到是套餐or菜品,然后通过获取菜品表/套餐表的image图片、amount价格、name名字,将其补充至这条购物车记录中(补充冗余数据)
    • 接着设置该商品数量为1,并设置记录创建时间
    • 最后将这条补充好的记录插入购物车表中
    /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        //1.判断购物车中,该商品是否存在

        // 因为DTO中只有dishid、setmealid、dishFlavor,而我们需要userid查询
        // 所以将DTO转为实体类,如果数据库表结构变化,只需要调整Entity和转换逻辑,不影响对外的DTO接口,保持接口稳定性
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);

        //为什么用list集合?
        //返回集合是为了接口通用,后面查询购物车也能用,但是这里只会有一个数据
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);

        //2.如果存在,商品数量+1
        if(list != null && list.size() > 0){
            //每一次请求操作只会传入一条数据,也就是第一条
            ShoppingCart cart = list.get(0);
            cart.setNumber(cart.getNumber() + 1);
            //更新一下商品数量
            shoppingCartMapper.updateNumberById(cart);
        }else{
            //3.如果不存在,添加进购物车

            //判断一下新增的是菜品or套餐
            Long dishId = shoppingCart.getDishId();

            if(dishId != null){
                //本次加入购物车的是菜品
                Dish dish = dishMapper.getById(dishId);
                //加入一些原本购物车内没有的元素:照片、价格、菜名
                shoppingCart.setImage(dish.getImage());
                shoppingCart.setAmount(dish.getPrice());
                shoppingCart.setName(dish.getName());
            }else{
                //本次加入购物车的是套餐
                Long setmealId = shoppingCart.getSetmealId();
                Setmeal setmeal = setmealMapper.getById(setmealId);
                shoppingCart.setImage(setmeal.getImage());
                shoppingCart.setAmount(setmeal.getPrice());
                shoppingCart.setName(setmeal.getName());
            }
            shoppingCart.setNumber(1); //新加入购物车的数量为1
            shoppingCart.setCreateTime(LocalDateTime.now());

            shoppingCartMapper.insert(shoppingCart);
        }

    }

【3】mapper层

@Mapper
public interface ShoppingCartMapper {
    /**
     * 条件查询
     * @param shoppingCart
     * @return
     */
    List<ShoppingCart> list(ShoppingCart shoppingCart);

    /**
     * 修改购物车数量
     * @param shoppingCart
     */
    @Update("update sky_take_out.shopping_cart set number = #{number} where id = #{id}")
    void updateNumberById(ShoppingCart shoppingCart);

    /**
     * 插入购物车数据
     * @param shoppingCart
     */
    @Insert("insert into sky_take_out.shopping_cart (name,user_id,dish_id,setmeal_id,dish_flavor,number,amount,image,create_time)" +
            "values (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})" )
    void insert(ShoppingCart shoppingCart);
}

【4】mybatis文件

<?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 sky_take_out.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>
    </select>
</mapper>

2、查看购物车 - GET接口

【1】controller层

    /**
     * 查看购物车
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查看购物车")
    public Result<List<ShoppingCart>> list(){
        List<ShoppingCart> list = shoppingCartService.showShoppingCart();
        return Result.success(list);
    }

【2】service层

    /**
     * 查看购物车
     * @return
     */
    public List<ShoppingCart> showShoppingCart() {
        ShoppingCart shoppingCart = new ShoppingCart();
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);

        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);

        return list;
    }

3、清空购物车 - DELETE接口

【1】controller层

    /**
     * 清空购物车
     * @return
     */
    @DeleteMapping("/clean")
    @ApiOperation("清空购物车")
    public Result delete(){
        shoppingCartService.deleteAll();
        return Result.success();
    }

【2】service层

    /**
     * 清空购物车
     */
    public void deleteAll() {
        Long user_id = BaseContext.getCurrentId();
        shoppingCartMapper.deleteAllByUserId(user_id);
    }

【3】mapper层

    /**
     * 删除当前用户的全部购物车数据
     * @param user_id
     */
    @Delete("delete from sky_take_out.shopping_cart where user_id = #{user_id}")
    void deleteAllByUserId(Long user_id);

4、删除购物车中的一个商品 - POST接口

【1】controller层

    /**
     * 删除购物车的一个商品
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/sub")
    @ApiOperation("删除购物车的一个商品")
    public Result delete(@RequestBody ShoppingCartDTO shoppingCartDTO){
        shoppingCartService.delete(shoppingCartDTO);
        return Result.success();
    }

【2】service层

这里逻辑和添加购物车差不多

  • 添加购物车时要先看该商品是否在车里,同理,在删除该商品前,要先看该商品是否在车里
  • 如何查询该商品是否在车里?
    • 将DTO中存在的【dish_id、setmeal_id、dish_flavor】 + 线程获得的【user_id】加入购物车实体类
    • 将这个购物车实体传入mapper层的条件查询list中
    • 如果该商品存在车内,那么list查询返回的列表不为空,且商品唯一,我们就进行删除;否则就不需要做删除操作
    • 当确定商品存在购物车内,又分两种情况:
      • 1、该商品只有1件,此时只需要根据【当前购物车记录id】直接删除这条商品购物车记录即可
      • 2、该商品有多件,此时只需要修改商品数量为原来-1即可
    /**
     * 删除购物车的一个商品
     * @param shoppingCartDTO
     */
    public void delete(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 num = shoppingCart.getNumber();
            if(num == 1){
                //如果该商品只有一个,直接删掉该商品
                shoppingCartMapper.deleteById(shoppingCart.getId());
            }else{
                //其他情况修改份数
                shoppingCart.setNumber(num - 1);
                shoppingCartMapper.updateNumberById(shoppingCart);
            }
        }
    }

【2】mapper层

    /**
     * 根据id删除购物车记录
     * @param id
     */
    @Delete("delete from sky_take_out.shopping_cart where id = #{id}")
    void deleteById(Long id);

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐