
思维导图:


1. 触发入口:
2. 业务规则(原型背后的逻辑):
基本信息
/user/shoppingCart/add
POST
请求参数(菜品id,套餐id,口味) 由于既可以添加菜品(Dish),也可以添加套餐(Setmeal),因此前端需要传递一个区分类型的标识。
请求头(Headers):
Content-Type: application/json
token: (用户JWT令牌,用于识别当前操作用户)
请求体(Body JSON 示例):
场景A:添加菜品(假设选择微辣)
json
{
"dishId": 101,
"setmealId": null,
"dishFlavor": "微辣"
}场景B:添加套餐
json
{
"dishId": null,
"setmealId": 202,
"dishFlavor": ""
}参数说明:
参数名 | 类型 | 是否必须 | 说明 |
|---|---|---|---|
dishId | Long | 否 (二选一) | 菜品ID |
setmealId | Long | 否 (二选一) | 套餐ID |
dishFlavor | String | 否 | 菜品口味(如果是菜品且有口味选项时必须传) |
在 Controller 和 Service 层中,该接口的核心处理逻辑如下:
userId)。
dishId/setmealId、dishFlavor 以及解析出的 userId 封装成一个购物车实体对象 (ShoppingCart)。
shopping_cart,条件为:user_id = ? AND (dish_id = ? OR setmeal_id = ?) AND dish_flavor = ?。
number 字段(数量)加1。
number = 1。
create_time 为当前时间。
为了支撑上述接口,数据库表通常包含以下字段:
字段名 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键ID |
name | varchar | 商品名称(冗余字段,方便查询) |
user_id | bigint | 关联的用户ID(核心,区分不同用户的购物车) |
dish_id | bigint | 菜品ID(可为空) |
setmeal_id | bigint | 套餐ID(可为空) |
dish_flavor | varchar | 菜品口味(如果是菜品) |
number | int | 商品数量 |
amount | decimal | 商品单价(冗余字段,下单时参考) |
image | varchar | 商品图片 |
create_time | datetime | 加入购物车的时间(通常用于按时间倒序展示) |
总结: 该接口的设计重点在于幂等性处理(同样的商品加两次变成数量2,而不是两条记录)以及用户隔离(通过user_id区分不同用户的购物车)。
冗余字段指的是那些可以通过其他表关联查询得到,但却故意在当前表中额外存储的字段。
在购物车表 shopping_cart 中,name(商品名称)、amount(单价)、image(图片)就是典型的冗余字段,因为这些信息本来存储在 dish(菜品表)和 setmeal(套餐表)中。
amount 字段
任何设计都有两面性,冗余字段也带来了一些问题:
sql
-- 如果菜品表的价格从 28 改为 32
UPDATE dish SET price = 32 WHERE id = 101;
-- 但购物车表里还是 28
SELECT amount FROM shopping_cart WHERE dish_id = 101; -- 还是 28冗余字段是一种“以空间换时间”的优化手段。在购物车这个场景中:
因此,适当使用冗余字段是非常合理的设计选择。
我们首先要考虑怎么接收前端提交过来的参数,我们可以用我们准备好的DTO实体类来封装提交过来的参数。

text
Controller层 (接收请求) → Service层 (业务逻辑) → Mapper层 (数据库操作)java
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class ShoppingCartDTO implements Serializable {
// 菜品ID
private Long dishId;
// 套餐ID
private Long setmealId;
// 口味
private String dishFlavor;
}java
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;
}java
package com.sky.controller.user;
import com.sky.dto.ShoppingCartDTO;
import com.sky.result.Result;
import com.sky.service.ShoppingCartService;
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.*;
@RestController
@RequestMapping("/user/shoppingCart")
@Api(tags = "用户端-购物车接口")
@Slf4j
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
/**
* 添加购物车
* @param shoppingCartDTO
* @return
*/
@PostMapping("/add")
@ApiOperation("添加购物车")
public Result<String> add(@RequestBody ShoppingCartDTO shoppingCartDTO) {
log.info("添加购物车:{}", shoppingCartDTO);
// 调用Service层处理业务
shoppingCartService.addShoppingCart(shoppingCartDTO);
return Result.success();
}
}java
package com.sky.service;
import com.sky.dto.ShoppingCartDTO;
public interface ShoppingCartService {
/**
* 添加购物车
* @param shoppingCartDTO
*/
void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}java
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 org.springframework.transaction.annotation.Transactional;
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
*/
@Transactional(rollbackFor = Exception.class)
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
// 1. 构建查询条件
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
// 2. 获取当前登录用户ID
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
// 3. 查询当前商品是否已经在购物车中
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
// 4. 如果已经存在,数量+1
if (list != null && list.size() > 0) {
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber() + 1); // 数量加1
shoppingCartMapper.updateNumberById(cart);
} else {
// 5. 如果不存在,需要插入一条购物车记录
// 5.1 设置基本属性
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
// 5.2 判断是菜品还是套餐,并补全其他信息
if (shoppingCartDTO.getDishId() != null) {
// 添加的是菜品
Dish dish = dishMapper.getById(shoppingCartDTO.getDishId());
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());
}
// 5.3 插入数据库
shoppingCartMapper.insert(shoppingCart);
}
}
}java
package com.sky.mapper;
import com.sky.entity.ShoppingCart;
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);
/**
* 根据id更新数量
* @param shoppingCart
*/
@Update("UPDATE shopping_cart SET number = #{number} WHERE id = #{id}")
void updateNumberById(ShoppingCart shoppingCart);
/**
* 插入购物车数据
* @param shoppingCart
*/
void insert(ShoppingCart shoppingCart);
}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">
<!-- 插入购物车数据 -->
<insert id="insert" parameterType="com.sky.entity.ShoppingCart">
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})
</insert>
<!-- 动态查询购物车列表 -->
<select id="list" parameterType="com.sky.entity.ShoppingCart"
resultType="com.sky.entity.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 dishFlavor != ''">
AND dish_flavor = #{dishFlavor}
</if>
</where>
ORDER BY create_time DESC
</select>
</mapper>java
// 用户A已有一个"宫保鸡丁 微辣"在购物车,数量=2
// 再次添加同一个商品
// 1. 查询条件:userId=1001, dishId=101, dishFlavor="微辣"
// 2. 查到记录:{id=5, number=2, ...}
// 3. setNumber(3) → 更新数据库
// 4. 结果:数量变为3java
// 用户A购物车没有"酸菜鱼"
// 1. 查询条件:userId=1001, dishId=102
// 2. 查不到记录,list为空
// 3. 从菜品表查询酸菜鱼信息
// 4. 组装完整对象,插入数据库
// 5. 结果:新增一条记录,数量=1json
POST /user/shoppingCart/add
Content-Type: application/json
token: eyJhbGciOiJIUzI1NiJ9...
{
"dishId": 101,
"dishFlavor": "微辣"
}json
{
"code": 1,
"msg": "操作成功",
"data": null
}BaseContext.getCurrentId()从ThreadLocal获取
@Transactional保证数据一致性
用户登录
↓
后端生成JWT(包含userId=1001)
↓
前端保存token
↓
用户添加购物车 → 请求头携带token
↓
拦截器拦截请求 → 解析token → 获取userId=1001
↓
存入ThreadLocal ← BaseContext.setCurrentId(1001)
↓
Controller接收请求
↓
Service调用 BaseContext.getCurrentId() → 得到1001
↓
Service使用userId处理业务
↓
请求结束 → 拦截器清理ThreadLocaljava
@Service
public class UserServiceImpl implements UserService {
public LoginVO login(UserLoginDTO loginDTO) {
// 1. 验证用户信息
User user = userMapper.getByOpenid(loginDTO.getCode());
// 2. 登录成功,生成JWT令牌
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId()); // 把用户ID放进token
String token = JwtUtil.createJWT(
jwtProperties.getUserSecretKey(),
jwtProperties.getUserTtl(),
claims
);
// 3. 返回token给前端
return LoginVO.builder()
.id(user.getId())
.token(token)
.build();
}
}javascript
// 前端代码:登录成功后保存token
localStorage.setItem('token', response.data.token);
// 后续每次请求都在header中携带token
axios.defaults.headers.common['token'] = localStorage.getItem('token');java
@Component
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 在请求进入Controller之前执行
*/
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 1. 从请求头获取token
String token = request.getHeader(jwtProperties.getUserTokenName());
// 2. 解析token
Claims claims = JwtUtil.parseJWT(
jwtProperties.getUserSecretKey(),
token
);
// 3. 从token中提取userId
Long userId = claims.get("userId", Long.class);
// 4. 【关键】存入ThreadLocal
BaseContext.setCurrentId(userId);
// 5. 放行请求
return true;
}
/**
* 请求结束后清理
*/
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// 防止内存泄漏
BaseContext.removeCurrentId();
}
}java
@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
// 直接从BaseContext获取当前登录用户的ID
Long userId = BaseContext.getCurrentId();
// 这个userId是安全的,来自token,不是用户传的
shoppingCart.setUserId(userId);
// ... 其他业务逻辑
}
}
结语:如果对你有帮助,请点赞,关注,收藏!