苍穹外卖 —— Spring Cache和购物车功能开发

一、前言

上一期我们学习了redis的基本使用,但是我们会发现,这样操纵缓存还是挺麻烦的,原因就是没有使用注解,是直接在类中写的逻辑,这样会造成代码维护困难,耦合度太高,所以这一期我们将接触一个新的缓存控制框架,并且用这个框架实现客户端的购物车功能。

二、 Spring cache

为了测试各个注解的作用,我们这里重新创建一个项目,单独说一下这个框架。

我们尽可能简化这个项目,去掉了Service层,直接用controller调用Mapper:

java 复制代码
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private Long id;

    private String name;

    private int age;
java 复制代码
@SpringBootApplication
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class,args);
    }
}
java 复制代码
@RestController
@RequestMapping("/user")

public class UserController {

    @Autowired
    private UserMapper userMapper;

    @PostMapping
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

    @DeleteMapping
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

	@DeleteMapping("/delAll")
    public void deleteAll(){
        userMapper.deleteAll();
    }

    @GetMapping
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }

}
java 复制代码
@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);
}

1.@EnableCaching

这个注解是用于开启缓存注解的,和@EnableTransactionManagement相似(这个是开启事务控制的),都是用在启动类上的注解,这个没什么好说的,后面的注解都是写在Controller的方法上的。

java 复制代码
@SpringBootApplication
@EnableCaching//开启缓存注解
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class,args);
    }
}

2.@CachePut

这个注解是用于将方法返回值添加进缓存的注解。

参数:

1.cacheNames:是用于给缓存创建一个大目录并作为缓存的前缀名,如下图:

2.key:指定存入缓存的后缀名(通常是id),我们在这里会使用SpringEL(spring表达式),需要注意,我们传入的通常都是方法参数的id,有几种表示方式,如下图,但是第一种是最容易维护的。

java 复制代码
@PostMapping
    @CachePut(cacheNames = "userCache",key = "#user.id") //荐,如果使用Spring cache缓存数据,key的生成:userCache::user.id,这里用springEL会将参数的值和key绑定,实现动态拼接
    //@CachePut(cacheNames = "userCache",key = "#result.id")//对象导航,但是这里的result是从返回值中取出来的
    //@CachePut(cacheNames = "userCache",key = "#p0.id")//表示第一个参数的id
    //@CachePut(cacheNames = "userCache",key = "#a0.id")//同上
    //@CachePut(cacheNames = "userCache",key = "#root.args[0].id")//同上
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

3.@Cacheable

这个注解是用于调用缓存的,如果有这个缓存就直接从缓存调用,如果没有,就从数据库调用,然后存到缓存中去。

这里会存在一个问题,就是如果缓存中没有,数据库中也没有,那还会被存到缓存中去吗?答案是会,但是存进去的是null(因为数据库中没有这个数据),这个现象叫做缓存污染,这样会导致缓存中出现无用信息,基于这个问题,@Cacheable会有一个参数:unless,加了这个参数就可以避免这种情况发生了:

复制代码
unless = "#result == null"
java 复制代码
@GetMapping
    @Cacheable(cacheNames = "userCache",key = "#id")//key的生成:userCache::id //有就直接调用缓存,没有就调用数据库,加到缓存
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }

4.@CacheEvict

这个注解是用于清除缓存的,可以定向清理,也可以清除全部:

定向:

java 复制代码
 @DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")//key的生成:userCache::id
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

全部:

java 复制代码
@DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)
    public void deleteAll(){
        userMapper.deleteAll();
    }

三、购物车功能

回到苍穹外卖中去,我们将完成购物车的功能实现,购物车整体功能没有菜品套餐那么多,它不需要修改,只需要增加、查看、删除、清空(删除全部)。

但是它的业务逻辑挺复杂的,尤其是增加,首先,购物车是私人的,也就是说我们需要根据用户id来对这个id的购物车进行操作,这个肯定就会用到BaseContext了。其次,购物车中点套餐的 "+" 只需要增加数量,但是点菜品就需要指定规格,这个规格是用于选择口味的,所以我们会在这个表中添加冗余字段,自然的,查询操作比起菜品也会更加复杂。

1.添加购物车

(1)文档

老规矩,先分析文档,可以看到是传回请求体,这里面会传三个参数,所以需要查看的表还挺多的。

这里看看DTO的属性,不然后面很难搞懂。

java 复制代码
@Data
public class ShoppingCartDTO implements Serializable {

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

}

(2)Controller

这个Controller看着还是人畜无害的,没什么好讲的。

java 复制代码
/**
     * 添加购物车
     * @param shoppingCartDTO
     * @return
     */
    @ApiOperation("添加购物车")
    @PostMapping("/add")
    public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("添加购物车,商品信息为:{}",shoppingCartDTO);
        shoppingCartService.addShoppingCart(shoppingCartDTO);
        return Result.success();
    }

(3)Service层

接口:

java 复制代码
 /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    void addShoppingCart(ShoppingCartDTO shoppingCartDTO);

实现类:这个就开始唬人了,我们慢慢来分析一下它的逻辑。

java 复制代码
 /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    @Override
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        //判断当前加入到啊购物车中的商品是否已经存在了
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
        Long userId = BaseContext.getCurrentId();
        shoppingCart.setUserId(userId);

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


        //如果已经存在了,只需要将数量加一
        if(list != null && !list.isEmpty()){
            ShoppingCart cart = list.get(0);
            cart.setNumber(cart.getNumber() + 1);//update shopping_cart set number = ? where id = ?
            shoppingCartMapper.updateNumberById(cart);
        }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{
                //本次添加到购物车的是套餐
                Long setmealId = shoppingCartDTO.getSetmealId();
                Setmeal setmeal = setmealMapper.getById(setmealId);
                shoppingCart.setName(setmeal.getName());
                shoppingCart.setImage(setmeal.getImage());
                shoppingCart.setAmount(setmeal.getPrice());


            }
            //不管是套餐还是菜品都要设置下面两行
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartMapper.insert(shoppingCart);
        }
    }

分析逻辑:

1.判断当前加入到购物车中的商品是否已经存在了。

2.如果已经存在了,只需要将数量加一

3.如果不存在,需要插入一条购物车数据

4.判断本次添加到购物车的是菜品还是套餐

一个一个慢慢说:

**第一,我们为什么要判断购物车的商品是否已经存在?**这是因为同样的商品只需要将数量+1即可,不同的商品才需要将整个数据重新添加到购物车中。

第二,我们怎么判断是否存在 ?这个就需要调用查询方法了,而且这个查询方法需要很精确 ,不是说只根据id查询就行的,因为同一个菜品,口味不一样也不能算作一个商品,必须口味相同才能算一个菜品,而套餐就简单一些了,同一个id的套餐中的菜品肯定是一样的,这时候只需要比较id就行了。但是不管是套餐还是菜品,都是商品 ,我们不可能写两个Mapper,所以这里我们采用动态sql,对ShoppingCart 中的每个相关属性都进行比较,即比较用户id、菜品id、套餐id、菜品口味id,如果能查出来,说明购物车里面就存在同一个商品了。

第三,为什么要判断本次添加到购物车的是菜品还是套餐? 这是因为一个人一次只能将一个商品放入购物车,不可能同时将一个菜品和一个套餐放入,所以需要判断到底是菜品还是套餐,如果是菜品,就应该将ShoppingCart的菜品id 改变,名称也应该改成菜品的名称,套餐同理,也就是说,每一个ShoppingCart的setmealId和dishId必定会空一个

搞懂了这三条,基本上也就搞清楚添加购物车的逻辑了。

(4)持久层

Mapper:

java 复制代码
/**
     * 动态条件查询
     * @param shoppingCart
     * @return
     */
    List<ShoppingCart> list(ShoppingCart shoppingCart);


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


/**
     * 根据商品修改商品数量
     * @param shoppingCart
     */
    @Update("update shopping_cart set number = #{number} where id = #{id}")
    void updateNumberById(ShoppingCart shoppingCart);

映射文件:

XML 复制代码
<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="setmealId != null">
                and setmeal_id = #{setmealId}
            </if>
            <if test="dishId != null">
                and dish_id = #{dishId}
            </if>
            <if test="dishFlavor != null">
                and dish_flavor = #{dishFlavor}
            </if>
        </where>
    </select>
</mapper>

可以看出这确实很复杂,一个添加功能需要调用三种Mapper,这个是前面的所有功能都没有用过的,是目前最复杂的一个功能,但是好消息是,因为我们都写了三个Mapper了,所以后面的查询功能就会简单很多了。

2.查看购物车

这里就不看文档了,因为我们都已经实现了这个接口了。

java 复制代码
/**
     * 查询购物车
     * @return
     */
    @ApiOperation("查询购物车")
    @GetMapping("/list")
    public Result<List<ShoppingCart>> list(){
        log.info("查询购物车");
        List<ShoppingCart> list = shoppingCartService.showShoppingCart();
        return Result.success(list);
    }
java 复制代码
 /**
     * 查询购物车
     * @return
     */
    List<ShoppingCart> showShoppingCart();
java 复制代码
/**
     * 查询购物车
     * @return
     */
    @Override
    public List<ShoppingCart> showShoppingCart() {
        //获取到当前微信用户的id
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = ShoppingCart.builder().
                userId(userId).
                build();
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        return list;
    }

3.清空购物车

这个功能也很简单,只需要根据用户id直接删除整条数据即可。

java 复制代码
/**
     * 清空购物车
     * @return
     */
    @ApiOperation("清空购物车")
    @DeleteMapping("/clean")
    public Result clean(){
        shoppingCartService.cleanShoppingCart();
        return Result.success();
    }
java 复制代码
 /**
     * 清空购物车
     */
    @Override
    public void cleanShoppingCart() {
        Long userId = BaseContext.getCurrentId();
        shoppingCartMapper.deleteByUserId(userId);
    }
java 复制代码
/**
     * 清空购物车
     */
    void cleanShoppingCart();
java 复制代码
/**
     * 根据用户Id删除购物车数据
     * @param userId
     */
    @Delete("delete from shopping_cart where user_id = #{userId}")
    void deleteByUserId(Long userId);

4.删除购物车

下面代码是我自己写的了。

java 复制代码
/**
     * 删除一个商品
     * @return
     */
    @ApiOperation("删除一个商品")
    @PostMapping("/sub")
    public Result deleteShoppingCart(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("删除一个商品:{}",shoppingCartDTO);
        shoppingCartService.deleteShoppingCart(shoppingCartDTO);
        return Result.success();
    }
java 复制代码
 /**
     * 删除商品
     */
    void deleteShoppingCart(ShoppingCartDTO shoppingCartDTO);
java 复制代码
/**
     * 删除商品
     *
     * @param shoppingCartDTO
     */
    @Override
    public void deleteShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        //判断当前加入到购物车中的商品是否已经存在了
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        Long userId = BaseContext.getCurrentId();
        shoppingCart.setUserId(userId);

        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        log.info("获取的list,{}",list);

        if (list != null && !list.isEmpty()) {
            ShoppingCart cart = list.get(0);
            log.info("获取的cart,{}",cart);
            if (cart.getNumber() - 1 == 0) {
                shoppingCartMapper.deleteShoppingCart(cart.getId());
            }
            cart.setNumber(cart.getNumber() - 1);
            shoppingCartMapper.updateNumberById(cart);
        }else {
            throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
        }

    }
java 复制代码
/**
     * 删除一个商品
     * @param shoppingCartId
     */
    @Delete("delete from shopping_cart where id = #{shoppingCartId}")
    void deleteShoppingCart(Long shoppingCartId);

花絮:bug

中间出了个bug,原因是搞忘绑定请求体了,导致无论按下哪个 "-" 号,删除的都是查询列表中的第一个商品(这个是因为如果没有绑定请求体,无论什么请求都将查询出所有在购物车中的商品,按照我的逻辑,删除list[0],就相当于删除了列表第一个数据),后面通过添加日志找出问题了,修改后即查询出的列表只会有一个商品了,这个时候删除列表第一个数据,就相当于删除指定的商品了。

在这里提醒各位,别忘了绑定请求体!!!

相关推荐
苍老流年2 小时前
1. SpringBoot初始化器ApplicationContextInitializer使用与源码分析
java·spring boot·后端
星光一影2 小时前
基于SpringBoot智慧社区系统/乡村振兴系统/大数据与人工智能平台
大数据·spring boot·后端·mysql·elasticsearch·vue
劲墨难解苍生苦2 小时前
spring ai alibaba mcp 开发demo
java·人工智能
leonardee2 小时前
Spring 中的 @ExceptionHandler 注解详解与应用
java·后端
不爱编程的小九九2 小时前
小九源码-springboot103-踏雪阁民宿订购平台
java·开发语言·spring boot
Elieal2 小时前
Spring 框架核心技术全解析
java·spring·sqlserver
组合缺一2 小时前
(对标 Spring)OpenSolon v3.7.0, v3.6.4, v3.5.8, v3.4.8 发布(支持 LTS)
java·后端·spring·web·solon
程序员爱钓鱼2 小时前
Python编程实战——Python实用工具与库:Pandas数据处理
后端·python·ipython
凸头2 小时前
Spring Boot接收前端参数的注解总结
前端·spring boot·后端