chenjunda
长期主义

苍穹外卖day07

2025-08-03 javaWeb

苍穹外卖day07

1. 缓存菜品

1.1 问题说明

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

image-20221208180228667

1.2 实现思路

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

image-20221208180818572

缓存逻辑分析:

  • 每个分类下的菜品保存一份缓存数据
  • 数据库中菜品数据有变更时清理缓存数据

image-20221208181007639

1.3 代码开发

修改用户端接口 DishController 的 list 方法,加入缓存处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@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.size() > 0){
//如果存在,直接返回,无须查询数据库
return Result.success(list);
}
////////////////////////////////////////////////////////
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

//如果不存在,查询数据库,将查询到的数据放入redis中
list = dishService.listWithFlavor(dish);
////////////////////////////////////////////////////////
redisTemplate.opsForValue().set(key, list);

return Result.success(list);
}

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

需要改造的方法:

  • 新增菜品
  • 修改菜品
  • 批量删除菜品
  • 起售、停售菜品

抽取清理缓存的方法:

在管理端DishController中添加

1
2
3
4
5
6
7
8
9
10
@Autowired
private RedisTemplate redisTemplate;
/**
* 清理缓存数据
* @param pattern
*/
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}

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

1). 新增菜品优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 新增菜品
*
* @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();
}

2). 菜品批量删除优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 菜品批量删除
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);

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

return Result.success();
}

3). 修改菜品优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 修改菜品
*
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);

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

return Result.success();
}

4). 菜品起售停售优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 菜品起售停售
*
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品起售停售")
public Result<String> startOrStop(@PathVariable Integer status, Long id) {
dishService.startOrStop(status, id);

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

return Result.success();
}

1.4 功能测试

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

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

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

1). 加入缓存

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

2). 菜品修改

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

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

2. 缓存套餐

2.1 Spring Cache

2.1.1 介绍

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

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

  • EHCache
  • Caffeine
  • Redis(常用)

起步依赖:

1
2
3
4
<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坐标即可。

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坐标(已实现)

1
2
3
4
5
6
7
8
9
<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注解,开启缓存注解功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 条件查询
*
* @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注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 新增套餐
*
* @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中缓存的套餐数据。和缓存菜品功能测试基本一致,不再赘述。

3. 添加购物车

3.1 需求分析和设计

3.1.1 产品原型

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

3.1.2 接口设计

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

image-20221208184342490

image-20221208184354291

3.1.3 表设计

用户的购物车数据,也是需要保存在数据库中的,购物车对应的数据表为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 创建时间

说明:

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

3.2 代码开发

3.2.1 DTO设计

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

image-20221208184938195

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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.service.ShoppingCartService;
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
public class ShoppingCartServiceImpl implements ShoppingCartService {

@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;

/**
* 添加购物车
*
* @param shoppingCartDTO
*/
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
//只能查询自己的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());

//判断当前商品是否在购物车中
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);

if (shoppingCartList != null && shoppingCartList.size() == 1) {
//如果已经存在,就更新数量,数量加1
shoppingCart = shoppingCartList.get(0);
shoppingCart.setNumber(shoppingCart.getNumber() + 1);
shoppingCartMapper.updateNumberById(shoppingCart);
} else {
//如果不存在,插入数据,数量就是1

//判断当前添加到购物车的是菜品还是套餐
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 {
//添加到购物车的是套餐
Setmeal setmeal = setmealMapper.getById(shoppingCartDTO.getSetmealId());
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
}
}

3.2.5 Mapper层

创建ShoppingCartMapper接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.sky.mapper;

import com.sky.entity.ShoppingCart;
import org.apache.ibatis.annotations.Delete;
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 {
/**
* 条件查询
*
* @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);

}

创建ShoppingCartMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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" 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 功能测试

4. 查看购物车

4.1 需求分析和设计

4.1.1 产品原型

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

image-20221208190102904

4.2 代码开发

4.2.1 Controller层

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

1
2
3
4
5
6
7
8
9
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
@ApiOperation("查看购物车")
public Result<List<ShoppingCart>> list(){
return Result.success(shoppingCartService.showShoppingCart());
}

4.2.2 Service层接口

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

1
2
3
4
5
/**
* 查看购物车
* @return
*/
List<ShoppingCart> showShoppingCart();

4.2.3 Service层实现类

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

1
2
3
4
5
6
7
8
9
10
/**
* 查看购物车
* @return
*/
public List<ShoppingCart> showShoppingCart() {
return shoppingCartMapper.list(ShoppingCart.
builder().
userId(BaseContext.getCurrentId()).
build());
}

4.3 功能测试

5. 清空购物车

5.1 需求分析和设计

5.1.1 产品原型

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

5.2 代码开发

5.2.1 Controller层

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

1
2
3
4
5
6
7
8
9
10
/**
* 清空购物车商品
* @return
*/
@DeleteMapping("/clean")
@ApiOperation("清空购物车商品")
public Result<String> clean(){
shoppingCartService.cleanShoppingCart();
return Result.success();
}

5.2.2 Service层接口

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

1
2
3
4
/**
* 清空购物车商品
*/
void cleanShoppingCart();

5.2.3 Service层实现类

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

1
2
3
4
5
6
/**
* 清空购物车商品
*/
public void cleanShoppingCart() {
shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId());
}

