商城项目-分布式高级-08-购物车功能实现

843 阅读4分钟

购物车

离线购物车

  • 离线的时候保存着用户没有登录时的购物车信息
  • 等用户登录后,离线购物车的内容自动合并到登录用户的购物车内
  • 离线购物车清空

vo封装

购物车的各个属性都需要计算

@Data
public class Cart {
    List<CartItem> items;
    private Integer countNum;           // 商品数量
    private Integer countType;         // 商品类型的个数
    private BigDecimal totalAmount;   // 当前购物车总价格
    private BigDecimal reduce = new BigDecimal(0);       // 优惠价格

    public Integer getCountNum() {
        int count = 0;
        if (items != null && items.size() > 0) {
            for (CartItem item : items) {
                count += item.getCount();
            }
        }
        setCountNum(count);
        return count;
    }

    public void setCountNum(Integer countNum) {
        this.countNum = countNum;
    }

    public Integer getCountType() {
        int count = 0;
        if (items != null && items.size() > 0) {
            for (CartItem item : items) {
                count += 1;
            }
        }
        setCountType(count);
        return countType;
    }

    public void setCountType(Integer countType) {
        this.countType = countType;
    }

    public BigDecimal getTotalAmount() {
        BigDecimal count = new BigDecimal(0);
        if (items != null && items.size() > 0) {
            for (CartItem item : items) {
                count = count.add(item.getTotalPrice();
            }
        }
        count = count.subtract(reduce);
        setTotalAmount(count);
        return totalAmount;
    }

    public void setTotalAmount(BigDecimal totalAmount) {
        this.totalAmount = totalAmount;
    }

    public BigDecimal getReduce() {
        return reduce;
    }

    public void setReduce(BigDecimal reduce) {
        this.reduce = reduce;
    }
}
@Data
public class CartItem {

    private Long skuId;
    private Boolean check = true;
    private String title;
    private String image;
    private List<String> skuAttr;
    private BigDecimal price;
    private Integer count;
    private BigDecimal totalPrice;

    public BigDecimal getTotalPrice() {
        totalPrice = price.multiply(new BigDecimal(count));
        return totalPrice;
    }
}

拦截器判断用户是否登录(threadLocal)

  1. 拦截器判断用户是否登录
  2. 登录保存用户id
  3. 没登录保存用户user-key
  4. 保存用户信息,共享出去

拦截器

@Component
public class CartInterceptor implements HandlerInterceptor {

    // 共享数据
    public static ThreadLocal<UserInfo> userInfoLocal = new ThreadLocal<>();

