苍穹外卖-day07

课程内容

  • 缓存菜品
  • 缓存套餐
  • 添加购物车
  • 查看购物车
  • 清空购物车

功能实现:缓存商品购物车

效果图:

在这里插入图片描述

1. 缓存菜品(业务逻辑)

1.1 问题说明

用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。

在这里插入图片描述

结果:系统响应慢、用户体验差

1.2 实现思路

通过Redis来缓存菜品数据,减少数据库查询操作。

  • redis基于内存速度快,如果是查询数据库是基于io操作性能低。
  • 业务逻辑:先去查询缓存如果有数据则直接使用缓存中的数据,如果没有在去查询数据库中的数据,之后将查询到的数据库中的数据保存在redis缓存中。
  • 问题:使用redis作为缓存来保存菜品中的数据,系统中可能有很多菜品数据,那么这个时候具体是如何保存到redis中呢???
    • 是所有的菜品保存一份缓存数据,还是每一个菜品都来保存一份缓存数据???

在这里插入图片描述

缓存具体逻辑分析:

  • 每个分类下的菜品保存一份缓存数据
    在这里插入图片描述

    • 由页面原型可知,它是点击一个分类然后展示这个分类下所有的菜品数据,也就是说这个小程序展示这些菜品的力度是根据分类来展示,所以说这个地方要缓存菜品数据应该是每个分类下面的这些菜品,对应的就是一份缓存数据,有多少份分类就对应多少份缓存数据。
    • redis是键值对结构,要保存一份缓存数据就是一对k-v结构,那么这个k该如何设计???
      在这里插入图片描述
      • 每个分类下保存一份菜品数据,跟这个分类有关系,所以可以使用分类的id来作为缓存的key,value是分类下面保存的具体菜品数据,这些菜品数据可以使用String字符串来保存。
      • K:使用一个统一前缀然后拼接上一个动态的分类id。
      • V:是个字符串,是分类下面对应的这些菜品,它实际上是个集合然后需要把这个集合转成一个字符串存到redis中。
      • 这个字符串指的是redis中的数据类型,实际上跟java中的数据类型并不是完全对应的,也就是说Java中的任何一个对象都可以转成redis中的String字符串来进行存储,
  • 数据库中菜品数据有变更时清理缓存数据,防止出现数据不一致。

    • 举例:在商家管理端把菜品的价格给改了,如果没有清理对应的缓存那么这个小程序里面展示的还是原来的价格,因为这个地方菜品是从缓存里面获取出来的,而这个缓存并没有同步更新过去。所以说当数据库中的菜品数据有变更时需要及时的把缓存中的数据给清理掉,要不然数据就不一致了。

1.3 代码开发

修改用户端接口 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) {
        //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);
        }


        //4.如果不存在,查询数据库,将查询到的数据放入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);
    }

}

为了保证数据库Redis中的数据保持一致,修改管理端接口 DishController 的相关方法,加入清理缓存逻辑。

需要改造的方法:

  • 新增菜品
    • 新增后分类下面多了一个菜品,原先是没有这个菜品的,所以需要清理缓存。
  • 修改菜品
    • 修改一个后台菜品的价格为80 修改的是数据库中的数据,redis中的数据没有变化还是70,此时小程序页面显示的查询redis中的数据为70,显然不合适。
  • 批量删除菜品
    • 后台删除一个菜品小程序页面就不应该在显示,此时查询的是redis中的数据任然可以显示,显然不合适。
  • 起售、停售菜品
    • 起售状态修改为停售 不能在查询出停售状态下的数据,所以需要清理缓存中的数据。同样停售状态修改为起售状态,此时需要查询出缓存中的数据,所以需要清理缓存。

注意:修改的是管理端admin接口 DishController里面的方法,因为这些操作是在管理端才能进行,用户端是修改不了这些数据的。

抽取清理缓存的方法:

在管理端DishController中添加
在这里插入图片描述

	@Autowired
    private RedisTemplate redisTemplate;
    /**
     * 抽取清理缓存的方法
     *  只在当前类中使用,所以私有的就可以了。
     * @param pattern
     */
    private void cleanCache(String pattern){
        Set keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }

调用清理缓存的方法,保证数据一致性:

1). 新增菜品优化

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

        //清理缓存数据:
        //注意不是一次性清除redis中的所有缓存数据,而是哪一份缓存数据受影响,那我们
        //   清理哪一份缓存数据就可以了。当前新增的这个菜品所属的分类这个key受到影响。
        String key = "dish_" + dishDTO.getCategoryId();
        cleanCache(key);

        return Result.success();
    }

2). 菜品批量删除优化

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

        //将所有的菜品缓存数据清理掉,所有以dish_开头的key:
        //批量删除有可能删除多个菜品,而这多个菜品可能属于同一个分类,也有可能是某些不同
        //  分类下面的菜品,也就是说可能会影响到多个key,具体影响几个key只能查询数据库才能知道,
        //  其实不需要那么复杂,只要你批量删除之后 直接把所有的缓存数据也就是dish_开头的缓存
        //  数据都清理掉就可以了。
        //注意:删除的时候不识别通配符,不能直接根据key删除,所以需要先把key查出来在进行删除(redisTemplate.keys(pattern))
        //删除是支持集合collection的 即一次性把所有的key都删除,所以这个地方就没必要遍历set集合一个个的来删除了。
        cleanCache("dish_*");

        return Result.success();
    }

3). 修改菜品优化

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

        //将所有的菜品缓存数据清理掉,所有以dish_开头的key:
        //同样修改的逻辑也比较复杂:如果修的是名称价格这些普通属性,那么只需要修改一个对应的key即可,
        //                     如果修改的是分类,比如鸡蛋汤给它换一个分类,此时影响的是2个
        //                     分类中的数据,原先分类的菜品少一个,现在分类的菜品多一个。
        //              总结:修改菜品有可能是影响1份数据 也有可能影响2份数据。
        //    解决:修改操作并不是常规操作 一般是很少修改 所以没有必要吧代码写的太过复杂,去判断
        //        下有没有修改这个分类 如果修改类分类具体是那2份数据受到影响,还要一个个的去查询 太过繁琐。
        //        这个地方统一删除所有的缓存数据就可以了。
        cleanCache("dish_*");

        return Result.success();
    }

4). 菜品起售停售优化

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

        //将所有的菜品缓存数据清理掉,所有以dish_开头的key
        //   想要精确清理只清理某一个key:根据菜品的id把对应的菜品数据查询出来,菜品里面就有
        //   分类的id,之后动态的把key构造出来 然后清理某一个key就可以了。但是这样写需要
        //   额外的去查询数据 就有的得不偿失了。所以这里同样是删除所有的缓存数据。
        cleanCache("dish_*");

        return Result.success();
    }

1.4 功能测试

可以通过如下方式进行测试:

  • 查看控制台sql
  • 前后端联调
  • 查看Redis中的缓存数据

加入缓存菜品修改两个功能测试为例,通过前后端联调方式,查看控制台sql的打印和Redis中的缓存数据变化。

1). 加入缓存

当第一次查询某个分类的菜品时,会从数据为中进行查询,同时将查询的结果存储到Redis中,在后绪的访问,若查询相同分类的菜品时,直接从Redis缓存中查询,不再查询数据库。

登录小程序:选择蜀味牛蛙(id=17)

在这里插入图片描述

查看控制台sql:有查询语句,说明是从数据库中进行查询

在这里插入图片描述

查看Redis中的缓存数据:说明缓存成功

在这里插入图片描述

再次访问:选择蜀味牛蛙(id=17)

在这里插入图片描述

说明是从Redis中查询的数据。

2). 菜品修改

当在后台修改菜品数据时,为了保证Redis缓存中的数据和数据库中的数据时刻保持一致,当修改后,需要清空对应的缓存数据。用户再次访问时,还是先从数据库中查询,同时再把查询的结果存储到Redis中,这样,就能保证缓存和数据库的数据保持一致。

进入后台:修改蜀味牛蛙分类下的任意一个菜品,当前分类的菜品数据已在Redis中缓存

在这里插入图片描述

修改:

在这里插入图片描述

查看Redis中的缓存数据:说明修改时,已清空缓存

在这里插入图片描述

用户再次访问同一个菜品分类时,需要先查询数据库,再把结果同步到Redis中,保证了两者数据一致性。

其它功能测试步骤基本一致,自已测试即可。

1.5 代码提交

在这里插入图片描述

后续步骤和其它功能代码提交一致,不再赘述。

2. 缓存套餐

2.1 Spring Cache

2.1.1 介绍

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:

  • EHCache
  • Caffeine
  • Redis(常用)

起步依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>  		            		       	 <version>2.7.3</version> 
</dependency>
2.1.2 常用注解

在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:

注解说明
@EnableCaching开启缓存注解功能,通常加在启动类上
@Cacheable在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 (既可以放也可以存缓存数据
@CachePut将方法的返回值放到缓存中(只能放缓存数据
@CacheEvict将一条或多条数据从缓存中删除

在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。

:SpringCache想要切换底层具体使用什么缓存来实现,只需要添加对应的依赖坐标即可(如:spring-boot-starter-data-redis),不需要做任何配置,这个时候SpringCache就会使用redis来作为我们真正的缓存实现,说白了就会把我们的数据呢把它保存到redis当中。

2.1.3 入门案例

1). 环境准备

导入基础工程:底层已使用Redis缓存实现

基础环境的代码,在我们今天的资料中已经准备好了, 大家只需要将这个工程导入进来就可以了。导入进来的工程结构如下:

在这里插入图片描述
主要依赖:
在这里插入图片描述

WebMvcConfiguration配置类:生成swagger接口文档配置

package com.itheima.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    /**
     * 生成swagger接口文档配置
     * @return
     */
    @Bean
    public Docket docket(){
        log.info("准备生成接口文档...");

        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("接口文档")
                .version("2.0")
                .description("接口文档")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.itheima.controller"))
                .paths(PathSelectors.any())
                .build();

        return docket;
    }

    /**
     * 设置静态资源映射
     * @param registry
     */
    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/");
    }
}

User实体类:

package com.itheima.entity;

import lombok.Data;
import java.io.Serializable;

@Data
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String name;

    private int age;

}

UserController:增、删、查

package com.itheima.controller;

import com.itheima.entity.User;
import com.itheima.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    //这个地方主要是来学习springCache,为了简化代码没有写service,直接注入的是mapper层。
    @Autowired
    private UserMapper userMapper;

    //插入一个用户数据
    @PostMapping
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

    //根据主键查询
    @GetMapping
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }

    //根据主键删除一条数据
    @DeleteMapping
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

    //删除所有数据
	@DeleteMapping("/delAll")
    public void deleteAll(){
        userMapper.deleteAll();
    }



}

UserMapper:

package com.itheima.mapper;

import com.itheima.entity.User;
import org.apache.ibatis.annotations.*;

@Mapper
public interface UserMapper{

    @Insert("insert into user(name,age) values (#{name},#{age})")
    @Options(useGeneratedKeys = true,keyProperty = "id")
    void insert(User user);

    @Delete("delete from user where id = #{id}")
    void deleteById(Long id);

    @Delete("delete from user")
    void deleteAll();

    @Select("select * from user where id = #{id}")
    User getById(Long id);
}

数据库准备:

创建名为spring_cache_demo数据库,将springcachedemo.sql脚本直接导入数据库中。

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
);

在这里插入图片描述

引导类上加@EnableCaching:
在这里插入图片描述

package com.itheima;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@Slf4j
@SpringBootApplication
@EnableCaching//开启缓存注解功能
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class,args);
        log.info("项目启动成功...");
    }
}

2). @CachePut注解

@CachePut 说明:

​ 作用: 将方法返回值,放入缓存

​ value: 缓存的名称, 每个缓存名称下面可以有很多key

​ key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

在save方法上加注解@CachePut

当前UserController的save方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,用法如下:

在这里插入图片描述

    /**
     * 插入一个用户数据
     * CachePut:将方法返回值放入缓存
     *	 value:缓存的名称(一般起名和业务有关),每个缓存名称下面可以有多个key
     *	 key:缓存的key
     *
     * 如果使用Spring Cache缓存数据,底层使用redis具体存储,redis为k-v结构,
     *   那么这个key的生成方式为:value::key
     *   即:@CachePut注解中设置的value值::@CachePut中设置的key值
     *   @CachePut中设置的key值不能写死,不然每一个用户计算出来的key都是固定的,这样
     *   就没有意义了,我们是希望每一个用户保存到数据库的同时需要对应自己的一个缓存数据,
     *   如果注解中的key写死导致整个redis算出的key也是固定的,那么后面的数据就会覆盖掉前面的数据。
     *
     * 要求:保证每一次插入一个用户的同时他计算出来的这个key都是动态的,所以这个地方写的应该是
     *      用户的唯一标识(因为数据库表中每一条数据的主键值都是不同的),那么如何在这个@CachePut
     *      注解中拿到这条数据的主键值(即:用户的id)呢???
     *   答:可以使用Spring提供的EL表达式  #user.id
     *         以#开头
     *         user和形参的参数名保持一致。
     *
     * EL表达式的不同写法:
     *      1.key = "#user.id"   从请求参数中获取到user(推荐,比较直观)
     *      2.key = "#result.id" 从返回值结果获取到user,因为这个方法的返回值就是user对象。
     *      3.key = "#p0.id"     p0代表第一个形参,p1代表第二个形参,这个地方只有一个形参user所以
     *                              p0恰好可以获取到。
     *      4.key = "#a0.id"     a0代表的也是第一个参数。
     *      5.key = "#root.args[0].id"   代表的也是方法中的第一个参数user。
     *
     *   这个id是数据插入到数据库表中自动生成的,在sql中通过useGeneratedKeys和keyProperty属性将获取到的主键值赋值给user对象的id属性,
     *        所以此时通过user.id就可以获取到值。
     *
     */
    @PostMapping
    @CachePut(value = "userCache", key = "#user.id")
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

说明:key的写法如下

#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;

#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;

#p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;

#a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;

#root.args[0].id:#root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;

启动服务,通过swagger接口文档测试,访问UserController的save()方法

因为id是自增,所以不需要设置id属性

