谷粒商城笔记+踩坑(18)——购物车

目录

一、环境搭建

1.1、购物车模块初始化

1.2、动静资源处理

1.3、页面跳转配置

二、数据模型分析

2.1、购物车需求

2.1.1、离线购物车和在线购物车需求、数据库选择redis

2.1.2、购物车数据结构

2.2、模型类抽取,Cart和CartItem

2.3、Redis依赖和配置、SpringSession配置类

三、ThreadLocal 用户身份鉴别

3.1、需求分析 

3.2、传输对象封装临时用户id,userKey,是否有临时用户

3.3、创建购物车常量类

3.4、自定义拦截器,临时用户信息放到ThreadLocal<>

3.5、把拦截器添加到WebMvcConfigurer配置类

3.6、Controller处理购物车请求

四、添加商品到购物车

4.1、前端页面修改

4.2、Controller

4.3、service 

4.3.1、远程查询sku的组合信息

4.3.2、远程查询sku的组合信息

4.3.3、异步编排,自定义线程池配置类

4.3.4、添加购物车业务实现

4.4、刷新页面不断发请求问题,RedirectAttribute

4.4.0、分析

4.4.1、改成重定向到添加成功页面并查询购物车数据

4.4.2、Service层 CartServiceImpl 实现类编写 获取购物车某个购物项方法

4.4.3、success页面修改

五、获取购物车

六、选中购物项[是否选中]

七、修改购物项数量

7.1、前端 cartList.html 页面修改

7.2、后端 接口编写

八、删除购物项

8.1、前端修改

8.2、后端接口


一、环境搭建

1.1、购物车模块初始化

第一步、创建gulimall-cart服务,并进行降版本处理


    org.springframework.boot
    spring-boot-starter-parent
    2.1.8.RELEASE
     

com.atguigu
gulimall-cart
0.0.1-SNAPSHOT
gulimall-cart
购物车

    1.8
    Greenwich.SR3

谷粒商城笔记+踩坑(18)——购物车_第1张图片
谷粒商城笔记+踩坑(18)——购物车_第2张图片

第二步、hosts添加域名映射

# Gulimall Host Start
127.0.0.1 gulimall.cn
127.0.0.1 search.gulimall.cn
127.0.0.1 item.gulimall.cn
127.0.0.1 auth.gulimall.cn
127.0.0.1 cart.gulimall.cn
# Gulimall Host End

第三步、导入公共模块依赖


    com.atguigu.gulimall
    gulimall-common
    0.0.1-SNAPSHOT

因为目前不用数据库,故排除掉

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallCartApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallCartApplication.class, args);
    }

}

第四步、bootstrap.yml添加nacos配置

server.port=40000

spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

第五步、为启动类添加注解,开启服务注册和发现

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallCartApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallCartApplication.class, args);
    }

}

第六步、修改网关,给购物车配置路由

- id: gulimall_cart_route
  uri: lb://gulimall-cart
  predicates:
  	- Host=cart.gulimall.cn

1.2、动静资源处理


  1. 静:将资料中购物车文件夹下的所有的静态资源复制到服务器的:mydata/nginx/html/static/cart 目录下

  2. 动:将资料中购物车文件夹下的 两个页面复制到 gulimall-cart服务的 templates 目录下

  3. 替换掉网页中的所有资源申请路径

1.3、页面跳转配置


需求:实现页面的跳转

  1. 当我们在商品详情页item.html点击加入购物车之后,跳转到加入成功页success.html
  2. 在成功页success.html 点击 购物车 进入购物车列表页 cartList.html
  3. 在成功页success.html 点击 查看商品详情 跳转到该商品的详情页
  4. 在 首页 index.html 中点击我的购物车也跳转到 购物车列表页 cartList.html

gulimall-product 服务中的 Item.html



//......


Gulimall-cart 服务中 success.html 页面


Gulimall-cart 服务中 success.html 页面



