• 谷粒商城 高级篇 (十八) --------- 购物车



    一、环境搭建

    创建购物车模块。。。

    在这里插入图片描述

    添加依赖。。。

    在这里插入图片描述

    创建完成后配置域名映射。。。。

    在这里插入图片描述

    将静态资源上传至 nginx 做动静分离。。。

    在这里插入图片描述
    将页面复制到项目的 template 文件夹下。。。注意改静态资源路径

    在这里插入图片描述
    将服务注册到注册中心。。。

    在这里插入图片描述
    排除数据库的相关配置。。。
    在这里插入图片描述
    配置 nacos 注册中心地址。。。

    在这里插入图片描述
    开启服务的注册与发现。。。

    在这里插入图片描述

    网关配置如下。。。
    在这里插入图片描述

    二、购物车需求

    A、需求描述

    用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】

    • 放入数据库
    • mongodb
    • 放入 redis(采用)

    登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车。

    用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】

    • 放入 localstorage(客户端存储,后台不存)
    • cookie
    • WebSQL
    • 放入 redis(采用)

    浏览器即使关闭,下次进入,临时购物车数据都在。

    用户可以使用购物车一起结算下单:

    • 给购物车添加商品
    • 用户可以查询自己的购物车
    • 用户可以在购物车中修改购买商品的数量。
    • 用户可以在购物车中删除商品。
    • 选中不选中商品
    • 在购物车中展示商品优惠信息
    • 提示购物车商品价格变化

    B、数据结构

    在这里插入图片描述
    因此每一个购物项信息,都是一个对象,基本字段包括:

    {
    	skuId: 2131241, 
    	check: true, 
    	title: "Apple iphone.....", 
    	defaultImage: "...", 
    	price: 4999, 
    	count: 1, 
    	totalPrice: 4999, 
    	skuSaleVO: {...}
    }
    

    另外,购物车中不止一条数据,因此最终会是对象的数组。即:

    [
    	{...},{...},{...}
    ]
    

    整个购物车封装成的 VO 如下:

    package com.fancy.gulimall.cart.vo;
    
    import java.math.BigDecimal;
    import java.util.List;
    
    /**
     * 整个购物车
     * 需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算
     */
    public class Cart {
    
        List<CartItem> items;
    
        private Integer countNum;//商品数量
    
        private Integer countType;//商品类型数量
    
        private BigDecimal totalAmount;//商品总价
    
        private BigDecimal reduce = new BigDecimal("0.00");//减免价格
    
        public List<CartItem> getItems() {
            return items;
        }
    
        public void setItems(List<CartItem> items) {
            this.items = items;
        }
    
        public Integer getCountNum() {
            int count = 0;
            if (items != null && items.size() > 0) {
                for (CartItem item : items) {
                    count += item.getCount();
                }
            }
            return count;
        }
    
    
        public Integer getCountType() {
            int count = 0;
            if (items != null && items.size() > 0) {
                for (CartItem item : items) {
                    count += 1;
                }
            }
            return count;
        }
    
    
        public BigDecimal getTotalAmount() {
            BigDecimal amount = new BigDecimal("0");
            //1、计算购物项总价
            if (items != null && items.size() > 0) {
                for (CartItem item : items) {
                    if(item.getCheck()){
                        BigDecimal totalPrice = item.getTotalPrice();
                        amount = amount.add(totalPrice);
                    }
                }
            }
    
            //2、减去优惠总价
            BigDecimal subtract = amount.subtract(getReduce());
    
            return subtract;
        }
    
    
        public BigDecimal getReduce() {
            return reduce;
        }
    
        public void setReduce(BigDecimal reduce) {
            this.reduce = reduce;
        }
    }
    

    CartItem 购物项封装成的 VO 如下

    package com.fancy.gulimall.cart.vo;
    
    import lombok.Data;
    
    import java.math.BigDecimal;
    import java.util.List;
    
    /**
    * 购物项内容
    */
    
    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 Long getSkuId() {
            return skuId;
        }
    
        public void setSkuId(Long skuId) {
            this.skuId = skuId;
        }
    
        public Boolean getCheck() {
            return check;
        }
    
        public void setCheck(Boolean check) {
            this.check = check;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public String getImage() {
            return image;
        }
    
        public void setImage(String image) {
            this.image = image;
        }
    
        public List<String> getSkuAttr() {
            return skuAttr;
        }
    
        public void setSkuAttr(List<String> skuAttr) {
            this.skuAttr = skuAttr;
        }
    
        public BigDecimal getPrice() {
            return price;
        }
    
        public void setPrice(BigDecimal price) {
            this.price = price;
        }
    
        public Integer getCount() {
            return count;
        }
    
        public void setCount(Integer count) {
            this.count = count;
        }
    
        /**
         * 计算当前项的总价
         * @return
         */
        public BigDecimal getTotalPrice() {
    
            return this.price.multiply(new BigDecimal("" + this.count));
        }
    
        public void setTotalPrice(BigDecimal totalPrice) {
            this.totalPrice = totalPrice;
        }
    }
    

    Redis 有 5 种不同数据结构,这里选择哪一种比较合适呢?Map>

    首先不同用户应该有独立的购物车,因此购物车应该以用户的作为 key 来存储,value 是用户的所有购物车信息。这样看来基本的k-v结构就可以了。

    但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,为了方便后期处理,我们的购物车也应该是k-v结构,key 是商品 id,value 才是这个商品的购物车信息。

    综上所述,我们的购物车结构是一个双层 Map:Map>

    • 第一层 Map,Key 是用户 id
    • 第二层 Map,Key 是购物车中商品 id,值是购物项数据

    三、流程

    两个功能:新增商品到购物车、查询购物车。
    新增商品:判断是否登录

    • 是:则添加商品到后台 Redis 中,把 user 的唯一标识符作为 key。
    • 否:则添加商品到后台 redis 中,使用随机生成的 user-key 作为 key。

    查询购物车列表:判断是否登录

    • 否:直接根据 user-key 查询 redis 中数据并展示
    • 是:已登录,则需要先根据 user-key 查询 redis 是否有数据。
      • 有:需要提交到后台添加到 redis,合并数据,而后查询。
      • 否:直接去后台查询 redis,而后返回。

    1. 身份鉴别

    进入购物车之前我们需要进行身份鉴别,无登录状态操作的就是离线购物车,登录状态时操作的是用户购物车,离线购物车添加完商品之后登录将进行合并购物车。。。。

    在这里插入图片描述

    因此,在执行目标方法之前,我们需要判断用户的登录状态,我们在拦截器中进行此操作。。。

    package com.fancy.gulimall.cart.interceptor;
    
    import com.alibaba.nacos.client.utils.StringUtils;
    import com.fancy.common.constant.AuthServerConstant;
    import com.fancy.common.constant.CartConstant;
    import com.fancy.common.vo.MemberRespVo;
    import com.fancy.gulimall.cart.vo.UserInfoTo;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.util.UUID;
    
    
    // 在执行目标方法之前,判断用户的登录状态。并封装传递(用户信息)给controller
    public class CartInterceptor implements HandlerInterceptor {
        public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            UserInfoTo userInfoTo = new UserInfoTo();
    
            HttpSession session = request.getSession();
            MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
            if(member != null){
                //用户登录
                userInfoTo.setUserId(member.getId());
            }
    
            Cookie[] cookies = request.getCookies();
            if(cookies!=null && cookies.length>0){
                for (Cookie cookie : cookies) {
                    //user-key
                    String name = cookie.getName();
                    if(name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
                        userInfoTo.setUserKey(cookie.getValue());
                        userInfoTo.setTempUser(true);
                    }
                }
            }
    
            //如果没有临时用户一定分配一个临时用户
            if(StringUtils.isEmpty(userInfoTo.getUserKey())){
                String uuid = UUID.randomUUID().toString();
                userInfoTo.setUserKey(uuid);
            }
            //目标方法执行之前
            threadLocal.set(userInfoTo);
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            UserInfoTo userInfoTo = threadLocal.get();
            //如果没有临时用户一定保存一个临时用户
            if(!userInfoTo.isTempUser()){
                //持续的延长临时用户的过期时间
                Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
                cookie.setDomain("gulimall.com");
                cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
                response.addCookie(cookie);
            }
        }
        
    }
    

    拦截器配置:

    package com.fancy.gulimall.cart.config;
    
    
    import com.fancy.gulimall.cart.interceptor.CartInterceptor;
    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 GulimallWebConfig implements WebMvcConfigurer {
    
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
        }
    }
    

    而且,因为用户鉴别和鉴别完之后执行操作是同一个线程内的,我们可以使用 ThreadLocal 将需要共享的数据放在这个线程中去。。。,详见如上代码。。。

    2. 临时与登录购物车

    A、临时购物车

    /**
    * 获取到我们要操作的购物车
    * @return
    */
    private BoundHashOperations<String, Object, Object> getCartOps() {
    	UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
    	String cartKey = "";
    	if (userInfoTo.getUserId() != null) {
    		//gulimall:cart:1
    		cartKey = CART_PREFIX + userInfoTo.getUserId();
    	} else {
    		cartKey = CART_PREFIX + userInfoTo.getUserKey();
    	}
    	BoundHashOperations<String, Object, Object> operations =
    	redisTemplate.boundHashOps(cartKey);
    	return operations;
    }
    

    B、登录购物车

    @Override
    public Cart getCart() throws ExecutionException, InterruptedException {
    	Cart cart = new Cart();
    	UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
    	if(userInfoTo.getUserId()!=null) {
    		//1、登录
    		String cartKey =CART_PREFIX+ userInfoTo.getUserId();
    		//2、如果临时购物车的数据还没有进行合并【合并购物车】
    		String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
    		List<CartItem> tempCartItems = getCartItems(tempCartKey);
    		if(tempCartItems!=null){
    			//临时购物车有数据,需要合并
    			for (CartItem item : tempCartItems) {
    				addToCart(item.getSkuId(),item.getCount());
    			}
    			//清除临时购物车的数据
    			clearCart(tempCartKey);
    		}
    		//3、获取登录后的购物车的数据【包含合并过来的临时购物车的数据,和登录后的购物车的数据】
    		List<CartItem> cartItems = getCartItems(cartKey);
    		cart.setItems(cartItems);
    	} else {
    		//2、没登录
    		String cartKey =CART_PREFIX+ userInfoTo.getUserKey();
    		//获取临时购物车的所有购物项
    		List<CartItem> cartItems = getCartItems(cartKey);
    		cart.setItems(cartItems);
    	}
    	return cart;
    }
    

    3. 添加购物车

    @Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    
        String res = (String) cartOps.get(skuId.toString());
        if(StringUtils.isEmpty(res)){
            //购物车无此商品
            //2、添加新商品到购物车
            //1、远程查询当前要添加的商品的信息
            CartItem cartItem = new CartItem();
            CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
                R skuInfo = productFeignService.getSkuInfo(skuId);
                SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                });
    
                cartItem.setCheck(true);
                cartItem.setCount(num);
                cartItem.setImage(data.getSkuDefaultImg());
                cartItem.setTitle(data.getSkuTitle());
                cartItem.setSkuId(skuId);
                cartItem.setPrice(data.getPrice());
            },executor);
    
    
            //2、远程查询sku的组合信息
            CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
                List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
                cartItem.setSkuAttr(values);
            }, executor);
    
    
            CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValues).get();
            String s = JSON.toJSONString(cartItem);
            cartOps.put(skuId.toString(),s);
    
            return cartItem;
        }
        else{
            //购物车有此商品,修改数量
            CartItem cartItem = JSON.parseObject(res, CartItem.class);
            cartItem.setCount(cartItem.getCount()+num);
    
            cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
            return cartItem;
    
        }
    
    }
    

    这里用到了 OpenFeign 远程调用,调用了远程 product 服务的方法来获取商品的相关信息。。。

  • 相关阅读:
    uniapp 解决计算时精度丢失问题
    关于vue的Layout跳转
    IDEA 2022最新版 基于 JVM极致优化 IDEA 启动速度
    【数据结构初阶】三、 线性表里的链表(无头+单向+非循环链表)
    Python自动化测试详解
    彻底弄懂C/C++指针数组与数组指针
    傻瓜式制作产品图册,一秒就能学会
    【Ribbon】SpringCloud的Ribbon负载均衡使用
    毕业季--写给大学毕业生的一番话
    AI智能伪原创工具:原创文章自动生成的革新
  • 原文地址:https://blog.csdn.net/m0_51111980/article/details/126943127