电商购物车系统设计与实现
2024/1/1
# 电商购物车系统设计与实现
# 系统概述
购物车系统是电商平台的重要组成部分,为用户提供商品临时存储、数量管理、价格计算等功能。一个优秀的购物车系统需要支持高并发访问、数据持久化、实时同步等特性,同时要考虑用户体验和系统性能的平衡。
# 购物车架构设计
# 存储方案对比
存储方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Cookie存储 | 无需登录、减轻服务器压力 | 容量限制、安全性差 | 简单场景 |
Session存储 | 服务器控制、相对安全 | 占用内存、不支持分布式 | 单机应用 |
数据库存储 | 数据持久化、支持复杂查询 | 性能较低、并发能力有限 | 数据重要性高 |
Redis存储 | 高性能、支持分布式 | 数据可能丢失 | 高并发场景 |
混合存储 | 兼顾性能和可靠性 | 实现复杂 | 生产环境推荐 |
# 推荐架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Web前端 │ │ 移动端APP │ │ 小程序 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌─────────────────────────────────────────────────┐
│ 购物车网关层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 用户认证 │ │ 参数验证 │ │ 限流控制 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────┐
│ 购物车服务层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 购物车管理 │ │ 商品验证 │ │ 价格计算 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────┐
│ 数据存储层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Redis缓存 │ │ MySQL数据库 │ │ 商品服务 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────┘
# 核心实体设计
# 1. 购物车实体
@Entity
@Table(name = "shopping_carts")
public class ShoppingCart {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long userId;
@Column(nullable = false)
private Long productId;
@Column(nullable = false, length = 200)
private String productName;
@Column(length = 500)
private String productImage;
@Column(length = 200)
private String productSpec;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice;
@Column(nullable = false)
private Integer quantity;
@Column(nullable = false)
private Boolean selected = true;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
// 计算总价
public BigDecimal getTotalPrice() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}
# 2. 购物车DTO
@Data
public class CartItemDTO {
private Long id;
private Long productId;
private String productName;
private String productImage;
private String productSpec;
private BigDecimal unitPrice;
private Integer quantity;
private Boolean selected;
private BigDecimal totalPrice;
private Boolean available; // 商品是否可用
private Integer stock; // 库存数量
private LocalDateTime addTime;
}
@Data
public class CartSummaryDTO {
private List<CartItemDTO> items;
private Integer totalItems; // 总商品种类数
private Integer totalQuantity; // 总商品数量
private BigDecimal totalAmount; // 总金额
private BigDecimal selectedAmount; // 选中商品总金额
private Integer selectedCount; // 选中商品数量
}
# 购物车服务实现
# 1. 核心服务类
@Service
public class ShoppingCartService {
@Autowired
private ShoppingCartRepository cartRepository;
@Autowired
private ProductService productService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private CartCacheService cartCacheService;
private static final String CART_KEY_PREFIX = "cart:user:";
/**
* 添加商品到购物车
*/
@Transactional
public void addToCart(Long userId, Long productId, Integer quantity, String spec) {
// 1. 验证商品信息
Product product = productService.findById(productId);
if (product == null || product.getStatus() != ProductStatus.ACTIVE) {
throw new BusinessException("商品不存在或已下架");
}
// 2. 检查库存
if (product.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 3. 查找是否已存在相同商品
ShoppingCart existingItem = cartRepository.findByUserIdAndProductIdAndProductSpec(
userId, productId, spec);
if (existingItem != null) {
// 更新数量
int newQuantity = existingItem.getQuantity() + quantity;
if (newQuantity > product.getStock()) {
throw new BusinessException("购物车中商品数量超过库存");
}
existingItem.setQuantity(newQuantity);
cartRepository.save(existingItem);
} else {
// 新增购物车项
ShoppingCart cartItem = new ShoppingCart();
cartItem.setUserId(userId);
cartItem.setProductId(productId);
cartItem.setProductName(product.getName());
cartItem.setProductImage(product.getMainImage());
cartItem.setProductSpec(spec);
cartItem.setUnitPrice(product.getPrice());
cartItem.setQuantity(quantity);
cartRepository.save(cartItem);
}
// 4. 更新缓存
cartCacheService.refreshUserCart(userId);
// 5. 发布事件
publishCartChangedEvent(userId, CartEventType.ADD_ITEM);
}
/**
* 更新购物车商品数量
*/
@Transactional
public void updateQuantity(Long userId, Long cartItemId, Integer quantity) {
ShoppingCart cartItem = cartRepository.findByIdAndUserId(cartItemId, userId);
if (cartItem == null) {
throw new BusinessException("购物车商品不存在");
}
if (quantity <= 0) {
// 删除商品
removeFromCart(userId, cartItemId);
return;
}
// 验证库存
Product product = productService.findById(cartItem.getProductId());
if (product.getStock() < quantity) {
throw new BusinessException("库存不足");
}
cartItem.setQuantity(quantity);
cartItem.setUnitPrice(product.getPrice()); // 更新最新价格
cartRepository.save(cartItem);
// 更新缓存
cartCacheService.refreshUserCart(userId);
publishCartChangedEvent(userId, CartEventType.UPDATE_QUANTITY);
}
/**
* 从购物车移除商品
*/
@Transactional
public void removeFromCart(Long userId, Long cartItemId) {
ShoppingCart cartItem = cartRepository.findByIdAndUserId(cartItemId, userId);
if (cartItem != null) {
cartRepository.delete(cartItem);
cartCacheService.refreshUserCart(userId);
publishCartChangedEvent(userId, CartEventType.REMOVE_ITEM);
}
}
/**
* 批量删除购物车商品
*/
@Transactional
public void batchRemove(Long userId, List<Long> cartItemIds) {
List<ShoppingCart> cartItems = cartRepository.findByIdInAndUserId(cartItemIds, userId);
if (!cartItems.isEmpty()) {
cartRepository.deleteAll(cartItems);
cartCacheService.refreshUserCart(userId);
publishCartChangedEvent(userId, CartEventType.BATCH_REMOVE);
}
}
/**
* 选择/取消选择商品
*/
@Transactional
public void selectItems(Long userId, List<Long> cartItemIds, Boolean selected) {
List<ShoppingCart> cartItems = cartRepository.findByIdInAndUserId(cartItemIds, userId);
cartItems.forEach(item -> item.setSelected(selected));
cartRepository.saveAll(cartItems);
cartCacheService.refreshUserCart(userId);
publishCartChangedEvent(userId, CartEventType.SELECT_CHANGE);
}
/**
* 全选/全不选
*/
@Transactional
public void selectAll(Long userId, Boolean selected) {
List<ShoppingCart> cartItems = cartRepository.findByUserId(userId);
cartItems.forEach(item -> item.setSelected(selected));
cartRepository.saveAll(cartItems);
cartCacheService.refreshUserCart(userId);
publishCartChangedEvent(userId, CartEventType.SELECT_ALL);
}
/**
* 获取用户购物车
*/
public CartSummaryDTO getUserCart(Long userId) {
// 先从缓存获取
CartSummaryDTO cachedCart = cartCacheService.getUserCart(userId);
if (cachedCart != null) {
return cachedCart;
}
// 从数据库获取
List<ShoppingCart> cartItems = cartRepository.findByUserIdOrderByCreatedAtDesc(userId);
CartSummaryDTO cartSummary = buildCartSummary(cartItems);
// 更新缓存
cartCacheService.cacheUserCart(userId, cartSummary);
return cartSummary;
}
/**
* 构建购物车摘要
*/
private CartSummaryDTO buildCartSummary(List<ShoppingCart> cartItems) {
List<CartItemDTO> itemDTOs = new ArrayList<>();
int totalQuantity = 0;
int selectedCount = 0;
BigDecimal totalAmount = BigDecimal.ZERO;
BigDecimal selectedAmount = BigDecimal.ZERO;
for (ShoppingCart item : cartItems) {
CartItemDTO dto = convertToDTO(item);
// 验证商品可用性
Product product = productService.findById(item.getProductId());
dto.setAvailable(product != null && product.getStatus() == ProductStatus.ACTIVE);
dto.setStock(product != null ? product.getStock() : 0);
// 更新价格(如果商品价格有变化)
if (product != null && !item.getUnitPrice().equals(product.getPrice())) {
dto.setUnitPrice(product.getPrice());
dto.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
}
itemDTOs.add(dto);
totalQuantity += item.getQuantity();
totalAmount = totalAmount.add(dto.getTotalPrice());
if (item.getSelected() && dto.getAvailable()) {
selectedCount += item.getQuantity();
selectedAmount = selectedAmount.add(dto.getTotalPrice());
}
}
CartSummaryDTO summary = new CartSummaryDTO();
summary.setItems(itemDTOs);
summary.setTotalItems(cartItems.size());
summary.setTotalQuantity(totalQuantity);
summary.setTotalAmount(totalAmount);
summary.setSelectedCount(selectedCount);
summary.setSelectedAmount(selectedAmount);
return summary;
}
}
# 2. 缓存服务
@Service
public class CartCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ShoppingCartRepository cartRepository;
private static final String CART_KEY_PREFIX = "cart:user:";
private static final int CACHE_EXPIRE_SECONDS = 3600; // 1小时
/**
* 缓存用户购物车
*/
public void cacheUserCart(Long userId, CartSummaryDTO cartSummary) {
String key = CART_KEY_PREFIX + userId;
redisTemplate.opsForValue().set(key, cartSummary, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
}
/**
* 获取缓存的购物车
*/
public CartSummaryDTO getUserCart(Long userId) {
String key = CART_KEY_PREFIX + userId;
return (CartSummaryDTO) redisTemplate.opsForValue().get(key);
}
/**
* 刷新用户购物车缓存
*/
public void refreshUserCart(Long userId) {
String key = CART_KEY_PREFIX + userId;
redisTemplate.delete(key);
}
/**
* 预热购物车缓存
*/
@Async
public void preloadUserCart(Long userId) {
CartSummaryDTO cachedCart = getUserCart(userId);
if (cachedCart == null) {
List<ShoppingCart> cartItems = cartRepository.findByUserIdOrderByCreatedAtDesc(userId);
// 构建购物车摘要并缓存
// ... 实现逻辑
}
}
}
# 3. 购物车同步服务
@Service
public class CartSyncService {
@Autowired
private ShoppingCartService cartService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 合并游客购物车到用户购物车
*/
@Transactional
public void mergeGuestCart(String guestCartId, Long userId) {
// 获取游客购物车
List<CartItemDTO> guestItems = getGuestCartItems(guestCartId);
if (guestItems.isEmpty()) {
return;
}
// 获取用户购物车
CartSummaryDTO userCart = cartService.getUserCart(userId);
Map<String, CartItemDTO> userItemMap = userCart.getItems().stream()
.collect(Collectors.toMap(
item -> item.getProductId() + "_" + item.getProductSpec(),
Function.identity()
));
// 合并购物车
for (CartItemDTO guestItem : guestItems) {
String key = guestItem.getProductId() + "_" + guestItem.getProductSpec();
CartItemDTO userItem = userItemMap.get(key);
if (userItem != null) {
// 商品已存在,更新数量
int newQuantity = userItem.getQuantity() + guestItem.getQuantity();
cartService.updateQuantity(userId, userItem.getId(), newQuantity);
} else {
// 新商品,添加到购物车
cartService.addToCart(userId, guestItem.getProductId(),
guestItem.getQuantity(), guestItem.getProductSpec());
}
}
// 清除游客购物车
clearGuestCart(guestCartId);
}
/**
* 获取游客购物车商品
*/
private List<CartItemDTO> getGuestCartItems(String guestCartId) {
String key = "guest:cart:" + guestCartId;
Object cartData = redisTemplate.opsForValue().get(key);
if (cartData instanceof List) {
return (List<CartItemDTO>) cartData;
}
return Collections.emptyList();
}
/**
* 清除游客购物车
*/
private void clearGuestCart(String guestCartId) {
String key = "guest:cart:" + guestCartId;
redisTemplate.delete(key);
}
}
# 购物车API接口
# 1. 控制器实现
@RestController
@RequestMapping("/api/cart")
public class ShoppingCartController {
@Autowired
private ShoppingCartService cartService;
@Autowired
private CartSyncService cartSyncService;
/**
* 添加商品到购物车
*/
@PostMapping("/add")
public ApiResponse<Void> addToCart(@RequestBody @Valid AddToCartRequest request,
HttpServletRequest httpRequest) {
Long userId = getCurrentUserId(httpRequest);
cartService.addToCart(userId, request.getProductId(),
request.getQuantity(), request.getSpec());
return ApiResponse.success();
}
/**
* 获取购物车
*/
@GetMapping
public ApiResponse<CartSummaryDTO> getCart(HttpServletRequest request) {
Long userId = getCurrentUserId(request);
CartSummaryDTO cart = cartService.getUserCart(userId);
return ApiResponse.success(cart);
}
/**
* 更新商品数量
*/
@PutMapping("/quantity")
public ApiResponse<Void> updateQuantity(@RequestBody @Valid UpdateQuantityRequest request,
HttpServletRequest httpRequest) {
Long userId = getCurrentUserId(httpRequest);
cartService.updateQuantity(userId, request.getCartItemId(), request.getQuantity());
return ApiResponse.success();
}
/**
* 删除购物车商品
*/
@DeleteMapping("/items/{itemId}")
public ApiResponse<Void> removeItem(@PathVariable Long itemId,
HttpServletRequest request) {
Long userId = getCurrentUserId(request);
cartService.removeFromCart(userId, itemId);
return ApiResponse.success();
}
/**
* 批量删除
*/
@DeleteMapping("/items/batch")
public ApiResponse<Void> batchRemove(@RequestBody @Valid BatchRemoveRequest request,
HttpServletRequest httpRequest) {
Long userId = getCurrentUserId(httpRequest);
cartService.batchRemove(userId, request.getItemIds());
return ApiResponse.success();
}
/**
* 选择商品
*/
@PutMapping("/select")
public ApiResponse<Void> selectItems(@RequestBody @Valid SelectItemsRequest request,
HttpServletRequest httpRequest) {
Long userId = getCurrentUserId(httpRequest);
cartService.selectItems(userId, request.getItemIds(), request.getSelected());
return ApiResponse.success();
}
/**
* 全选/全不选
*/
@PutMapping("/select/all")
public ApiResponse<Void> selectAll(@RequestParam Boolean selected,
HttpServletRequest request) {
Long userId = getCurrentUserId(request);
cartService.selectAll(userId, selected);
return ApiResponse.success();
}
/**
* 合并游客购物车
*/
@PostMapping("/merge")
public ApiResponse<Void> mergeGuestCart(@RequestParam String guestCartId,
HttpServletRequest request) {
Long userId = getCurrentUserId(request);
cartSyncService.mergeGuestCart(guestCartId, userId);
return ApiResponse.success();
}
/**
* 获取购物车商品数量
*/
@GetMapping("/count")
public ApiResponse<Integer> getCartCount(HttpServletRequest request) {
Long userId = getCurrentUserId(request);
CartSummaryDTO cart = cartService.getUserCart(userId);
return ApiResponse.success(cart.getTotalQuantity());
}
}
# 2. 请求DTO
@Data
public class AddToCartRequest {
@NotNull(message = "商品ID不能为空")
private Long productId;
@NotNull(message = "数量不能为空")
@Min(value = 1, message = "数量必须大于0")
private Integer quantity;
private String spec; // 商品规格
}
@Data
public class UpdateQuantityRequest {
@NotNull(message = "购物车项ID不能为空")
private Long cartItemId;
@NotNull(message = "数量不能为空")
@Min(value = 0, message = "数量不能小于0")
private Integer quantity;
}
@Data
public class BatchRemoveRequest {
@NotEmpty(message = "商品ID列表不能为空")
private List<Long> itemIds;
}
@Data
public class SelectItemsRequest {
@NotEmpty(message = "商品ID列表不能为空")
private List<Long> itemIds;
@NotNull(message = "选择状态不能为空")
private Boolean selected;
}
# 购物车优化策略
# 1. 性能优化
@Component
public class CartPerformanceOptimizer {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 批量获取商品信息
*/
public Map<Long, Product> batchGetProducts(List<Long> productIds) {
// 先从缓存获取
Map<Long, Product> cachedProducts = new HashMap<>();
List<Long> uncachedIds = new ArrayList<>();
for (Long productId : productIds) {
String key = "product:" + productId;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
cachedProducts.put(productId, product);
} else {
uncachedIds.add(productId);
}
}
// 从数据库批量获取未缓存的商品
if (!uncachedIds.isEmpty()) {
List<Product> products = productService.findByIds(uncachedIds);
for (Product product : products) {
cachedProducts.put(product.getId(), product);
// 缓存商品信息
String key = "product:" + product.getId();
redisTemplate.opsForValue().set(key, product, 300, TimeUnit.SECONDS);
}
}
return cachedProducts;
}
/**
* 异步更新购物车价格
*/
@Async
public void asyncUpdateCartPrices(Long userId) {
List<ShoppingCart> cartItems = cartRepository.findByUserId(userId);
List<Long> productIds = cartItems.stream()
.map(ShoppingCart::getProductId)
.collect(Collectors.toList());
Map<Long, Product> products = batchGetProducts(productIds);
boolean hasUpdate = false;
for (ShoppingCart item : cartItems) {
Product product = products.get(item.getProductId());
if (product != null && !item.getUnitPrice().equals(product.getPrice())) {
item.setUnitPrice(product.getPrice());
hasUpdate = true;
}
}
if (hasUpdate) {
cartRepository.saveAll(cartItems);
cartCacheService.refreshUserCart(userId);
}
}
}
# 2. 数据一致性保证
@Component
public class CartConsistencyManager {
@Autowired
private ShoppingCartRepository cartRepository;
@Autowired
private ProductService productService;
/**
* 定时清理无效购物车项
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void cleanInvalidCartItems() {
// 清理下架商品
List<ShoppingCart> invalidItems = cartRepository.findInvalidItems();
if (!invalidItems.isEmpty()) {
cartRepository.deleteAll(invalidItems);
log.info("清理无效购物车项:{} 条", invalidItems.size());
}
// 清理过期购物车(超过30天未更新)
LocalDateTime expireTime = LocalDateTime.now().minusDays(30);
List<ShoppingCart> expiredItems = cartRepository.findByUpdatedAtBefore(expireTime);
if (!expiredItems.isEmpty()) {
cartRepository.deleteAll(expiredItems);
log.info("清理过期购物车项:{} 条", expiredItems.size());
}
}
/**
* 验证购物车数据一致性
*/
public void validateCartConsistency(Long userId) {
List<ShoppingCart> cartItems = cartRepository.findByUserId(userId);
List<Long> productIds = cartItems.stream()
.map(ShoppingCart::getProductId)
.collect(Collectors.toList());
Map<Long, Product> products = productService.findByIdsMap(productIds);
for (ShoppingCart item : cartItems) {
Product product = products.get(item.getProductId());
// 检查商品是否存在
if (product == null) {
cartRepository.delete(item);
continue;
}
// 检查商品状态
if (product.getStatus() != ProductStatus.ACTIVE) {
cartRepository.delete(item);
continue;
}
// 检查库存
if (item.getQuantity() > product.getStock()) {
item.setQuantity(Math.max(1, product.getStock()));
cartRepository.save(item);
}
// 检查价格
if (!item.getUnitPrice().equals(product.getPrice())) {
item.setUnitPrice(product.getPrice());
cartRepository.save(item);
}
}
}
}
# 3. 购物车事件处理
@Component
public class CartEventListener {
@Autowired
private NotificationService notificationService;
@Autowired
private UserBehaviorService userBehaviorService;
@Autowired
private RecommendationService recommendationService;
/**
* 购物车变更事件处理
*/
@EventListener
@Async
public void handleCartChanged(CartChangedEvent event) {
Long userId = event.getUserId();
CartEventType eventType = event.getEventType();
// 记录用户行为
userBehaviorService.recordBehavior(userId, "CART_" + eventType.name(),
event.getProductId());
// 更新推荐算法
if (eventType == CartEventType.ADD_ITEM) {
recommendationService.updateUserPreference(userId, event.getProductId());
}
// 发送通知(如购物车商品降价提醒)
if (eventType == CartEventType.PRICE_CHANGE) {
notificationService.sendPriceDropNotification(userId, event.getProductId());
}
}
/**
* 购物车商品库存不足事件
*/
@EventListener
@Async
public void handleStockShortage(StockShortageEvent event) {
// 通知用户库存不足
notificationService.sendStockShortageNotification(
event.getUserId(), event.getProductId());
// 推荐替代商品
recommendationService.recommendAlternativeProducts(
event.getUserId(), event.getProductId());
}
}
# 数据库设计优化
# 1. 索引设计
-- 购物车表索引
CREATE INDEX idx_cart_user_id ON shopping_carts(user_id);
CREATE INDEX idx_cart_user_product ON shopping_carts(user_id, product_id);
CREATE INDEX idx_cart_user_product_spec ON shopping_carts(user_id, product_id, product_spec);
CREATE INDEX idx_cart_updated_at ON shopping_carts(updated_at);
CREATE INDEX idx_cart_created_at ON shopping_carts(created_at);
# 2. 分表策略
@Component
public class CartShardingStrategy {
/**
* 根据用户ID分表
*/
public String getTableName(Long userId) {
int shardIndex = (int) (userId % 10);
return "shopping_carts_" + shardIndex;
}
/**
* 获取所有分表名
*/
public List<String> getAllTableNames() {
List<String> tableNames = new ArrayList<>();
for (int i = 0; i < 10; i++) {
tableNames.add("shopping_carts_" + i);
}
return tableNames;
}
}
# 总结
购物车系统的关键设计要点:
- 存储策略:采用Redis+MySQL混合存储,兼顾性能和可靠性
- 数据一致性:定时同步、实时验证、异常处理机制
- 性能优化:缓存策略、批量操作、异步处理
- 用户体验:游客购物车、登录合并、实时更新
- 扩展性:事件驱动、分表分库、微服务架构
- 数据清理:定时清理过期和无效数据
通过以上设计,可以构建一个高性能、高可用的购物车系统,为用户提供流畅的购物体验。