注意:此时端口号是8888,所以swagger接口文档地址为http://localhost:8888/doc.html

在这里插入图片描述

查看user表中的数据

在这里插入图片描述

查看Redis中的数据

在这里插入图片描述

3). @Cacheable注解

@Cacheable 说明:

​作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,通过反射调用方法并将方法返回值放到缓存中,此时使用的是数据库中的数据,再次查询相同id的数据时,直接从redis中直接获取,不再查询数据库。

​ value: 缓存的名称,每个缓存名称下面可以有多个key

​ key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

在getById上加注解@Cacheable

    /**
     * 根据主键查询
     * Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,
     *           调用方法并将方法返回值放到缓存中
     * value:缓存的名称,每个缓存名称下面可以有多个key
     * key:缓存的key。
     *
     * redis中key的生成方式同样是value::key
     * 注意:请求参数传的就是id,所以这里直接就可以获取到id值key="#id",
     *      但是他没有result的写法key = "#result.id"(错误,查看源码可知)
     */
    @GetMapping
    @Cacheable(cacheNames = "userCache",key="#id")
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }

重启服务,通过swagger接口文档测试,访问UserController的getById()方法

第一次访问,会请求我们controller的方法,查询数据库。后面再查询相同的id,就直接从Redis中查询数据,不用再查询数据库了,就说明缓存生效了。

提前在redis中手动删除掉id=1的用户数据

在这里插入图片描述

查看控制台sql语句:说明从数据库查询的用户数据

在这里插入图片描述

查看Redis中的缓存数据:说明已成功缓存

在这里插入图片描述

再次查询相同id的数据时,直接从redis中直接获取,不再查询数据库。

4). @CacheEvict注解

@CacheEvict 说明:

​ 作用: 清理指定缓存

说明:如果数据库中的数据已经删掉了,那么对应的这个缓存数据也应该清理掉。

​ value: 缓存的名称,每个缓存名称下面可以有多个key

​ key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

原理:加入此注解之后由这个SpringCatche框架,为我们当前这个controller来创建代理对象,它是在代理对象中动态的把这个key给计算出来,然后通过代理对象操作redis最终把缓存数据给它清理掉。

在 delete 方法上加注解@CacheEvict

    /**
     * 根据主键删除一条数据
     * redis中key的生成方式同样是value::key
     */
    @CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据(userCache::5)
    @DeleteMapping
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

    /**
     * 删除所有数据
     * redis中key的生成方式同样是value::key
     * allEntries = true:表示所有的键值对
     * @CacheEvict(cacheNames = "userCache",allEntries = true):此时删除的是userCache下面所有的键值对
     */
	@DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
    public void deleteAll(){
        userMapper.deleteAll();
    }

重启服务,通过swagger接口文档测试,访问UserController的deleteAll()方法

在这里插入图片描述

查看user表:数据清空

在这里插入图片描述

查询Redis缓存数据

在这里插入图片描述

2.2 实现思路

实现步骤:

1). 导入Spring Cache和Redis相关maven坐标

2). 在启动类上加入@EnableCaching注解,开启缓存注解功能

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

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

2.3 代码开发

按照上述实现步骤:

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注解,开启缓存注解功能

package com.sky;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching //开启缓存注解功能
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}

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

在这里插入图片描述

	/**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    @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注解

在这里插入图片描述

	/**
     * 新增套餐
     *
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }
	/**
     * 批量删除套餐
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result delete(@RequestParam List<Long> ids) {
        setmealService.deleteBatch(ids);
        return Result.success();
    }
	/**
     * 修改套餐
     *
     * @param setmealDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result update(@RequestBody SetmealDTO setmealDTO) {
        setmealService.update(setmealDTO);
        return Result.success();
    }

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

2.4 功能测试

通过前后端联调方式来进行测试,同时观察redis中缓存的套餐数据。和缓存菜品功能测试基本一致,不再赘述。

启动项目:

  • 测试根据分类id查询套餐的方法,在方法加的是@Cacheable(cacheNames = "setmealCache",key = "#categoryId")
    在这里插入图片描述

    • 点击人气套餐此时缓存中没有数据,所以使用的是数据库中的数据,之后把获取到的数据(方法的返回值)放到redis缓存中。
      在这里插入图片描述
    • 后台执行了sql说明数据是从数据库中获取到的。
      在这里插入图片描述
    • redis中保存了方法的返回值
      在这里插入图片描述
  • 测试套餐起售停售方法中添加的@CacheEvict(cacheNames = "setmealCache",allEntries = true)注解

    • 修改套餐状态,此时应该删除redis中缓存的数据。
      在这里插入图片描述
    • 可以看到之前缓存中保存方法的返回值数据已经删除
      在这里插入图片描述

2.5 代码提交

在这里插入图片描述

后续步骤和其它功能代码提交一致,不再赘述。

3. 添加购物车(业务逻辑)

3.1 需求分析和设计

3.1.1 产品原型

用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车; 对于套餐来说,可以直接点击+将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。

生活中的购物车:用于暂时存放所选商品的一种手推车

效果图:

在这里插入图片描述

总结:

  • 菜品:
    • 情况1:菜品没有设置口味数据,直接点击+将菜品添加到购物车当中。
    • 情况2:菜品设置了口味数据,需要先点击选择规格来选择对应的口味,之后在点击加入购物车按钮,这样就可以把菜品数据添加到购物车当中。
    • 如果这个商品已经被加入到了购物车,你还想添加相同的商品的时候,可以在购物车当中点击+,这样就可以快速的来添加一个相同的商品到购物车当中。
  • 套餐:直接点击+就可以把套餐添加到购物车当中。
3.1.2 接口设计

通过上述原型图,设计出对应的添加购物车接口。

在这里插入图片描述

说明

  • 添加购物车时,有可能添加菜品,也有可能添加套餐。故传入参数要么是菜品id,要么是套餐id。
  • 菜品可能还有口味数据,所以还需要包含口味数据的参数。
  • 对于某一次添加购物车来说,要么添加的就是菜品要么添加的就是套餐,不可能你同时添加的又是菜品又是套餐,这样的话是不合理的,所以这3个参数是非必须的。
  • 但是对于某一次添加购物车这个业务来说,菜品id或者套餐id必须提交过来一个。
3.1.3 表设计(冗余字段)

用户的购物车数据,也是需要保存在数据库中的,购物车对应的数据表为shopping_cart表,具体表结构如下:

数据库设计分析:

  • 作用:暂时存放所选商品的地方
  • 选的什么商品
  • 每个商品都买了几个
  • 不同用户的购物车需要区分开
    • 不可能是让两个用户共用一个购物车。
字段名数据类型说明备注
idbigint主键自增
namevarchar(32)商品名称冗余字段
imagevarchar(255)商品图片路径冗余字段
user_idbigint用户id逻辑外键
dish_idbigint菜品id逻辑外键
setmeal_idbigint套餐id逻辑外键
dish_flavorvarchar(50)菜品口味
numberint商品数量
amountdecimal(10,2)商品单价冗余字段
create_timedatetime创建时间

说明1:

  • 购物车数据是关联用户的,在表结构中,我们需要记录,每一个用户的购物车数据是哪些
  • 菜品列表展示出来的既有套餐,又有菜品,如果用户选择的是套餐,就保存套餐ID(setmeal_id),如果用户选择的是菜品,就保存菜品ID(dish_id)
  • 对同一个菜品/套餐,如果选择多份不需要添加多条记录,增加数量number即可

说明2:分析3个特殊的字段name、image、amount

  • 特殊在这3个字段不是必须的,因为我通过dish_id或者setmeal_id就可以查询出来相应的这个商品名称、图片路径、商品单价,这个时候这3个字段称作冗余字段。
  • 冗余字段:反复出现的重复字段,eg:菜品表当中已经有了菜品的名称、图片路径、价格字段了,在当前表shopping_cart又出现了一遍,这就是冗余字段
  • 为什么要这样设计?????
    • 因为通过设计这些冗余字段就可以提高我们的查询速度,eg:在展示购物车数据的时候需要展示商品的名称、 价格 、图片,如果没有这些冗余字段在查询数据的时候除了要查询我们这个购物车表,还需要联合去查询菜品表或者套餐表,相当于是2张表的连接查询,现在有了冗余字段就变成了单表查询,我们直接查询这个购物车表就可以了,所以查询速度就会快很多。
    • 所以在有些场景下会故意的设计一些冗余字段,来提高查询速度.
    • 但是要注意冗余字段是不能大量使用的,而且这些冗余字段相对来说呢应该是比较稳定不能经常变化的,比如:说咱们这个地方的商品名称啊 图片啊 价格啊 ,相对来说是比较稳定并不是经常变化的,所以说咱们这个地方使用冗余字段相对来说比较合理。

3.2 代码开发

3.2.1 DTO设计

根据添加购物车接口的参数设计DTO:

在这里插入图片描述

在sky-pojo模块,ShoppingCartDTO.java已定义

在这里插入图片描述

package com.sky.dto;

import lombok.Data;
import java.io.Serializable;

@Data
public class ShoppingCartDTO implements Serializable {

    private Long dishId;
    private Long setmealId;
    private String dishFlavor;

}
3.2.2 Controller层

根据添加购物车接口创建ShoppingCartController:

购物车属于用户端的操作,所以在user包下创建。

在这里插入图片描述

package com.sky.controller.user;


import com.sky.dto.ShoppingCartDTO;
import com.sky.result.Result;
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")
@Slf4j
@Api(tags = "C端-购物车接口")
public class ShoppingCartController {

    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 添加购物车
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/add")
    @ApiOperation("添加购物车")
    public Result<String> add(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("添加购物车:{}", shoppingCartDTO);
        shoppingCartService.addShoppingCart(shoppingCartDTO);//后绪步骤实现
        return Result.success();
    }
}
3.2.3 Service层接口

创建ShoppingCartService接口:

package com.sky.service;

import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.ShoppingCart;
import java.util.List;

public interface ShoppingCartService {

    /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}
3.2.4 Service层实现类

创建ShoppingCartServiceImpl实现类,并实现add方法:

package com.sky.service.impl;

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;

    /**
     * 添加购物车
     * 业务分析:购物车中添加了2份商品数据,那么体现在购物车表里面并不是2条数据,
     *         因为购物车表里面有一个number字段,代表的是商品的数量,也就是说
     *         如果是相同的商品只需要把它的这个数量加1就可以了。
     *
     * 总结:当我们添加购物车的时候首先需要判断一下,当前添加到购物车的这个商品它是否在购物
     *      车当中已经存在了,如果存在只需要执行一个修改操作把这个数量加1,如果不存在 在执行一个
     *      insert插入操作来插入一条数据。
     *
     *  问题:不同的用户需要有自己的购物车,通过user_id字段来体现,所以说在查询购物车中的这个商品
     *       的时候,需要把这个用户的id作为条件去查询。
     *
     *
     * @param shoppingCartDTO
     */
    @Override
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        //构造ShoppingCart封装请求参数,因为它包含用户的id。
        ShoppingCart shoppingCart = new ShoppingCart();
        //对象属性拷贝:dishId、setmealId、dishFlavor
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        //设置用户id:用户端每次发送请求都会携带token,通过JwtTokenUserInterceptor拦截器去解析
        //         这个token,解析出来的用户id和ThreadLocal进行绑定,之后在这个地方通过ThreadLocal取出来即可。
        Long userid = BaseContext.getCurrentId();
        shoppingCart.setUserId(userid);


        /**
         * 1.判断当前加入到购物车中的商品是否已经存在了:
         *    如何判断是否存在了,需要去查询,那要查询的话是根据什么条件去查询呢???
         *    购物车添加的是套餐:根据套餐id和用户id,因为不同的用户有自己的购物车数据,需要通过user_id来区分出来。
         *        eg:select * from shopping_cart where user_id = ? and setmeal_id = xxx;
         *           结果有可能查出来有可能查不出来,之后按照查不出出来的情况分别判断。
         *    购物车添加的是菜品:除了菜品id、用户id 还需要根据口味数据查询,因为对于同一个菜品来说如果它的口味
         *                    不一样的话,在购物车里面也是不同的2条数据,你就不能简单地把这个数量给加1了,所以
         *                    口味也是一个查询字段。
         *       eg:select * from shopping_cart where user_id = ? and dish_id = xxx and dish_flavor = xx;
         *
         *    不需要分别写这2条sql,只需要使用一条动态的sql来动态拼接条件即可。
         *
         */
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);

        if (shoppingCartList != null && shoppingCartList.size() == 1) {
            //2.如果已经存在,就更新数量,数量加1
            //注意:当前方法返回值虽然是一个list集合,但是按照当前设置的这些条件来说,实际上是不可能查出来多条数据的。
            //     根据用户id再加上菜品id或者套餐id来查询,对于相同的商品只需要修改数量就可以了,不会说在重新插入一条数据。
            //     所以说按照当前这些条件去查询的话只有2种结果:查不到或者查到之后只有一条数据。
            shoppingCart = shoppingCartList.get(0);//取出来第一条数据 也是唯一的一条数据。
            shoppingCart.setNumber(shoppingCart.getNumber() + 1);//在原先的数量基础上加1,之后执行update语句。
            // update shopping_cart set number = ? where id = ?
            //因为是从数据库里面查出来的,所以cart里面一定是有id的。
            shoppingCartMapper.updateNumberById(shoppingCart);
        } else {
            //3.如果不存在,需要插入一条购物车数据
            //思路分析:ShoppingCart实体类插入数据时除了上面设置的参数,还需要name名称、价格amount、图片
            //        的路径image,这几个参数前端并没有给我们提交过来 所以自己手动查询出来。
            //   情况1:如果提交的是一个菜品,需要在菜品表里面去查询 菜品的名称 价格 和图片路径。
            //   情况2:如果提交的是一个套餐,需要在套餐表里面去查询 套餐的名称 价格 和图片路径。
            // 所以说呢我们在这个地方啊需要做什么事情啊,先来判断一下你这一次添加到购物车中的这个商品,具体是
            //     一个菜品还是一个套餐,因为只有知道了是一个菜品还是一个套餐才能知道具体去查询那个表。
            //     只需要通过获取shoppingCartDTO的菜品id或套餐id来判断是否为空,就可以知道这一次添加到
            //      购物车中的商品是菜品还是套餐。

            //4.判断当前添加到购物车的是菜品还是套餐
            Long dishId = shoppingCartDTO.getDishId();
            if (dishId != null) {
                //添加到购物车的是菜品
                //dish_id(菜品id)不为空说明添加的就是菜品,不可能是套餐因为之前说过要么添加的是菜品要么是套餐,
                //   你不可能某一次添加的购物车既是菜品又是套餐。
                Dish dish = dishMapper.getById(dishId);
                shoppingCart.setName(dish.getName());
                shoppingCart.setImage(dish.getImage());
                shoppingCart.setAmount(dish.getPrice());
            } else {
                //添加到购物车的是套餐
                //这个地方不用再判断了,因为进到了else说明这个dishId一定为空,dishId为空说明
                //    这个SetmealId一定不为空。
                Setmeal setmeal = setmealMapper.getById(shoppingCartDTO.getSetmealId());
                shoppingCart.setName(setmeal.getName());
                shoppingCart.setImage(setmeal.getImage());
                shoppingCart.setAmount(setmeal.getPrice());
            }
            shoppingCart.setNumber(1);//设置数量,固定第一次插入就是1.
            shoppingCart.setCreateTime(LocalDateTime.now());//创建时间
            shoppingCartMapper.insert(shoppingCart);
        }
    }
}

