SpringBoot 购物车设计与实现完整方案
一、购物车设计方案对比
1.1 常见实现方式
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 基于Session | 实现简单,无需额外存储 | 无法跨设备,服务重启数据丢失 | 单服务器部署,临时购物车 |
| 基于Redis | 性能高,支持分布式 | 需要额外维护Redis服务 | 分布式系统,高并发场景 |
| 基于数据库 | 数据持久化,可靠 | 性能较差,并发能力弱 | 数据安全性要求极高的场景 |
| Redis+数据库 | 兼顾性能与可靠性 | 实现复杂度高 | 综合场景,推荐方案 |
二、技术选型
- 框架:Spring Boot 2.7.x
- 缓存:Redis 6.x
- ORM:MyBatis-Plus 3.5.x
- 数据库:MySQL 8.0
- 前端:可与Vue/React等框架配合
三、完整实现方案
3.1 依赖配置
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2 配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/shop?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
timeout: 3000
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
3.3 Redis配置类
package com.example.shop.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// Key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// Value序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
3.4 实体类设计
package com.example.shop.entity;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
public class CartItem implements Serializable {
private static final long serialVersionUID = 1L;
// 商品ID
private Long productId;
// 商品名称
private String productName;
// 商品价格
private BigDecimal price;
// 购买数量
private Integer quantity;
// 商品图片
private String image;
// 是否选中
private Boolean selected = true;
// 商品总价
public BigDecimal getTotalPrice() {
return price.multiply(new BigDecimal(quantity));
}
}
package com.example.shop.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String description;
private BigDecimal price;
private String image;
private Integer stock;
private Integer status;
private Date createTime;
private Date updateTime;
}
3.5 购物车服务接口
package com.example.shop.service;
import com.example.shop.entity.CartItem;
import java.util.List;
import java.util.Map;
public interface CartService {
/**
* 添加商品到购物车
*/
void addToCart(String cartKey, Long productId, Integer quantity);
/**
* 从购物车删除商品
*/
void removeFromCart(String cartKey, Long productId);
/**
* 更新购物车商品数量
*/
void updateCartItemQuantity(String cartKey, Long productId, Integer quantity);
/**
* 更新购物车商品选中状态
*/
void updateCartItemSelected(String cartKey, Long productId, Boolean selected);
/**
* 获取购物车所有商品
*/
List<CartItem> getCartItems(String cartKey);
/**
* 获取购物车商品总数
*/
Integer getCartItemCount(String cartKey);
/**
* 获取购物车总价
*/
Double getCartTotalPrice(String cartKey);
/**
* 清空购物车
*/
void clearCart(String cartKey);
/**
* 合并购物车(登录时)
*/
void mergeCart(String tempCartKey, String userCartKey);
/**
* 生成购物车key
*/
String generateCartKey(Long userId, String sessionId);
}
3.6 购物车服务实现
package com.example.shop.service.impl;
import com.example.shop.entity.CartItem;
import com.example.shop.entity.Product;
import com.example.shop.mapper.ProductMapper;
import com.example.shop.service.CartService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Service
public class CartServiceImpl implements CartService {
private static final String CART_PREFIX = "cart:";
private static final long CART_EXPIRE_DAYS = 7;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
@Autowired
private ObjectMapper objectMapper;
@Override
public void addToCart(String cartKey, Long productId, Integer quantity) {
// 查询商品信息
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 构造购物车项
CartItem cartItem = new CartItem();
cartItem.setProductId(product.getId());
cartItem.setProductName(product.getName());
cartItem.setPrice(product.getPrice());
cartItem.setImage(product.getImage());
// 检查是否已存在该商品
String itemKey = cartKey + ":" + productId;
CartItem existingItem = (CartItem) redisTemplate.opsForValue().get(itemKey);
if (existingItem != null) {
// 已存在则增加数量
cartItem.setQuantity(existingItem.getQuantity() + quantity);
cartItem.setSelected(existingItem.getSelected());
} else {
cartItem.setQuantity(quantity);
}
// 保存到Redis
redisTemplate.opsForValue().set(itemKey, cartItem, CART_EXPIRE_DAYS, TimeUnit.DAYS);
// 将商品ID添加到购物车集合中,用于快速获取购物车中的所有商品
redisTemplate.opsForSet().add(cartKey + ":ids", productId.toString());
redisTemplate.expire(cartKey + ":ids", CART_EXPIRE_DAYS, TimeUnit.DAYS);
}
@Override
public void removeFromCart(String cartKey, Long productId) {
String itemKey = cartKey + ":" + productId;
redisTemplate.delete(itemKey);
redisTemplate.opsForSet().remove(cartKey + ":ids", productId.toString());
}
@Override
public void updateCartItemQuantity(String cartKey, Long productId, Integer quantity) {
String itemKey = cartKey + ":" + productId;
CartItem cartItem = (CartItem) redisTemplate.opsForValue().get(itemKey);
if (cartItem != null) {
cartItem.setQuantity(quantity);
redisTemplate.opsForValue().set(itemKey, cartItem, CART_EXPIRE_DAYS, TimeUnit.DAYS);
}
}
@Override
public void updateCartItemSelected(String cartKey, Long productId, Boolean selected) {
String itemKey = cartKey + ":" + productId;
CartItem cartItem = (CartItem) redisTemplate.opsForValue().get(itemKey);
if (cartItem != null) {
cartItem.setSelected(selected);
redisTemplate.opsForValue().set(itemKey, cartItem, CART_EXPIRE_DAYS, TimeUnit.DAYS);
}
}
@Override
public List<CartItem> getCartItems(String cartKey) {
List<CartItem> cartItems = new ArrayList<>();
Set<Object> productIds = redisTemplate.opsForSet().members(cartKey + ":ids");
if (productIds != null) {
for (Object id : productIds) {
String itemKey = cartKey + ":" + id;
CartItem cartItem = (CartItem) redisTemplate.opsForValue().get(itemKey);
if (cartItem != null) {
cartItems.add(cartItem);
}
}
}
return cartItems;
}
@Override
public Integer getCartItemCount(String cartKey) {
Integer count = 0;
List<CartItem> cartItems = getCartItems(cartKey);
for (CartItem item : cartItems) {
count += item.getQuantity();
}
return count;
}
@Override
public Double getCartTotalPrice(String cartKey) {
Double totalPrice = 0.0;
List<CartItem> cartItems = getCartItems(cartKey);
for (CartItem item : cartItems) {
if (item.getSelected()) {
totalPrice += item.getTotalPrice().doubleValue();
}
}
return totalPrice;
}
@Override
public void clearCart(String cartKey) {
Set<Object> productIds = redisTemplate.opsForSet().members(cartKey + ":ids");
if (productIds != null) {
for (Object id : productIds) {
String itemKey = cartKey + ":" + id;
redisTemplate.delete(itemKey);
}
}
redisTemplate.delete(cartKey + ":ids");
}
@Override
public void mergeCart(String tempCartKey, String userCartKey) {
List<CartItem> tempCartItems = getCartItems(tempCartKey);
if (!tempCartItems.isEmpty()) {
for (CartItem item : tempCartItems) {
// 将临时购物车商品添加到用户购物车
addToCart(userCartKey, item.getProductId(), item.getQuantity());
// 更新选中状态
updateCartItemSelected(userCartKey, item.getProductId(), item.getSelected());
}
// 清空临时购物车
clearCart(tempCartKey);
}
}
@Override
public String generateCartKey(Long userId, String sessionId) {
if (userId != null) {
return CART_PREFIX + "user:" + userId;
} else if (!StringUtils.isEmpty(sessionId)) {
return CART_PREFIX + "session:" + sessionId;
}
throw new RuntimeException("无法生成购物车key");
}
}
3.7 购物车控制器
package com.example.shop.controller;
import com.example.shop.entity.CartItem;
import com.example.shop.service.CartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("/cart")
public class CartController {
@Autowired
private CartService cartService;
/**
* 获取当前登录用户ID(实际项目中应从Session或Token中获取)
*/
private Long getCurrentUserId(HttpSession session) {
// 实际项目中这里应该从Session或JWT Token中获取用户ID
return (Long) session.getAttribute("userId");
}
/**
* 获取购物车Key
*/
private String getCartKey(HttpSession session) {
Long userId = getCurrentUserId(session);
String sessionId = session.getId();
return cartService.generateCartKey(userId, sessionId);
}
/**
* 查看购物车
*/
@GetMapping("/list")
public String cartList(Model model, HttpSession session) {
String cartKey = getCartKey(session);
List<CartItem> cartItems = cartService.getCartItems(cartKey);
model.addAttribute("cartItems", cartItems);
model.addAttribute("totalCount", cartService.getCartItemCount(cartKey));
model.addAttribute("totalPrice", cartService.getCartTotalPrice(cartKey));
return "cart/list";
}
/**
* 添加商品到购物车
*/
@PostMapping("/add")
@ResponseBody
public Map<String, Object> addToCart(@RequestParam Long productId,
@RequestParam Integer quantity,
HttpSession session) {
Map<String, Object> result = new HashMap<>();
try {
String cartKey = getCartKey(session);
cartService.addToCart(cartKey, productId, quantity);
result.put("success", true);
result.put("totalCount", cartService.getCartItemCount(cartKey));
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return result;
}
/**
* 更新购物车商品数量
*/
@PostMapping("/update/quantity")
@ResponseBody
public Map<String, Object> updateQuantity(@RequestParam Long productId,
@RequestParam Integer quantity,
HttpSession session) {
Map<String, Object> result = new HashMap<>();
try {
String cartKey = getCartKey(session);
cartService.updateCartItemQuantity(cartKey, productId, quantity);
result.put("success", true);
result.put("totalPrice", cartService.getCartTotalPrice(cartKey));
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return result;
}
/**
* 更新商品选中状态
*/
@PostMapping("/update/selected")
@ResponseBody
public Map<String, Object> updateSelected(@RequestParam Long productId,
@RequestParam Boolean selected,
HttpSession session) {
Map<String, Object> result = new HashMap<>();
try {
String cartKey = getCartKey(session);
cartService.updateCartItemSelected(cartKey, productId, selected);
result.put("success", true);
result.put("totalPrice", cartService.getCartTotalPrice(cartKey));
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return result;
}
/**
* 删除购物车商品
*/
@PostMapping("/delete")
@ResponseBody
public Map<String, Object> deleteItem(@RequestParam Long productId,
HttpSession session) {
Map<String, Object> result = new HashMap<>();
try {
String cartKey = getCartKey(session);
cartService.removeFromCart(cartKey, productId);
result.put("success", true);
result.put("totalCount", cartService.getCartItemCount(cartKey));
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return result;
}
/**
* 清空购物车
*/
@PostMapping("/clear")
@ResponseBody
public Map<String, Object> clearCart(HttpSession session) {
Map<String, Object> result = new HashMap<>();
try {
String cartKey = getCartKey(session);
cartService.clearCart(cartKey);
result.put("success", true);
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return result;
}
}
3.8 登录拦截器(用于购物车合并)
package com.example.shop.interceptor;
import com.example.shop.service.CartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private CartService cartService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
Long userId = (Long) session.getAttribute("userId");
// 用户已登录,需要检查是否有临时购物车需要合并
if (userId != null) {
String tempCartKey = cartService.generateCartKey(null, session.getId());
String userCartKey = cartService.generateCartKey(userId, null);
// 合并临时购物车到用户购物车
cartService.mergeCart(tempCartKey, userCartKey);
}
return true;
}
}
3.9 拦截器配置
package com.example.shop.config;
import com.example.shop.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 排除登录和注册接口
registry.addInterceptor(loginInterceptor())
.excludePathPatterns("/user/login", "/user/register")
.addPathPatterns("/**");
}
}
四、进阶功能
4.1 数据库持久化(可选)
对于需要持久化购物车数据的场景,可以在用户登录或定期将Redis购物车同步到数据库。
package com.example.shop.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("user_cart")
public class UserCart {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long productId;
private Integer quantity;
private Boolean selected;
private Date createTime;
private Date updateTime;
}
4.2 分布式锁处理并发
对于高并发场景,可以使用Redis分布式锁来处理购物车的并发操作:
package com.example.shop.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
@Component
public class RedisLockUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 加锁
*/
public boolean lock(String key, String value, long expireTime) {
return redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
}
/**
* 解锁
*/
public void unlock(String key, String value) {
String currentValue = (String) redisTemplate.opsForValue().get(key);
if (value.equals(currentValue)) {
redisTemplate.delete(key);
}
}
}
4.3 购物车预热
对于活跃用户,可以在系统启动时或定时任务中预热购物车数据,提高用户体验。
五、性能优化建议
- 使用Redis Pipeline:批量操作Redis命令,减少网络往返时间
- 设置合理的过期时间:临时购物车可设置较短过期时间
- 异步处理:购物车合并等操作可考虑异步处理
- 压缩数据:对大型购物车数据考虑压缩存储
- 使用本地缓存:对热门商品信息使用本地缓存
六、安全考虑
- 防止恶意修改:验证购物车数据的合法性
- 防止越权访问:确保用户只能访问自己的购物车
- 防止缓存穿透:对不存在的商品进行缓存处理
- 敏感信息加密:确保商品价格等敏感信息在传输过程中加密
通过以上设计,可以实现一个功能完整、性能良好、安全可靠的SpringBoot购物车系统,满足大多数电商平台的需求。