    /**
     * 方法执行前
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserInfo userInfo = new UserInfo();

        // 封装userInfo
        HttpSession session = request.getSession();
        MemberVo user = (MemberVo) session.getAttribute(AuthConstant.LOGIN_USER);
        if (user != null) {
            // 获取登录用户的购物车 -> userId
            userInfo.setUserId(user.getId());
        }
        // 获取离线购物车 -> user-key
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(CartConstant.User_COOKIE_NAME)) {
                    userInfo.setUserKey(cookie.getValue());
                    userInfo.setTemp(true);
                    break;
                }
            }
        }
        // 用户第一次登录分配一个随机的user-key
        if (StringUtils.isBlank(userInfo.getUserKey())) {
            userInfo.setUserKey(UUID.randomUUID().toString());
        }
        // 目标方法执行前
        userInfoLocal.set(userInfo);
        return true;
    }

    /**
     * 方法执行后
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserInfo userInfo = userInfoLocal.get();

        // 如果是false就表明是第一次
        if (!userInfo.isTemp()) {
            Cookie cookie = new Cookie(CartConstant.User_COOKIE_NAME, userInfo.getUserKey());
            cookie.setDomain("localhost");
            cookie.setMaxAge(CartConstant.COOKIE_TTL);
            response.addCookie(cookie);
        }
    }
}

注册拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器 -> 拦截所有请求
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}

购物车功能(redis保存,异步编排)

controller方法

@GetMapping("/addToCart")
public String addToCart(@RequestParam String skuId, @RequestParam Integer num, Model model) throws ExecutionException, InterruptedException {
    CartItem cartItem = cartService.addToCart(skuId, num);
    model.addAttribute("item", cartItem);
    return "success";
}

service

运用了线程池以及异步编排

@Override
public CartItem addToCart(String skuId, Integer num) throws ExecutionException, InterruptedException {
    BoundHashOperations<String, Object, Object> ops = getCartOps();
    CartItem cartItem;
    
    // 判断这个商品在购物车中是否存在
    Object o = ops.get(JSON.toJSONString(skuId)); // fix 保存格式为json 所以读取格式也要是json
    if (Objects.isNull(o)) {
        cartItem = new CartItem();
        // 添加新商品:
        // 1.查询当前要添加的商品信息
        CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
            R r = productFeignService.info(Long.parseLong(skuId));  // 远程调用
            SkuInfoEntity info = BeanUtil.toBean(r.get("skuInfo"), SkuInfoEntity.class);
            cartItem.setSkuId(info.getSkuId());
            cartItem.setCheck(true);
            cartItem.setTitle(info.getSkuTitle());
            cartItem.setImage(info.getSkuDefaultImg());
            cartItem.setPrice(info.getPrice());
            cartItem.setCount(num);
            cartItem.setTotalPrice(info.getPrice().multiply(new BigDecimal(num)));
        }, thread);
        // 2.查询属性信息
        CompletableFuture<Void> getAttrTask = CompletableFuture.runAsync(() -> {
            List<String> value = productFeignService.getSkuSaleAttrValue(skuId.toString());  // 远程调用
            cartItem.setSkuAttr(value);
        }, thread);
        
        CompletableFuture.allOf(getAttrTask, getSkuInfoTask).get();
    } else {
        // 1.修改数量
        cartItem = (CartItem) o;
        cartItem.setCount(cartItem.getCount() + num);
        cartItem.setTotalPrice(cartItem.getTotalPrice());
    }
    // 3.保存到redis中
    ops.put(JSON.toJSONString(skuId), cartItem);
    
    return cartItem;
}

获取购物车功能

private static final String cart_prefix = "cart:";

/**
 * 获取购物车
 *
 * @return {@link BoundHashOperations<String, Object, Object>}
 */
private BoundHashOperations<String, Object, Object> getCartOps() {
    UserInfo user = CartInterceptor.userInfoLocal.get();

    // 1.生成redis中的key
    StringBuilder cartKey = new StringBuilder(cart_prefix);
    if (user.getUserId() != null) {
        cartKey.append(user.getUserId());
    } else {
        cartKey.append(user.getUserKey());
    }

    BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(cartKey.toString());
    return ops;
}

功能测试

image-20201224190631928

image-20201224190658760

发送请求后:

image-20201224190715301

image-20201224190727885

解决页面刷新,再次发送请求的问题

@Override
public CartItem getCartItem(String skuId) {
    BoundHashOperations<String, Object, Object> ops = getCartOps();
    String s = (String) ops.get(JSON.toJSONString(skuId));
    return JSON.parseObject(s, new TypeReference<CartItem>() {});
}

image-20201224203550749

增加用户登录后合并购物车功能

/**
 * 购物车列表
 * 浏览器有一个cookie:user-key,用来表示用户的身份
 * 登录:按session
 * 没有登录:user-key
 * 第一次:创建user-key
 *
 * @return {@link String}
 */
@GetMapping("/cartList.html")
public String cartList(Model model) throws ExecutionException, InterruptedException {
    // 获取当前登录用户的信息
    Cart cart = cartService.getCart();
    model.addAttribute("cart",cart);
    return "cartList";
}
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
    UserInfo user = CartInterceptor.userInfoLocal.get();
    Cart cart = new Cart();

    // 1.获取离线购物车
    List<CartItem> items = getCartItems(cart_prefix+user.getUserKey());
    // 判断离线购物车中是否有内容
    if (items != null && items.size() > 0) {
        // 2.获取登录购物车
        Long userId = user.getUserId();
        if (userId != null) {
            // 3.用戶已经登录->合并购物车->清空离线购物车
            for (CartItem cartItem : items) {
                addItemToCart(cartItem.getSkuId().toString(),cartItem.getCount());  // 合并购物车
            }
            deleteCart(cart_prefix+ user.getUserKey());  // 清空离线购物车
            items = getCartItems(cart_prefix + userId);   // 获取合并后的购物车内容
        }
    }
    cart.setItems(items);

    return cart;
}

/**
 * 删除购物车
 *
 * @param key user key
 */
private void deleteCart(String key) {
    redisTemplate.delete(key);
}

/**
 * 根据购物项的key,获取对应购物项
 *
 * @param key 关键
 * @return {@link List<CartItem>}
 */
private List<CartItem> getCartItems(String key) {
    BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(key);
    List<Object> values = ops.values();
    if (values != null && values.size() > 0)
        return values.stream()
                .map(s -> (CartItem) s)
                .collect(Collectors.toList());
    return null;
}

修复用户登录后获取购物车失败

@Override
public Cart getCart() throws ExecutionException, InterruptedException {
    UserInfo user = CartInterceptor.userInfoLocal.get();
    System.out.println(user);
    Cart cart = new Cart();

    // 1.获取离线购物车
    List<CartItem> items = getCartItems(cart_prefix + user.getUserKey());
    // 判断离线购物车中是否有内容

    // 2.获取登录购物车
    Long userId = user.getUserId();
    if (userId != null) {
        // 3.用戶已经登录->合并购物车->清空离线购物车
        if (items != null && items.size() > 0) {
            for (CartItem cartItem : items) {
                addItemToCart(cartItem.getSkuId().toString(), cartItem.getCount());  // 合并购物车
            }
        }
        deleteCart(cart_prefix + user.getUserKey());  // 清空离线购物车
        items = getCartItems(cart_prefix + userId);   // 获取合并后的购物车内容
    }

    cart.setItems(items);

    return cart;
}

image-20201226174312259