3.2.5 Mapper层(动态sql上面是属性名 下面是字段名)

创建ShoppingCartMapper接口:

package com.sky.mapper;

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 {
    /**
     * 条件查询:
     *      ShoppingCartDTO中没有用户的id,所以不能使用ShoppingCartDTO传递参数,
     *      而是使用shoppingCart这个实体类来提交参数。
     * @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, 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);

}

ShoppingCart实体类:

package com.sky.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 购物车
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShoppingCart implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //名称
    private String name;

    //用户id
    private Long userId;

    //菜品id
    private Long dishId;

    //套餐id
    private Long setmealId;

    //口味
    private String dishFlavor;

    //数量
    private Integer number;

    //金额
    private BigDecimal amount;

    //图片
    private String image;

    private LocalDateTime createTime;
}

创建ShoppingCartMapper.xml:

<?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">
    <!--注意:动态sql里面,上面这个判断条件写的是实体类中的属性名,
            下面的sql写的是表中的字段名。-->
    <select id="list" parameterType="ShoppingCart" resultType="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>

3.3 功能测试

进入小程序,添加菜品

在这里插入图片描述

加入购物车,查询数据库

在这里插入图片描述

因为现在没有实现查看购物车功能,所以只能在表中进行查看。

在前后联调时,后台可通断点方式启动,查看运行的每一步。

3.4 代码提交

在这里插入图片描述

后续步骤和其它功能代码提交一致,不再赘述。

4. 查看购物车(不需要传递user_id)

4.1 需求分析和设计

4.1.1 产品原型

当用户添加完菜品和套餐后,可进入到购物车中,查看购物中的菜品和套餐。

在这里插入图片描述

4.1.2 接口设计

说明:这个地方不需要提交任何参数过去,因为我们当前查询的是用户的所有购物车数据。

问题:既然你要查询当前这个微信用户的购物车数据,那要不要把微信用户的id给传过去呢????
答:不需要,因为我们每一次业务操作的时候呢,都会在请求头里面携带一个token过去,后端通过解析这个token就可以拿到这个用户id,所以这个user_id不传过去也是没有问题的。

在这里插入图片描述
在这里插入图片描述

4.2 代码开发

4.2.1 Controller层

在ShoppingCartController中创建查看购物车的方法:

在这里插入图片描述

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

在ShoppingCartService接口中声明查看购物车的方法:

	/**
     * 查看购物车
     * @return
     */
    List<ShoppingCart> showShoppingCart();
4.2.3 Service层实现类

在ShoppingCartServiceImpl中实现查看购物车的方法:

    /**
     * 查看购物车
     * @return
     */
    @Override
    public List<ShoppingCart> showShoppingCart() {
        //查询某个用户的购物车数据,所以需要传递一个user_id
        //获取当前这个微信用户的id
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = ShoppingCart.builder()
                    .userId(userId)
                    .build();
        List<ShoppingCart> list =  shoppingCartMapper.list(shoppingCart);
        return list;
    }
4.2.4 Mapper以及Mapper.xml

用的是之前添加购物车时写好的方法:

在这里插入图片描述

在这里插入图片描述

4.3 功能测试

当进入小程序时,就会发起查看购物车的请求

在这里插入图片描述

点击购物车图标

在这里插入图片描述

测试成功。

4.4 代码提交

在这里插入图片描述

后续步骤和其它功能代码提交一致,不再赘述。

5. 清空购物车

5.1 需求分析和设计

5.1.1 产品原型

当点击清空按钮时,会把购物车中的数据全部清空。

在这里插入图片描述

5.1.2 接口设计

在这里插入图片描述

5.2 代码开发

和查看购物车类似,同样不需要提交参数,因为在后端实际上能够知道当前用户的id。

5.2.1 Controller层

在ShoppingCartController中创建清空购物车的方法:

在这里插入图片描述

	/**
     * 清空购物车商品
     * @return
     */
    @DeleteMapping("/clean")
    @ApiOperation("清空购物车商品")
    public Result<String> clean(){
        shoppingCartService.cleanShoppingCart();
        return Result.success();
    }
5.2.2 Service层接口

在ShoppingCartService接口中声明清空购物车的方法:

	/**
     * 清空购物车商品
     */
    void cleanShoppingCart();
5.2.3 Service层实现类

在ShoppingCartServiceImpl中实现清空购物车的方法:

    /**
     * 清空购物车商品:你不能删除别人的数据只能删除自己的购物车数据,
     *              所以需要有user_id作为删除条件。
     */
    @Override
    public void cleanShoppingCart() {
        //获取到当前用户的id
        Long userId = BaseContext.getCurrentId();
        shoppingCartMapper.deleteByUserId(userId);
    }
5.2.4 Mapper层

在ShoppingCartMapper接口中创建删除购物车数据的方法:

	/**
     * 根据用户id删除购物车数据
     *
     * @param userId
     */
    @Delete("delete from shopping_cart where user_id = #{userId}")
    void deleteByUserId(Long userId);

5.3 功能测试

进入到购物车页面

在这里插入图片描述

点击清空

在这里插入图片描述

查看数据库中的数据

在这里插入图片描述

说明当前用户的购物车数据已全部删除。

5.4 代码提交

在这里插入图片描述

后续步骤和其它功能代码提交一致,不再赘述。

6. 删除购物车中一个商品

6.1 产品原型

在这里插入图片描述

6.2 接口设计

在这里插入图片描述

6.3 数据模型

shopping_cart表:

在这里插入图片描述

6.4 代码开发

6.4.1 ShoppingCartController

在这里插入图片描述

/**
     * 删除购物车中一个商品
     * @param shoppingCartDTO
     * @return
*/
@PostMapping("/sub")
@ApiOperation("删除购物车中一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO){
    log.info("删除购物车中一个商品,商品:{}", shoppingCartDTO);
    shoppingCartService.subShoppingCart(shoppingCartDTO);
    return Result.success();
}
6.4.2 ShoppingCartService
/**
     * 删除购物车中一个商品
     * @param shoppingCartDTO
*/
void subShoppingCart(ShoppingCartDTO shoppingCartDTO);
6.4.3 ShoppingCartServiceImpl
/**
     * 删除购物车中一个商品
     * @param shoppingCartDTO
*/
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
   //对象拷贝封装请求参数
    ShoppingCart shoppingCart = new ShoppingCart();
    BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
    //设置查询条件,查询当前登录用户的购物车数据
    shoppingCart.setUserId(BaseContext.getCurrentId());
    //此时根据用户id,菜品id(可能要口味数据)或者套餐id查询的结果要么是1条要么是没有
    //   因为购物车添加相同的商品只是修改数量number字段,而不是真正添加一条数据。
    List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);

    if(list != null && list.size() > 0){
    //不为空说明购物车有数据,因为只有1条所以直接获取第一个下标即可获得对象数据
        shoppingCart = list.get(0);

     //获得购物车实体类的数量字段:
     //   1.如果数量为1,说明此时只有一条数据,此时在减去1就是删除数据库的这条数据.
     //   2.如果数量不是1,说明此时有多条数据,多条数据只是number发生了变化实际上还是一条数据,所以此时的
     //     删除只需要减少number数量字段即可。
        Integer number = shoppingCart.getNumber();
        if(number == 1){
            //当前商品在购物车中的份数为1,直接删除当前记录
            shoppingCartMapper.deleteById(shoppingCart.getId());
        }else {
            //当前商品在购物车中的份数不为1,修改份数即可
            shoppingCart.setNumber(shoppingCart.getNumber() - 1);
            shoppingCartMapper.updateNumberById(shoppingCart);
        }
    }
}
6.4.4 ShoppingCartMapper
/**
     * 根据id删除购物车数据
     * @param id
*/
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);

6.5 功能测试

  • 重启项目,在购物车中添加好测试数据:
    在这里插入图片描述
    在这里插入图片描述

  • 只有一条数据的删除:
    在这里插入图片描述
    在这里插入图片描述

  • 原先有多条数据的删除:
    在这里插入图片描述
    在这里插入图片描述

6.6 代码提交

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