商城系统购物车逻辑优化:避免下单重复提交问题
问题背景
在电商系统中,购物车操作需要特别注意幂等性问题。用户在提交订单时可能会因为网络延迟或操作重复导致重复下单,这需要我们在系统设计中加入幂等性保护机制。
购物车控制器逻辑
package com.mall.shoppingcart.controller;
import com.mall.shoppingcart.service.ShoppingCartService;
import com.mall.shoppingcart.vo.ShoppingCartItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.concurrent.ExecutionException;
@Controller
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
@GetMapping("/addToCart")
public String addToCart(@RequestParam("productId") Long productId, @RequestParam("amount") Integer amount, RedirectAttributes model) throws ExecutionException, InterruptedException {
if(amount > 0) {
ShoppingCartItem cartItem = shoppingCartService.addProductToCart(productId, amount);
model.addAttribute("productId", productId);
return "redirect:http://cart.mall.com/addToCartSuccess.html";
}
return "redirect:http://cart.mall.com/addToCartSuccess.html";
}
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam("productId") Long productId, Model model) {
ShoppingCartItem cartItem = shoppingCartService.getProductFromCart(productId);
model.addAttribute("item", cartItem);
return "success";
}
}
购物车服务接口
package com.mall.shoppingcart.service;
import com.mall.shoppingcart.vo.ShoppingCartItem;
public interface ShoppingCartService {
ShoppingCartItem addProductToCart(Long productId, Integer amount) throws ExecutionException, InterruptedException;
ShoppingCartItem getProductFromCart(Long productId);
}
购物车服务实现
package com.mall.shoppingcart.service.impl;
import com.mall.common.utils.R;
import com.mall.shoppingcart.feign.ProductFeignService;
import com.mall.shoppingcart.service.ShoppingCartService;
import com.mall.shoppingcart.vo.ShoppingCartItem;
import com.mall.shoppingcart.vo.ProductInfoVo;
import com.mall.shoppingcart.vo.UserContext;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
@Slf4j
@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ProductFeignService productFeignService;
@Autowired
private ThreadPoolExecutor executor;
private final String SHOPPING_CART_PREFIX = "mallshoppingcart:";
@Override
public ShoppingCartItem addProductToCart(Long productId, Integer amount) throws ExecutionException, InterruptedException {
BoundHashOperations cartOps = getCartOperations();
String existingItem = (String) cartOps.get(productId.toString());
if (StringUtils.isEmpty(existingItem)) {
ShoppingCartItem newItem = new ShoppingCartItem();
CompletableFuture<Void> getProductInfo = CompletableFuture.runAsync(() -> {
R info = productFeignService.fetchProductInfo(productId);
if (info.getCode() == 0) {
ProductInfoVo product = info.get("product", new TypeReference<ProductInfoVo>() {});
newItem.setChecked(true);
newItem.setQuantity(amount);
newItem.setImage(product.getDefaultImage());
newItem.setTitle(product.getTitle());
newItem.setProductId(productId);
newItem.setPrice(product.getPrice());
}
}, executor);
CompletableFuture<Void> getAttrValues = CompletableFuture.runAsync(() -> {
List<String> attrs = productFeignService.fetchSkuAttrValues(productId);
newItem.setSkuAttributes(attrs);
}, executor);
CompletableFuture.allOf(getProductInfo, getAttrValues).get();
String json = JSON.toJSONString(newItem);
cartOps.put(productId.toString(), json);
return newItem;
} else {
ShoppingCartItem existingItemObj = JSON.parseObject(existingItem, ShoppingCartItem.class);
existingItemObj.setQuantity(existingItemObj.getQuantity() + amount);
cartOps.put(productId.toString(), JSON.toJSONString(existingItemObj));
return existingItemObj;
}
}
@Override
public ShoppingCartItem getProductFromCart(Long productId) {
BoundHashOperations cartOps = getCartOperations();
String itemStr = (String) cartOps.get(productId.toString());
return JSON.parseObject(itemStr, ShoppingCartItem.class);
}
private BoundHashOperations getCartOperations() {
UserContext userContext = UserContextHolder.getContext();
String cartKey = "";
if (userContext.getUserId() != null) {
cartKey = SHOPPING_CART_PREFIX + userContext.getUserId();
} else {
cartKey = SHOPPING_CART_PREFIX + userContext.getUserKey();
}
return redisTemplate.boundHashOps(cartKey);
}
}