//.....

  • 首页
  • Gulimall-cart 服务中的 CartController类中添加映射

    @Controller
    public class CartController {
    
        @GetMapping("/cart.html")
        public String cartListPage(){
    
            return "cartList";
        }
    
        /**
         * 添加商品到购物车
         * @return
         */
        @GetMapping("/addToCart")
        public String addToCart() {
            return "success";
        }
    }
    


     

    二、数据模型分析

    2.1、购物车需求


    2.1.1、离线购物车和在线购物车需求、数据库选择redis

    需求描述

    离线购物车:

    • 用户可在 未登录状态 下将商品添加到购物车 [ 用户离线临时购物车 ]。浏览器即使关闭,下次进入,临时购物车数据都在

    在线购物车: 

    • 用户可以在 登录状态 下将商品添加到购物车 [ 用户在线购物车 ]。登录之后,会将离线购物车的数据全部合并过来,并清空离线购物车


    • 购物功能
      • 用户可以使用购物车一起结算下单
      • 添加商品到购物车
      • 用户可以查询自己的购物车
      • 用户可以在购物车中修改购买商品的数量
      • 用户可以在购物车中删除商品
      • 在购物车中展示商品优惠信息
      • 提示购物车商品价格变化

    数据存储:

    购物车是一个读多写多的场景,因此放入数据库并不合适,但购物车又需要持久化,因此这里我们选用Redis的持久化机制存储购物车数据。

    redis默认是内存数据库,所有数据存在内存,

    2.1.2、购物车数据结构


    谷粒商城笔记+踩坑(18)——购物车_第3张图片

    购物车Redis的Hash进行存储,key是用户标识码例如gulimall:cart:1,值是一个个CartItem

    Redis中 每个用户的购物车 都是由各个购物项组成,根据分析这里使用 Hash进行存储比较合适:

    • MapMap>
      • K1:用户标识
      • Map
        • K2 :商品Id
        • CartltemInfo :购物项详情

    谷粒商城笔记+踩坑(18)——购物车_第4张图片

    2.2、模型类抽取,Cart和CartItem


    谷粒商城笔记+踩坑(18)——购物车_第5张图片

    • Cart
      需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算
      • 计算商品的总数量
      • 计算商品类型数量
      • 计算商品的总价

    注意:

    • 这里不用@Data,自己生成getter和setter方法,主要为了数量、金额等属性自定义计算方法。例如Cart里的商品数量通过CartItem列表计算总数量。
    • 金额相关数据必须用BigDecimal类型,进行精确的运算
    package com.atguigu.cart.vo;
    /**
     * Description: 整体购物车
     *  这里不用@Data,自己生成getter和setter方法,主要为了数量、金额等属性自定义计算方法。
     *  例如Cart里的商品数量通过CartItem列表计算总数量。
     */
    public class Cart {
    
        /**
         * 购物车子项信息
         */
        List items;
        /**
         * 商品的总数量
         */
        private Integer countNum;
        /**
         * 商品类型数量
         */
        private Integer countType;
        /**
         * 商品总价
         */
        private BigDecimal totalAmount;
        /**
         * 减免价格
         */
        private BigDecimal reduce = new BigDecimal("0");
    
    //需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算
        public List getItems() {
            return items;
        }
    
        public void setItems(List items) {
            this.items = items;
        }
    
        public Integer getCountNum() {
            int count = 0;
            if (items!=null && items.size()>0) {
                for (CartItem item : items) {
                    countNum += item.getCount();
                }
            }
            return count;
        }
    
    
        public Integer getCountType() {
            int count = 0;
            if (items!=null && items.size()>0) {
                for (CartItem item : items) {
                    countNum += 1;
                }
            }
            return count;
        }
    
    
        public BigDecimal getTotalAmount() {
            BigDecimal amount = new BigDecimal("0");
            // 1、计算购物项总价
            if (items!=null && items.size()>0) {
                for (CartItem item : items) {
                    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
      • 计算小计价格
    package com.atguigu.cart.vo;
    /**
     * Description: 购物项内容。
     *  这里不用@Data,自己生成getter和setter方法,主要为了数量、金额等属性自定义计算方法。
     *  例如Cart里的商品数量通过CartItem列表计算总数量。
     */
    public class CartItem {
        /**
         * 商品Id
         */
        private Long skuId;
        /**
         * 商品是否被选中(默认被选中)
         */
        private Boolean check = true;
        /**
         * 商品标题
         */
        private String title;
        /**
         * 商品图片
         */
        private String image;
        /**
         * 商品套餐信息
         */
        private List 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 getSkuAttr() {
            return skuAttr;
        }
    
        public void setSkuAttr(List 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;
        }
    }
    

    2.3、Redis依赖和配置、SpringSession配置类


    1、导入redis和SpringSession的依赖

    
      org.springframework.session
      spring-session-data-redis
    
    
      org.springframework.boot
      spring-boot-starter-data-redis
    
    

    2、编写配置

    # 配置redis
    spring.redis.host=124.222.223.222
    spring.redis.port=6379
    

    3、添加SpringSession配置类(自定义Session配置类)

    作用:配置类设置session使用json序列化,并放大作用域(自定义)。

    将 gulimall-auth-server 服务中 /com/atguigu/gulimall/auth/config路径下的GulimallSessionConfig.java配置类复制到 gulimall-cart服务的config包下:

    package com.atguigu.cart.config;
    
    @Configuration
    public class GulimallSessionConfig {
    
        @Bean
        public CookieSerializer cookieSerializer() {
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
            cookieSerializer.setDomainName("gulimall.cn");
            cookieSerializer.setCookieName("GULISESSION");
    
            return cookieSerializer;
        }
    
        @Bean
        public RedisSerializer springSessionDefaultRedisSerializer() {
            return new GenericJackson2JsonRedisSerializer();
        }
    }
     
      


     

    三、ThreadLocal 用户身份鉴别

    3.1、需求分析 

    需求:

    • 用户登录,访问Session中的用户信息
    • 用户未登录
      • Cookie中有 user-key,则表示有临时用户
      • Cookie中没有 user-key,则表示没有临时用户
        • 创建一个封装 并返回 user-key

    ThreadLocal:同一个线程共享数据

    • 核心原理是:Map threadLocal
    • 在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。线程之间互不干扰

    已知:

    • 一次请求进来: 拦截器 ==>> Controller ==>> Service ==>> dao 用的都是同一个线程

    谷粒商城笔记+踩坑(18)——购物车_第6张图片


    (1)用户身份鉴别方式

    • 当用户登录之后点击购物车,则进行用户登录
    • 用户未登录的时候点击购物车,会为临时用户生成一个name为user-key的cookie临时标识,过期时间为一个月,以后每次浏览器进行访问购物车的时候都会携带user-key。user-key 是用来标识和存储临时购物车数据的

    2)使用ThreadLocal 进行用户身份鉴别信息传递

    • 在调用购物车的接口前,先通过session信息判断是否登录,并分别进行用户身份信息的封装
      • session有用户信息则进行用户登录 userInfoTo.setUserId(member.getId());
      • session中没有用户信息
        • cookie中携带 user-key,则表示有临时用户,把user-key进行用户身份信息的封装: userInfoTo.setUserKey(cookie.getValue());
          userInfoTo.setTempUser(true); 并标识携带user-key
        • cookie中未携带 user-key,则表示没有临时用户,进行分配
    • 将信息封装好放进ThreadLocal
    • 在调用购物车的接口后,若cookie中未携带 user-key,则分配临时用户,让浏览器保存

    user-key在cookie里,标识用户身份,第一次使用购物车,都会给一个临时用户信息,浏览器保存cookie后,每次访问都会从cookie中取到user-key。

    3.2、传输对象封装临时用户id,userKey,是否有临时用户

    传输对象,起名to。 

    package com.atguigu.cart.vo;
    
    @ToString
    @Data
    public class UserInfoTo {
        private Long userId;
        private String userKey; 
    
        private boolean tempUser = false;   // 判断是否有临时用户
    }
    

    3.3、创建购物车常量类

    package com.atguigu.common.constant;
    
    public class CartConstant {
        public static final String TEMP_USER_COOKIE_NAME = "user-key";
        public static final int TEMP_USER_COOKIE_TIMEOUT = 60*60*24*30;
    }
    

    3.4、自定义拦截器,临时用户信息放到ThreadLocal<>

    业务流程: 

    1. 在执行目标方法之前,检测cookie里的userKey,如果没有则新建用户传输对象,userKey设为随机uuid
    2. 将用户传输对象封装进ThreadLocal。
    3. 在执行目标方法之后,创建cookie并,设置作用域和过期时间,让浏览器保存

    购物车模块 

    package com.xx.gulimall.cart.interceptor;
    
    /**
     * @Description: 在执行目标方法之前,判断用户的登录状态.并封装传递给controller目标请求
     **/
    
    public class CartInterceptor implements HandlerInterceptor {
    
    //创建ThreadLocal<>对象,同一个线程共享数据
        public static ThreadLocal toThreadLocal = new ThreadLocal<>();
    
        /***
         * 目标方法执行之前
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            UserInfoTo userInfoTo = new UserInfoTo();
    
            HttpSession session = request.getSession();
            //1.从session获得当前登录用户的信息
            MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(LOGIN_USER);
    
            if (memberResponseVo != null) {
                //2.1 如果用户登录了,给用户传输对象添加id
                userInfoTo.setUserId(memberResponseVo.getId());
            }
    //        3.获取cookie
            Cookie[] cookies = request.getCookies();
    //        如果cookie不为空,找到和"user-key"同名的cookie,设置userKey,标记临时用户
            if (cookies != null && cookies.length > 0) {
                for (Cookie cookie : cookies) {
                    //user-key
                    String name = cookie.getName();
                    if (name.equals(TEMP_USER_COOKIE_NAME)) {
                        userInfoTo.setUserKey(cookie.getValue());
                        //标记为已是临时用户
                        userInfoTo.setTempUser(true);
                    }
                }
            }
    
            //如果没有临时用户一定分配一个临时用户,userKey是临时id。
            if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
                String uuid = UUID.randomUUID().toString();
                userInfoTo.setUserKey(uuid);
            }
    
            //目标方法执行之前,将用户传输信息放到ThreadLocal里,同一个线程共享数据。
            toThreadLocal.set(userInfoTo);
            return true;
        }
    
    
        /**
         * 业务执行之后,分配临时用户来浏览器保存
         */
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
            //获取当前用户的值
            UserInfoTo userInfoTo = toThreadLocal.get();
    
            //如果没有临时用户则保存一个临时用户,并延长cookie过期时间,扩大cookie域,实现子域名共享cookie。
            if (!userInfoTo.getTempUser()) {
                //创建一个cookie
                Cookie cookie = new Cookie(TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
                //扩大作用域
                cookie.setDomain("gulimall.com");
                //设置过期时间
                cookie.setMaxAge(TEMP_USER_COOKIE_TIMEOUT);
                response.addCookie(cookie);
            }
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
        }
    }
    

    3.5、把拦截器添加到WebMvcConfigurer配置类

    添加拦截器的配置,不能只把拦截器加入容器中,不然拦截器不生效的

    package com.atguigu.cart.config;
    
    @Configuration
    public class GulimallWebConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
        }
    }
    

    3.6、Controller处理购物车请求

    package com.atguigu.cart.controller;
    
    @Controller
    public class CartController {
    
        /**
         * 去往用户购物车页面
         *  浏览器有一个cookie:user-key 用来标识用户身份,一个月后过期
         *  如果第一次使用京东的购物车功能,都会给一个临时用户身份;浏览器以后保存,每次访问都会带上这个cookie;
         * 登录:Session有
         * 没登录:按照cookie里面的user-key来做。
         *  第一次:如果没有临时用户,帮忙创建一个临时用户。
         * @return
         */
        @GetMapping(value = "/cart.html")
        public String cartListPage(Model model) throws ExecutionException, InterruptedException {
            //快速得到用户信息:id,user-key
            // UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
    
            CartVo cartVo = cartService.getCart();
            model.addAttribute("cart",cartVo);
            return "cartList";
        }
    }
    


     

    四、添加商品到购物车

    在gulimall-product模块,修改添加购物车按钮

    4.1、前端页面修改

    第一步、修改item页面

    点击 加入购物车 按钮时,发送请求:

    http://cart.gulimall.cn/addToCart?skuId=?&num=?

    • skuId:当前商品的skuId
    • num: 当前商品加入购物车的数量

    谷粒商城笔记+踩坑(18)——购物车_第7张图片
    谷粒商城笔记+踩坑(18)——购物车_第8张图片

    $("#addToCartA").click(function () {
       var num = $("#numInput").val();
       var skuId = $(this).attr("skuId");
       location.href = "http://cart.gulimall.cn/addToCart?skuId="+skuId+"&num="+num;
    });
    

    第二步、修改 success页面

    谷粒商城笔记+踩坑(18)——购物车_第9张图片

    业务逻辑:

    1. 保存在Redis中的key
      • 如果用户已经登录,则存储在Redis中的key,则是用户的Id
      • 如果用户没有登录,则存在在Redis中的key,是临时用户对应的 user-key
    2. 购物车保存
      • 若当前商品已经存在购物车,只需增添数量
      • 否则需要查询商品购物项所需信息,并添加新商品至购物车

    4.2、Controller

    1、Controller层接口 CartController类 编写添加商品到购物车方法

    /**
     * 添加商品到购物车
     * @param skuId 商品的skuid
     * @param num   添加的商品数量
     * @return
     */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            Model model) throws ExecutionException, InterruptedException {
    
        CartItem cartItem = cartService.addToCart(skuId,num);
        model.addAttribute("item",cartItem);
        return "success";
    }
    

    4.3、service 

    4.3.1、远程查询sku的组合信息


    在gulimall-cart 服务中编写远程调用feign接口

    package com.atguigu.cart.feign;
    
    @FeignClient("gulimall-product")
    public interface ProductFeignService {
        @GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
        List getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
    }
    

    4.3.2、远程查询sku的组合信息

    Gulimall-product 服务中

    1. Controller层编写查询sku的组合信息
    @RestController
    @RequestMapping("product/skusaleattrvalue")
    public class SkuSaleAttrValueController {
        @Autowired
        private SkuSaleAttrValueService skuSaleAttrValueService;
    
        @GetMapping("/stringlist/{skuId}")
        public List getSkuSaleAttrValues(@PathVariable("skuId") Long skuId){
            return  skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);
        }
      
      //....
    }
    

    Service层实现类 SkuSaleAttrValueServiceImpl 中编写方法

    @Override
    public List getSkuSaleAttrValuesAsStringList(Long skuId) {
        SkuSaleAttrValueDao dao = this.baseMapper;
        return dao.getSkuSaleAttrValuesAsStringList(skuId);
    }
    

    Dao层xml的SQL语句 SkuSaleAttrValueDao.xml

    
    

    在gulimall-cart 服务中编写远程调用feign接口

    package com.atguigu.cart.feign;
    
    @FeignClient("gulimall-product")
    public interface ProductFeignService {
    
        @RequestMapping("/product/skuinfo/info/{skuId}")
        R getSkuInfo(@PathVariable("skuId") Long skuId);
    
        @GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
        List getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
    }
    

    4.3.3、异步编排,自定义线程池配置类

    假设 远程查询sku的组合信息 查询需要1秒,远程查询sku的组合信息有需要1.5秒,那总耗时就需要2.5秒。
    若使用异步编排的话,只需要1.5秒。

    1、 将gulimall-product中 com/atguigu/gulimall/product/config 路径下的 MyThreadConfig、ThreadPoolConfigProperties类复制到 gulimall-cart 服务下的 config 路径下:

    package com.atguigu.cart.config;
    
    @Configuration
    public class MyThreadConfig {
    
        @Bean
        public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
            return new ThreadPoolExecutor(pool.getCoreSize(),
                    pool.getMaxSize(),
                    pool.getKeepAliveTime(),
                    TimeUnit.SECONDS,
                    new LinkedBlockingDeque<>(100000),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.AbortPolicy());
        }
    }
    
    package com.atguigu.cart.config;
    
    @ConfigurationProperties(prefix = "gulimall.thread")    /自动注入
    @Component
    @Data
    public class ThreadPoolConfigProperties {
        private Integer coreSize;
        private Integer maxSize;
        private Integer keepAliveTime;
    }
    

    2、配置 线程池

    # 配置线程池
    gulimall.thread.core-size: 20
    gulimall.thread.max-size: 200
    gulimall.thread.keep-alive-time: 10
    

    4.3.4、添加购物车业务实现

    CartServiceImpl  

    @Slf4j
    @Service
    public class CartServiceImpl implements CartService {
    
        @Autowired
        StringRedisTemplate redisTemplate;
    
        @Autowired
        ProductFeignService productFeignService;
    
        @Autowired
        ThreadPoolExecutor executor;
    
        // 用户标识前缀
        private final String CART_PREFIX = "gulimall:cart:";
    
    
    @Slf4j
    @Service("cartService")
    public class CartServiceImpl implements CartService {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Autowired
        private ProductFeignService productFeignService;
    
        @Autowired
        private ThreadPoolExecutor executor;
    
        @Override
        public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
    
            //1.拿到要操作的购物车redis操作器信息
            BoundHashOperations cartOps = getCartOps();
    
            //2.判断Redis是否有该商品的信息
            String productRedisValue = (String) cartOps.get(skuId.toString());
    
            if (StringUtils.isEmpty(productRedisValue)) {
    //2.1如果没有就添加数据
                //添加新的商品到购物车(redis)
                CartItemVo cartItemVo = new CartItemVo();
                //开启第一个异步任务,存商品基本信息
                CompletableFuture getSkuInfoFuture = CompletableFuture.runAsync(() -> {
                    //远程调用商品模块查询当前要添加商品的sku信息
                    R productSkuInfo = productFeignService.getInfo(skuId);
                    SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference() {});
                    //sku信息数据赋值给单个CartItem
                    cartItemVo.setSkuId(skuInfo.getSkuId());
                    cartItemVo.setTitle(skuInfo.getSkuTitle());
                    cartItemVo.setImage(skuInfo.getSkuDefaultImg());
                    cartItemVo.setPrice(skuInfo.getPrice());
                    cartItemVo.setCount(num);
                }, executor);
    
                //开启第二个异步任务,存商品属性信息
                CompletableFuture getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
                    //2、远程查询skuAttrValues组合信息
                    List skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
                    cartItemVo.setSkuAttrValues(skuSaleAttrValues);
                }, executor);
    
                //等待所有的异步任务全部完成
                CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();
    
                String cartItemJson = JSON.toJSONString(cartItemVo);
                cartOps.put(skuId.toString(), cartItemJson);
    
                return cartItemVo;
            } else {
    //2.2 购物车有此商品,修改数量即可
                CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class);
                cartItemVo.setCount(cartItemVo.getCount() + num);
                //修改redis的数据
                String cartItemJson = JSON.toJSONString(cartItemVo);
                cartOps.put(skuId.toString(),cartItemJson);
    
                return cartItemVo;
            }
        }
    
        /**
         * 获取到要操作的购物车
         * @return
         */
        private  BoundHashOperations getCartOps() {
            UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
            // 1、判断用户有没有登录
            String cartKey = "";   
            if (userInfoTo.getUserId() != null){
                // 用户已登录,则存储在Redis中的key 是 用户的Id
                cartKey = CART_PREFIX+userInfoTo.getUserId();
            } else {
                // 用户没有登录,则存在在Redis中的key 是 临时用户对应的 `user-key`
                cartKey = CART_PREFIX+userInfoTo.getUserKey();
            }
            // 绑定hash
            BoundHashOperations operations = redisTemplate.boundHashOps(cartKey);
            return operations;
        }
    }
    


     

    4.4、刷新页面不断发请求问题,RedirectAttribute

    4.4.0、分析

    目前问题:

    不断刷新“添加成功” 页面,会不断发请求,数量会不断增长:

    解决办法,这里修改逻辑:

    • 在controller的addToCart方法里添加商品
    • 商品添加完跳转到成功页面我们改为改成重定向另一个方法,专门查询数据跳转到成功页面

    4.4.1、改成重定向到添加成功页面并查询购物车数据

    /**
     * 添加商品到购物车
     * @param skuId 商品的skuid
     * @param num   添加的商品数量
     * @return
     * RedirectAttributes
     *  ra.addFlashAttribute(, ) :将数据放在session里面可以在页面里取出,但是只能取一次
     *  ra.addAttribute(,); 将数据放在url后面
     */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            RedirectAttributes ra) throws ExecutionException, InterruptedException {
        cartService.addToCart(skuId,num);
        ra.addAttribute("skuId", skuId);
        return "redirect:http://cart.gulimall.cn/addToCartSuccess.html";
    }
    
    /**
     * 跳转到成功页
     * @param skuId
     * @param model
     * @return
     */
    @GetMapping("/addToCartSuccess.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model) {
        // 重定向到成功页面,再次查询购物车数据
        CartItem cartItem = cartService.getCartItem(skuId);
        model.addAttribute("item",cartItem);
        return "success";
    }
    

    4.4.2、Service层 CartServiceImpl 实现类编写 获取购物车某个购物项方法

    @Override
    public CartItem getCartItem(Long skuId) {
        BoundHashOperations cartOps = getCartOps();
        String str = (String) cartOps.get(skuId.toString());
        CartItem cartItem = JSON.parseObject(str, CartItem.class);
        return cartItem;
    }
    

    4.4.3、success页面修改

    
    


     

    五、获取购物车

    • 若用户未登录,则使用user-key获取Redis中购物车数据

    • 若用户登录,则使用userId获取Redis中购物车数据,并将

      • user-key 对应的临时购物车数据
      • 用户购物车数据

      合并 并删除临时购物车。

    第一步、Controller层 CartController 类编写方法

    @Controller
    public class CartController {
    
        @Autowired
        CartService cartService;
    
        @GetMapping("/cart.html")
        public String cartListPage(Model model) throws ExecutionException, InterruptedException {
            Cart cart = cartService.getCart();
            model.addAttribute("cart",cart);
            return "cartList";
        }
    

    第二步、编写Service层 方法

    package com.atguigu.cart.service;
    
    public interface CartService {
    		//....
    
        /**
         * 获取购物车某个购物项
         * @param skuId
         * @return
         */
        CartItem getCartItem(Long skuId);
    
        /**
         * 获取整个购物车
         * @return
         */
        Cart getCart() throws ExecutionException, InterruptedException;
    
        /**
         * 清空购物车数据
         * @param cartKey
         */
        void clearCart(String cartKey);
    }
    

    实现类 CartServiceImpl 方法:

    @Override
    public CartItem getCartItem(Long skuId) {
        BoundHashOperations cartOps = getCartOps();
        String str = (String) cartOps.get(skuId.toString());
        CartItem cartItem = JSON.parseObject(str, CartItem.class);
        return cartItem;
    }
        /**
         * 获取购物车里面的数据
         * @param cartKey
         * @return
         */
        private List getCartItems(String cartKey) {
            //获取购物车里面的所有商品
            BoundHashOperations operations = redisTemplate.boundHashOps(cartKey);
            List values = operations.values();
            if (values != null && values.size() > 0) {
                List cartItemVoStream = values.stream().map((obj) -> {
                    String str = (String) obj;
                    CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
                    return cartItem;
                }).collect(Collectors.toList());
                return cartItemVoStream;
            }
            return null;
    
        }
    @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 tempCartItems = getCartItems(tempCartKey);
            if (tempCartItems!=null) {
                // 临时购物车有数据,需要合并
                for (CartItem item : tempCartItems) {
                    addToCart(item.getSkuId(),item.getCount());
                }
                // 清除临时购物车的数据
                clearCart(tempCartKey);
            }
            // 3、删除临时购物车
            // 4、获取登录后的购物车数据
            List cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
    
        } else {
            // 2、没登录状态
            String cartKey = CART_PREFIX + userInfoTo.getUserKey();
            // 获取临时购物车的所有项
            List cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
        }
        return cart;
    }
    
    @Override
    public void clearCart(String cartKey) {
        // 直接删除该键
        redisTemplate.delete(cartKey);
    }
     
      

    第三步、修改购物车前端页面 cartList.html

    谷粒商城笔记+踩坑(18)——购物车_第10张图片

    测试结果:


     

    六、选中购物项[是否选中]

    **第一步、**Controller层方法编写

    gulimall-cart 服务com/atguigu/cart/controller/ 路径下 CartController.java类中添加映射方法

    @GetMapping("/checkItem")
    public String checkItem(@RequestParam("skuId") Long skuId,
                            @RequestParam("check") Integer check) {
        cartService.checkItem(skuId,check);
        return "redirect:http://cart.gulimall.cn/cart.html";
    }
    

    **第二步、**Service层实现类方法中编写是否选中购物项方法

    /**
     * 勾选购物项
     * @param skuId
     * @param check
     */
    void checkItem(Long skuId, Integer check);
    

    gulimall-cart 服务中 com/atguigu/cart/service/impl/ 路径下 CartServiceImpl.java 实现类:

    @Override
    public void checkItem(Long skuId, Integer check) {
        BoundHashOperations cartOps = getCartOps();
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCheck(check==1?true:false);
        String s = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),s);
    }
    

    第三步、页面修改

    谷粒商城笔记+踩坑(18)——购物车_第11张图片

    $(".itemCheck").click(function () {
        var skuId = $(this).attr("skuId");
        var check = $(this).prop("checked");
        location.href = "http://cart.gulimall.cn/checkItem?skuId="+skuId+"&check="+(check?1:0);
    });
    


     

    七、修改购物项数量

    7.1、前端 cartList.html 页面修改

    前端 cartList.html 页面修改

  • - 5 +

  • $(".countOpsBtn").click(function () {
        var skuId = $(this).parent().attr("skuId");
        var num = $(this).parent().find(".countOpsNum").text();
        location.href = "http://cart.gulimall.cn/countItem?skuId="+skuId+"&num="+num; 
    });
    

    7.2、后端 接口编写

    后端 接口编写

    1. Controller 层 接口编写

    修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:

    @GetMapping("/countItem")
    public String countItem(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num) {
        cartService.countItem(skuId,num);
        return "redirect:http://cart.gulimall.cn/cart.html";
    }
    

    Service 层编写

    /**
     * 修改购物项数量
     * @param skuId
     * @param num
     */
    void countItem(Long skuId, Integer num);
    

    修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:

    @Override
    public void countItem(Long skuId, Integer num) {
        BoundHashOperations cartOps = getCartOps();
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCount(num);
        cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
    }
    


     

    八、删除购物项

    8.1、前端修改

    在这里插入图片描述
    谷粒商城笔记+踩坑(18)——购物车_第12张图片

    8.2、后端接口

    CartController

    @GetMapping("/deleteItem")
    public String deleteItem(@RequestParam("skuId") Long skuId) {
        cartService.deleteItem(skuId);
        return "redirect:http://cart.gulimall.cn/cart.html";
    }
    

    CartServiceImpl.java

    /**
     * 删除购物项
     * @param skuId
     */
    @Override
    public void deleteItem(Long skuId) {
        BoundHashOperations cartOps = getCartOps();
        cartOps.delete(skuId.toString());
    }
    

    你可能感兴趣的:(谷粒商城项目,java学习路线,spring,maven,java,spring,boot,spring,cloud)