蒼穹外賣學習筆記——第七天

zgg1h發表於2024-07-18

快取商品、購物車

快取菜品

問題說明

  • 使用者端小程式展示的菜品資料都是透過查詢資料庫獲得,如果使用者端訪問量比較大,資料庫訪問壓力隨之增大,從而導致系統響應慢、使用者體驗差。

實現思路

  • 透過Redis來快取菜品資料,減少資料庫查詢操作,具體流程如下:
蒼穹外賣學習筆記——第七天
  • 快取邏輯分析:
    • 每個分類下的菜品儲存一份快取資料,其中快取資料的key為“dish_分類id”,value為對應菜品陣列序列化後的string。
    • 資料庫中菜品資料有變更時清理快取資料。

程式碼開發

  • 修改使用者端介面 DishController 的 list 方法,加入快取處理邏輯:
@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.isEmpty()) {
        //如果存在,直接返回,無需查詢資料庫
        return Result.success(list);
    }

    //如果不存在,查詢資料庫
    Dish dish = new Dish();
    dish.setCategoryId(categoryId);
    dish.setStatus(StatusConstant.ENABLE);//查詢起售中的菜品

    list = dishService.listWithFlavor(dish);

    //將查詢結果的資料放入redis中
    redisTemplate.opsForValue().set(key, list);

    return Result.success(list);
}
  • 修改管理端介面 DishController 的相關方法,加入清理快取的邏輯,需要改造以下方法:新增菜品、修改菜品、批次刪除菜品和起售、停售菜品。

    • 抽取清理快取的方法:

private void cleanCache(String pattern) {
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}


* 呼叫清理快取的方法,保證資料一致性:

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

    //清理快取資料
    String key = "dish_" + dishDTO.getCategoryId();
    cleanCache(key);
    return Result.success();
}

//其他方法內新增以下程式碼
//將所有的菜品快取資料清理掉,即所有以dish_開頭的key
cleanCache("dish_*");

功能測試

  • 可以透過如下方式進行測試:檢視控制檯sql、前後端聯調、檢視Redis中的快取資料。

快取套餐

Spring Cache

  • Spring Cache 是一個框架,實現了基於註解的快取功能,只需要簡單地加一個註解,就能實現快取功能。

  • Spring Cache 提供了一層抽象,底層可以切換不同的快取實現,例如:EHCache、Caffeine和Redis。

  • maven座標為

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

常用註解

常用註解 說明
@EnableCaching 開啟快取註解功能,通常加在啟動類上
@Cacheable 在方法執行前先查詢快取中是否有資料,如果有資料,則直接返回快取資料;如果沒有快取資料,呼叫方法並將方法返回值放到快取中
@CachePut 將方法的返回值放到快取中
@CacheEvict 將一條或多條資料從快取中刪除

用法:

  • CachePut(cacheNames = "cachename", key = "#variable.property"),使用Spring Cache快取資料時,key的生成規則為:cachename::{variable.property的值},其中key按照spEL(spring Expression Language)的規則來寫,其中一種寫法是目標變數.目標屬性,這個"."叫做物件導航。
  • CacheEvict(cacheNames = "cachename", allEntries = true),使用allEntries = true即可刪除cachename下的所有鍵值對。

實現思路

  • 匯入Spring Cache和Redis相關maven座標。
  • 在啟動類上加入@EnableCaching註解,開啟快取註解功能。
  • 在使用者端介面SetmealController的 list 方法上加入@Cacheable註解。
  • 在管理端介面SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict註解。

程式碼開發

  • 在使用者端介面SetmealController的 list 方法上加入@Cacheable註解:
@GetMapping("/list")
@ApiOperation("根據分類id查詢套餐")
@Cacheable(cacheNames = "setmealCache", key = "#categoryId")
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);
}
  • 在管理端介面SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict註解:
@CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId") //save上

@CacheEvict(cacheNames = "setmealCache", allEntries = true) //其他方法上

功能測試

  • 透過前後端聯調方式來進行測試,同時觀察redis中快取的套餐資料。

新增購物車

需求分析和設計

產品原型

蒼穹外賣學習筆記——第七天

介面設計

蒼穹外賣學習筆記——第七天

資料庫設計

shopping_cart購物車表
欄位名 資料型別 說明 備註
id bigint 主鍵 自增
name varchar(32) 商品名稱 冗餘欄位
image varchar(255) 商品圖片路徑 冗餘欄位
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 建立時間

程式碼開發

  • 根據新增購物車介面的引數設計DTO:
@Data
public class ShoppingCartDTO implements Serializable {

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

}
  • 根據新增購物車介面建立ShoppingCartController:
@RestController("userShopController")
@RequestMapping("/user/shop")
@Api(tags = "C端-店鋪相關介面")
@Slf4j
public class ShopController {

    public static final String KEY = "SHOP_STATUS";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 查詢店鋪營業狀態
     *
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("查詢店鋪營業狀態")
    public Result<Integer> getStatus() {
        Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("查詢店鋪營業狀態為:{}", status == 1 ? "營業中" : "打烊中");
        return Result.success(status);
    }
}
  • 建立ShoppingCartService介面:
public interface ShoppingCartService {

    /**
     * 新增購物車
     *
     * @param shoppingCartDTO
     */
    void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}
  • 建立ShoppingCartServiceImpl實現類,並實現add方法:
@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);

        //如果已經存在了,只需要將數量加一
        if (list != null && !list.isEmpty()) {
            ShoppingCart cart = list.get(0);
            cart.setNumber(cart.getNumber() + 1);
            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);
        }
    }
}
  • 建立ShoppingCartMapper介面:
@Mapper
public interface ShoppingCartMapper {

    /**
     * 動態條件查詢
     *
     * @param shoppingCart
     * @return
     */
    List<ShoppingCart> list(ShoppingCart shoppingCart);

    /**
     * 根據id修改菜品數量
     *
     * @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:
<?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="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>

功能測試

  • 可以透過如下方式進行測試:檢視控制檯sql、Swagger介面文件測試、前後端聯調。

檢視購物車

需求分析和設計

產品原型

蒼穹外賣學習筆記——第七天

介面設計

蒼穹外賣學習筆記——第七天

程式碼開發

  • 在ShoppingCartController中建立檢視購物車的方法:
@GetMapping("/list")
@ApiOperation("檢視購物車")
public Result<List<ShoppingCart>> list() {
    List<ShoppingCart> list = shoppingCartService.showShoppingCart();
    return Result.success(list);
}
  • 在ShoppingCartService介面中宣告檢視購物車的方法:
List<ShoppingCart> showShoppingCart();
  • 在ShoppingCartServiceImpl中實現檢視購物車的方法:
@Override
public List<ShoppingCart> showShoppingCart() {
    ShoppingCart shoppingCart = new ShoppingCart().builder()
            .userId(BaseContext.getCurrentId())
            .build();
    List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
    return list;
}

功能測試

可以透過介面文件進行測試,最後完成前後端聯調測試即可。

清空購物車

需求分析和設計

產品原型

蒼穹外賣學習筆記——第七天

介面設計

蒼穹外賣學習筆記——第七天

程式碼開發

  • 在ShoppingCartController中建立清空購物車的方法:
@DeleteMapping("/clean")
@ApiOperation("清空購物車")
public Result clean() {
    shoppingCartService.cleanShoppingCart();
    return Result.success();
}
  • 在ShoppingCartService介面中宣告清空購物車的方法:
void cleanShoppingCart();
  • 在ShoppingCartServiceImpl中實現清空購物車的方法:
@Override
public void cleanShoppingCart() {
    Long userId = BaseContext.getCurrentId();
    shoppingCartMapper.deleteByUserId(userId);
}
  • 在ShoppingCartMapper介面中建立根據使用者id清空購物車的方法:
@Delete("delete from shopping_cart where user_id = #{userId}")
void deleteByUserId(Long userId);

功能測試

透過Swagger介面文件進行測試,透過後再前後端聯調測試即可。

刪除購物車中一個商品

需求分析和設計

產品原型

蒼穹外賣學習筆記——第七天

介面設計

蒼穹外賣學習筆記——第七天

介面設計

程式碼開發

  • 在ShoppingCartController中建立刪除購物車中一個商品的方法:
@PostMapping("/sub")
@ApiOperation("刪除購物車中一個商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO) {
    log.info("刪除購物車中一個商品:{}", shoppingCartDTO);
    shoppingCartService.subShoppingCart(shoppingCartDTO);
    return Result.success();
}
  • 在ShoppingCartService介面中宣告刪除購物車中一個商品的方法:
void subShoppingCart(ShoppingCartDTO shoppingCartDTO);
  • 在ShoppingCartServiceImpl中實現刪除購物車中一個商品的方法:
@Override
public void subShoppingCart(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 = list.get(0);

        Integer number = shoppingCart.getNumber();
        if (number == 1) {
            //當前商品在購物車中的分數為1,直接刪除當前記錄
            shoppingCartMapper.deleteById(shoppingCart.getId());
        } else {
            //當前商品在購物車中的分數不為1,修改份數即可
            shoppingCart.setNumber(number - 1);
            shoppingCartMapper.updateNumberById(shoppingCart);
        }
    }
}
  • 在ShoppingCartMapper介面中建立根據id刪除購物車中一個商品的方法:
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);

功能測試

透過Swagger介面文件進行測試,透過後再前後端聯調測試即可。

相關文章