5.2.4 Mapper层

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

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

5.3 功能测试

6购物车其他功能代码

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

1.1 产品原型
1.4 代码开发
1.4.1 ShoppingCartController
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
* @return
*/
@PostMapping("/sub")
@ApiOperation("删除购物车中一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("删除购物车中一个商品,商品:{}", shoppingCartDTO);
shoppingCartService.subShoppingCart(shoppingCartDTO);
return Result.success();
}
1.4.2 ShoppingCartService
1
2
3
4
5
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
*/
void subShoppingCart(ShoppingCartDTO shoppingCartDTO);
1.4.3 ShoppingCartServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
*/
public void subShoppingCart(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 number = shoppingCart.getNumber();
if(number == 1){
//当前商品在购物车中的份数为1,直接删除当前记录
shoppingCartMapper.deleteById(shoppingCart.getId());
}else {
//当前商品在购物车中的份数不为1,修改份数即可
shoppingCart.setNumber(shoppingCart.getNumber() - 1);
shoppingCartMapper.updateNumberById(shoppingCart);
}
}
}
1.4.4 ShoppingCartMapper
1
2
3
4
5
6
/**
* 根据id删除购物车数据
* @param id
*/
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);

为什么要这样写,我这样写不行吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* @Override
public void delete(ShoppingCartDTO shoppingCartDTO) {

//先判断这个商品是哪一个
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
shoppingCart=shoppingCartMapper.findByDTO(shoppingCartDTO);
//然后获取这个商品的number
Integer number=shoppingCart.getNumber();
//如果大于1就number-1然后更新
if(number>1){
shoppingCart.setNumber(number-1);
shoppingCartMapper.updateNumberById(shoppingCart);
}else {
//如果=1就直接删掉
shoppingCartMapper.delete(shoppingCartDTO);
}


System.out.println("删除");
}
*/

同样能实现功能,那样写有什么好处

ChatGPT 说:

你这两个方法 功能确实类似,都是从购物车中删除一个商品(减一份,如果为 1 就删记录)。
第一个写法更严谨、安全、易维护,主要体现在以下几点:


✅ 对比分析:

对比点 第一种写法(推荐) 你写的写法
查询逻辑 查询当前登录用户 + 商品信息,返回列表 自定义方法 findByDTO 查询商品
安全性 明确绑定用户ID,防止误删他人数据 findByDTO 内部逻辑不清楚,可能缺少用户ID条件
健壮性 判断 list 是否为空,防止空指针 没有空指针判断,若查不到数据将报错
灵活性 可拓展支持多个相同商品记录(取第一个) 假设商品唯一,不支持多记录情况
规范性 使用 deleteByIdupdateNumberById,操作明确 自定义 delete 方法,可能不够清晰

🔎 核心问题分析:

1. findByDTO 的实现细节未知,可能存在的问题:

  • 没传 userId,就可能查到别人的商品。
  • 可能查不到数据而抛 NullPointerException
  • 如果数据库中一条商品记录因某些原因有多条(极端情况),你写的可能查不到或报错。

2. 第一种写法的优势:

  • 明确筛选当前登录用户的数据
  • 返回列表后再判断,避免查不到时报错。
  • 对商品记录数多的情况也更稳妥(虽然实际业务中可能不会发生)。

✅ 建议:

  • 你写的方式在逻辑正确、数据规范的前提下也能工作,适用于简单项目。
  • 但如果你在写企业级项目或多人协作项目,建议用第一种方式,更健壮、清晰、符合规范

✍总结一句话:

你的写法在功能上是通的,但第一种写法更安全、鲁棒、对异常处理更完善,适合复杂业务和多人维护的系统。

您说:

你是说使用list获取然后可以判断是否为null,而我写的方法没有这个功能吗,而且使用DTO传进Mapper没法确定userId容易删除别人的购物车这两个是最大纰漏吗

ChatGPT 说:

是的,你理解得非常准确。你提到的两个问题确实是你那种写法的最大纰漏,我再帮你总结清晰一点:


第一:是否能判断是否为空

  • 第一种写法(使用 list
    • shoppingCartMapper.list(shoppingCart) 返回一个 List<ShoppingCart>,即使查不到数据也只是空集合。
    • 你可以通过 if (list != null && list.size() > 0)空数据判断,避免 NullPointerException
  • ⚠️ 你的写法(findByDTO
    • 如果查不到对应记录,很可能直接返回 null
    • 你没有判断 shoppingCart == null 就直接调用 getNumber(),可能导致空指针异常
    • 所以它更容易报错,而且错误不易察觉

第二:userId 风险

  • 第一种写法
    • 显式调用 shoppingCart.setUserId(BaseContext.getCurrentId()),把当前登录用户的 ID 作为查询条件之一。
    • 可以确保查询和删除的都是当前登录用户自己的购物车数据
  • ⚠️ 你的写法
    • 你直接用 DTO 调用 findByDTO(shoppingCartDTO),如果 DTO 中没有封装 userIdMapper 查询语句就没有 userId 限制
    • 那么在多用户系统中就可能误删或修改其他用户的购物车数据,这是严重的安全漏洞

✅ 小结一句话:

对!你写的方式存在两个最大风险:

  1. 没有判断空对象,容易空指针。
  2. 没加 userId 限制,容易删错别人的数据。

建议:你可以在你现有方法中改进:

1
2
3
4
5
6
java复制编辑// 强制加上 userId,避免查错用户数据
shoppingCart.setUserId(BaseContext.getCurrentId());
shoppingCart = shoppingCartMapper.findByDTO(shoppingCart);
if (shoppingCart == null) {
return; // 或者抛异常
}

这样也能弥补这两个漏洞,但总体还是建议参考第一种写法的结构化设计思维

Author: chenjunda

Link: http://example.com/2025/08/03/%E8%8B%8D%E7%A9%B9%E5%A4%96%E5%8D%96day07/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

NextPost >
diary2025-8-2
CATALOG
  1. 1. 苍穹外卖day07
    1. 1.1. 1. 缓存菜品
      1. 1.1.1. 1.1 问题说明
      2. 1.1.2. 1.2 实现思路
      3. 1.1.3. 1.3 代码开发
      4. 1.1.4. 1.4 功能测试
    2. 1.2. 2. 缓存套餐
      1. 1.2.1. 2.1 Spring Cache
        1. 1.2.1.1. 2.1.1 介绍
        2. 1.2.1.2. 2.1.2 常用注解
      2. 1.2.2. 2.2 实现思路
      3. 1.2.3. 2.3 代码开发
      4. 1.2.4. 2.4 功能测试
    3. 1.3. 3. 添加购物车
      1. 1.3.1. 3.1 需求分析和设计
        1. 1.3.1.1. 3.1.1 产品原型
        2. 1.3.1.2. 3.1.2 接口设计
        3. 1.3.1.3. 3.1.3 表设计
      2. 1.3.2. 3.2 代码开发
        1. 1.3.2.1. 3.2.1 DTO设计
        2. 1.3.2.2. 3.2.2 Controller层
        3. 1.3.2.3. 3.2.3 Service层接口
        4. 1.3.2.4. 3.2.4 Service层实现类
        5. 1.3.2.5. 3.2.5 Mapper层
      3. 1.3.3. 3.3 功能测试
    4. 1.4. 4. 查看购物车
      1. 1.4.1. 4.1 需求分析和设计
        1. 1.4.1.1. 4.1.1 产品原型
      2. 1.4.2. 4.2 代码开发
        1. 1.4.2.1. 4.2.1 Controller层
        2. 1.4.2.2. 4.2.2 Service层接口
        3. 1.4.2.3. 4.2.3 Service层实现类
      3. 1.4.3. 4.3 功能测试
    5. 1.5. 5. 清空购物车
      1. 1.5.1. 5.1 需求分析和设计
        1. 1.5.1.1. 5.1.1 产品原型
      2. 1.5.2. 5.2 代码开发
        1. 1.5.2.1. 5.2.1 Controller层
        2. 1.5.2.2. 5.2.2 Service层接口
        3. 1.5.2.3. 5.2.3 Service层实现类
        4. 1.5.2.4. 5.2.4 Mapper层
      3. 1.5.3. 5.3 功能测试
      4. 1.5.4. 6购物车其他功能代码
        1. 1.5.4.1. 1. 删除购物车中一个商品
          1. 1.5.4.1.1. 1.1 产品原型
          2. 1.5.4.1.2. 1.4 代码开发
            1. 1.5.4.1.2.1. 1.4.1 ShoppingCartController
            2. 1.5.4.1.2.2. 1.4.2 ShoppingCartService
            3. 1.5.4.1.2.3. 1.4.3 ShoppingCartServiceImpl
            4. 1.5.4.1.2.4. 1.4.4 ShoppingCartMapper
            5. 1.5.4.1.2.5. ChatGPT 说:
      5. 1.5.5. ✅ 对比分析:
      6. 1.5.6. 🔎 核心问题分析:
        1. 1.5.6.1. 1. findByDTO 的实现细节未知,可能存在的问题:
        2. 1.5.6.2. 2. 第一种写法的优势:
      7. 1.5.7. ✅ 建议:
      8. 1.5.8. ✍总结一句话:
        1. 1.5.8.0.1. 您说:
          1. 1.5.8.0.1.1. ChatGPT 说:
  2. 1.6. ✅ 第一:是否能判断是否为空
  3. 1.7. ✅ 第二:userId 风险
  4. 1.8. ✅ 小结一句话: