【烘焙坊项目】后端搭建(9)- 缓存实现及购物车相关功能开发

一、缓存菜品

问题说明

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

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

实现思路

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

【开始】------>【后端服务】 ------>(缓存是否存在)no------>【查询数据库】------>【载入缓存】

yes ------>【读取缓存】

缓存逻辑分析:

每个分类的菜品保存一份缓存数据

数据库中菜品数据有变更时清理缓存数据

key value
dish_1 string(...)
dish_2 string(...)
dish_3 string(...)

代码开发

问题:

当进行新增菜品修改菜品删除菜品起售、停售菜品时需要及时清理缓存逻辑,否则小程序端将直接查询之前缓存的数据,而没有及时展示数据库中的最新数据。

编写cleanCache类,并在 新增菜品,修改菜品,删除菜品,起售、停售菜品 部分调用

功能测试

成功

二、缓存套餐

Spring Cache

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>

常用注解:

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

实现思路

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

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

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

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

代码开发

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

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

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

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

java 复制代码
package com.cake.controller.admin;

import com.cake.dto.SetmealDTO;
import com.cake.dto.SetmealPageQueryDTO;
import com.cake.result.PageResult;
import com.cake.result.Result;
import com.cake.service.SetmealService;
import com.cake.vo.SetmealVO;
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.cache.annotation.CacheEvict;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController("adminSetmealController")
@Slf4j
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    /**
     * 新增套餐
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    //@CacheEvict(cacheNames = "setmealCache",key="#categoryId")
    public Result addSetmeal(@RequestBody SetmealDTO setmealDTO){
        log.info("新增套餐:{}", setmealDTO);
        setmealService.addSetmeal(setmealDTO);
        return Result.success();
    }

    /**
     * 套餐分页查询
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("套餐分页查询")
    public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO){
        log.info("套餐分页查询...");
        PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
        return Result.success(pageResult);
    }

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

    /**
     * 根据id查询套餐
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询套餐")
    public Result<SetmealVO> getSetmealById(@PathVariable Long id){
        log.info("根据id查询套餐:{}",id);
        SetmealVO setmealVO = setmealService.getByIdWithDish(id);
        return Result.success(setmealVO);
    }

    /**
     * 修改套餐
     * @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();
    }

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

功能测试

缓存成功

三、添加购物车

需求分析和设计

接口设计:

请求方式:POST

请求路径:/user/shoppingCart/add

请求参数(可选):套餐id,菜品id,口味

返回结果:code,data,msg

查看接口文档:

数据库设计:

作用:暂时存放所选商品的地方

选的什么商品

每个商品都买了几个

不同用户的购物车需要区分开

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 商品名称 冗余字段
image varchar(32) 商品图片路径 冗余字段
user_id bigint 用户id 逻辑外键
dish_id bigint 菜品id 逻辑外键
setmeal_id bigint 套餐id 逻辑外键
dish_flavor varchar(50) 菜品口味
number int 商品数量
amount decimal(10,2) 商品单价 冗余字段
create_time datetime 创建时间

代码开发

controller层

service层

java 复制代码
import com.cake.context.BaseContext;
import com.cake.dto.ShoppingCartDTO;
import com.cake.entity.Dish;
import com.cake.entity.Setmeal;
import com.cake.entity.ShoppingCart;
import com.cake.mapper.DishMapper;
import com.cake.mapper.SetmealMapper;
import com.cake.mapper.ShoppingCartMapper;
import com.cake.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
     */
    @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);

        //如果已经存在了,需要数量+1
        if(list != null && list.size()>0){
            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);

        }

    }
}

dao层

功能测试

数据库中成功查到

四、查看购物车

需求分析和设计

接口设计:

代码开发

controller层

service层

dao层

复用之前的代码

功能测试

五、清空购物车

需求分析和设计

查看接口文档:

代码开发

controller层

service层

dao层

功能测试

成功

六、删除购物车中一个商品

需求分析和设计

查看接口文档:

代码开发

controller层

service层

java 复制代码
 /**
     * 删除购物车中一个商品
     * @param shoppingCartDTO
     */
    @Override
    public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        //查询当前用户的购物车数据,判断number
        shoppingCart.setUserId(BaseContext.getCurrentId());
        log.info("查询购物车参数:userId={},dishId={},dishFlavor={}",
                shoppingCart.getUserId(), shoppingCart.getDishId(), shoppingCart.getDishFlavor());
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        if(list != null && list.size()>0){
            log.info("查询到{}条记录,第一条记录id:{},dishId:{},dishFlavor:{}",
                    list.size(), list.get(0).getId(), list.get(0).getDishId(), list.get(0).getDishFlavor());
            shoppingCart = list.get(0);

            Integer number = shoppingCart.getNumber();
            if(number == 1){
                //直接删除数据
                shoppingCartMapper.delete(shoppingCart.getId());
            }else{
                //数量大于1时,仅修改数量
                shoppingCart.setNumber(shoppingCart.getNumber() - 1);
                shoppingCartMapper.updateNumberById(shoppingCart);
            }

        }
    }

dao层

功能测试

出现了错误,在如下情况时,我点击删除测试菜品,删除的缺是蛋糕配件。检查后端数据库动态语句拼写,shoppingCartDTO和shoppingCart属性名称也没有不对称的情况发生,前端传递信息发现没有异常。查看后台日志一时半会也没有发现异常点,没有报错,没有标红,正常到我以为是我出现了错觉。结果再次尝试时发现如果购物车里只有一件商品时单个删除没有问题,多个商品时这样的问题有时会出现有时又不会。

于是我在拿到当前用户id后和查询购物车信息后添加了日志,并且及时清空控制台。

再次进行测试,等问题再次发生时第一时间查看控制台

终于找到问题,后端没有拿到前端传回的 口味 数据,导致查询购物车信息时没有查到具体信息,根据接下来的代码会拿到第一条数据并进行数量判断和删除操作。因此知道了问题没出现在service层或者dao层,肯定是controller层了,查看后发现代码编写时漏下了**@RequestBody**注解,导致不能接受到完整数据,从而引发的bug

最后再次进行测试,问题已顺利解决。

七、小结

添加购物车功能部分略显麻烦,一开始没有看懂查询数据为什么只返回一条数据的逻辑,总认为得先查找购物车内的所有数据,如何进行遍历比对要添加的数据,如果已有要添加的数据则修改数量,若没有再判断是套餐还是菜品,并进行对应数据添加。

现在我写的代码逻辑就简单一些,直接查询购物车有没有要添加的物品信息,有则改数据,没有则进行判断。因为购物车一次只能添加一件物品,所有最多也只能对应查询出一条数据。并且在查看购物车功能部分做到了代码复用,很精巧。同样的问题,在删除购物车单个商品时没有考虑到数量,以后要多加注意。

测试时发现飘忽不定的异常冷静地利用日志查看信息并解决,以后要更加注意细节才是。

相关推荐
gameboy0311 小时前
在Nginx上配置并开启WebDAV服务的完整指南
java·运维·nginx
重庆小透明2 小时前
【面试问题第一篇】快手后端java一面
java·面试·职场和发展
阿鑫_9962 小时前
通用-ESLint+Prettier基础知识
前端·后端
1104.北光c°2 小时前
我理解的Leaf号段模式:美团分布式ID生成系统
java·开发语言·笔记·分布式·github·leaf
kele_save2 小时前
手把手教你开发一个 AI 可用的天气查询 MCP 服务
后端·node.js
printfall2 小时前
openclaw.mjs
后端
空空潍2 小时前
RabbitMQ高级(2w字笔记)
java·rabbitmq·java-rabbitmq
神奇小汤圆2 小时前
2022 年 Java 后端面试题,吃透 20 套专题技术栈
后端