谷粒商场学习笔记(14.购物车)

一、环境搭建

1、项目初始化

创建gulimall-cart服务
在这里插入图片描述
添加暂时需要的依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-loadbalancer</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.0.5.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                    </exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>
配置nacos redis
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-cart
  redis:
    host: 127.0.0.1
    port: 6379

server:
  port: 40000

配置启动项
在这里插入图片描述

2、网关配置在这里插入图片描述

并再hosts文件中配置
cart.yjlmall.com 映射到 本地地址

3、静态资源处理

将静态资源复制到nginx下
在这里插入图片描述
并把其中的连接地址修改
在这里插入图片描述

二、需求分析

1、购物车需求

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

离线购物车:

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

在线购物车:

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

购物功能:

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

数据存储:

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

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

2、购物车数据结构

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

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

Map<String k1,Map<String k2,CartltemInfo>>
K1:用户标识
Map<String k2,CartltemInfo>
K2 :商品Id
CartltemInfo :购物项详情

在这里插入图片描述

3、购物车实体类设计

在这里插入图片描述
在这里插入图片描述

Cart

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

注意:

这里不用@Data,自己生成getter和setter方法,主要为了数量、金额等属性自定义计算方法。例如Cart里的商品数量通过CartItem列表计算总数量。
金额相关数据必须用BigDecimal类型,进行精确的运算

package com.atguigu.gulimall.cart.vo;

import java.math.BigDecimal;
import java.util.List;
/**
 * Description: 整体购物车
 *  这里不用@Data,自己生成getter和setter方法,主要为了数量、金额等属性自定义计算方法。
 *  例如Cart里的商品数量通过CartItem列表计算总数量。
 */
public class Cart {

    /**
     * 购物车子项信息
     */
    List<CartItem> items;
    /**
     * 商品的总数量
     */
    private Integer countNum;
    /**
     * 商品类型数量
     */
    private Integer countType;
    /**
     * 商品总价
     */
    private BigDecimal totalAmount;
    /**
     * 减免价格
     */
    private BigDecimal reduce = new BigDecimal("0");

    //需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算
    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) {
                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.gulimall.cart.vo;

import java.math.BigDecimal;
import java.util.List;

/**
 * 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<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;
    }

}

三、购物车功能实现

1、相关配置类

在这里插入图片描述

1.SpringSession配置

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

package com.atguigu.gulimall.cart.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;


@Configuration
public class GulimallSessionConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.cn");
        cookieSerializer.setCookieName("GULISESSION");

        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

2.异步编排,自定义线程池配置类

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

在这里插入图片描述

package com.atguigu.gulimall.cart.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@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.gulimall.cart.config;


import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}

配置线程池

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

2、ThreadLocal 用户身份鉴别

1.需求介绍

需求:

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

ThreadLocal:同一个线程共享数据

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

已知:

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

在这里插入图片描述
(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。

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

传输对象,起名to。
在这里插入图片描述

package com.atguigu.gulimall.cart.vo;


import lombok.Data;
import lombok.ToString;

@Data

@ToString
public class UserInfoTo {
    private Long userId;
    private String userKey;

    private boolean tempUser = false;   // 判断是否有临时用户

}

3.创建购物车常量类

com.atguigu.common.constant.CartConstant

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;
}

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

业务流程:

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

购物车模块
在这里插入图片描述

package com.atguigu.gulimall.cart.interceptor;

import com.atguigu.common.vo.MemberResponseVo;
import com.atguigu.gulimall.cart.vo.UserInfoTo;
import org.apache.commons.lang.StringUtils;
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;

import static com.atguigu.common.constant.AuthServerConstant.LOGIN_USER;
import static com.atguigu.common.constant.CartConstant.TEMP_USER_COOKIE_NAME;
import static com.atguigu.common.constant.CartConstant.TEMP_USER_COOKIE_TIMEOUT;

public class CartInterceptor implements HandlerInterceptor {
    //创建ThreadLocal<>对象,同一个线程共享数据
    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();
        //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里,同一个线程共享数据。
        threadLocal.set(userInfoTo);
        return true;
    }


    /**
     * 业务执行之后,分配临时用户来浏览器保存
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        //获取当前用户的值
        UserInfoTo userInfoTo = threadLocal.get();

        //如果没有临时用户则保存一个临时用户,并延长cookie过期时间,扩大cookie域,实现子域名共享cookie。
        if (!userInfoTo.isTempUser()) {
            //创建一个cookie
            Cookie cookie = new Cookie(TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
            //扩大作用域
            cookie.setDomain("yjlmall.com");
            //设置过期时间
            cookie.setMaxAge(TEMP_USER_COOKIE_TIMEOUT);
            response.addCookie(cookie);
        }

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

}

4.把拦截器添加到WebMvcConfigurer配置类

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

package com.atguigu.gulimall.cart.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@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());
    }
}

3、详细功能实现

1.跳转购物车页面

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

2.添加商品到购物车

controller类:

/**
     * 添加商品到购物车
     * @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.yjlmall.com/addToCartSuccess.html";
    }

前端Item商品详情页面修改

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

http://cart.yjlmall.com/addToCart?skuId=?&num=?

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

在这里插入图片描述
在这里插入图片描述
前端success页面修改
在这里插入图片描述
业务逻辑:

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

service类:
在这里插入图片描述

Impl实现类

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private ThreadPoolExecutor executor;


    // 用户标识前缀
    private final String CART_PREFIX = "gulimall:cart:";

    @Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {

        //1.拿到要操作的购物车redis操作器信息
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        //2.判断Redis是否有该商品的信息
        String productRedisValue = (String) cartOps.get(skuId.toString());

        if (StringUtils.isEmpty(productRedisValue)) {
            //2.1如果没有就添加数据
            //添加新的商品到购物车(redis)
            CartItem cartItemVo = new CartItem();
            //开启第一个异步任务,存商品基本信息
            CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
                //远程调用商品模块查询当前要添加商品的sku信息
                R productSkuInfo = productFeignService.getSkuInfo(skuId);
                SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
                //sku信息数据赋值给单个CartItem
                cartItemVo.setSkuId(skuInfo.getSkuId());
                cartItemVo.setTitle(skuInfo.getSkuTitle());
                cartItemVo.setImage(skuInfo.getSkuDefaultImg());
                cartItemVo.setPrice(skuInfo.getPrice());
                cartItemVo.setCount(num);
            }, executor);

            //开启第二个异步任务,存商品属性信息
            CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
                //2、远程查询skuAttrValues组合信息
                List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
                cartItemVo.setSkuAttr(skuSaleAttrValues);
            }, executor);

            //等待所有的异步任务全部完成
            CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();

            String cartItemJson = JSON.toJSONString(cartItemVo);
            cartOps.put(skuId.toString(), cartItemJson);

            return cartItemVo;
        } else {
            //2.2 购物车有此商品,修改数量即可
            CartItem cartItemVo = JSON.parseObject(productRedisValue, CartItem.class);
            cartItemVo.setCount(cartItemVo.getCount() + num);
            //修改redis的数据
            String cartItemJson = JSON.toJSONString(cartItemVo);
            cartOps.put(skuId.toString(),cartItemJson);

            return cartItemVo;
        }
    }

3.远程调用功能(上面实现添加需要调用product商品模块的功能)

在这里插入图片描述
getskuinfo:远程查询sku的组合信息
getSkuSaleAttrValues:获取sku属性信息

import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;


@FeignClient("gulimall-product")
public interface ProductFeignService {

    @RequestMapping("/product/skuinfo/info/{skuId}")
    R getSkuInfo(@PathVariable("skuId") Long skuId);

    @GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
    List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
}

实现类:
在这里插入图片描述
dao层:
在这里插入图片描述

4.跳转到成功页面

解决刷新页面不断发请求问题,RedirectAttribute

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

/**
     * 跳转到成功页
     * @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";
    }

Service层 CartServiceImpl 实现类编写 获取购物车某个购物项方法
在这里插入图片描述

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

前端页面修改

<div class="success-wrap">
        <div class="w" id="result">
            <div class="m succeed-box">
                <div th:if="${item!=null}" class="mc success-cont">
        <div class="success-lcol">
        <div class="success-top"><b class="succ-icon"></b>
        <h3 class="ftx-02">商品已成功加入购物车</h3></div>
    <div class="p-item">
        <div class="p-img">
        <a href="/javascript:;" target="_blank"><img style="height: 60px;width:60px;"  th:src="${item.image}"></a>
        </div>
        <div class="p-info">
        <div class="p-name">
        <a th:href="'http://item.yjlmall.com/'+${item.skuId}+'.html'"
           th:text="${item.title}"  >TCL 55A950C 55英寸32核人工智能 HDR曲面超薄4K电视金属机身(枪色)</a>
    </div>
    <div class="p-extra"> <span class="txt" th:text="'数量:'+${item.count}" >  数量:1</span></div>
    </div>
    <div class="clr"></div>
        </div>
        </div>
        <div class="success-btns success-btns-new">
        <div class="success-ad">
        <a href="/#none"></a>
        </div>
        <div class="clr"></div>
        <div class="bg_shop">
        <a class="btn-tobback" th:href="'http://item.yjlmall.com/'+${item.skuId}+'.html'">查看商品详情</a>
        <a class="btn-addtocart"  href="http://cart.yjlmall.com/cart.html" id="GotoShoppingCart"><b></b>去购物车结算</a>
   </div>
    </div>
    </div>
                <div class="mc success-cont">
                    <h2>购物车中无商品</h2> <a href="http://yjlmall.com">去购物</a>
                </div>
    </div>
    </div>
    </div>

5.获取购物车信息

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

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

user-key 对应的临时购物车数据 与
用户购物车数据
合并 并删除临时购物车。

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);
}

Impl实现类

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

修改前端catrList页面:

<div class="One_ShopCon">
					<h1 th:if="${cart.items == null}">
						购物车还没有商品, <a href="http://yjlmall.com">去购物</a>
					</h1>
					<ul th:if="${cart.items != null}">
						<li th:each="item:${cart.items}">
							<div>
								<ol>
                            <li><input type="checkbox"  th:attr="skuId=${item.skuId}" class="itemCheck" th:checked="${item.check}"></li>
                            <li>
                                <dt><img th:src="${item.image}" alt=""></dt>
                                <dd style="width: 300px;">
                                    <p>

                                        <span th:text="${item.title}">TCL 55A950C 55英寸32核</span>
                                        <span th:each="attr:${item.skuAttr}" th:text="${attr}">尺码: 55时 超薄曲面 人工智能</span>
                                    </p>

                                </dd>
                            </li>
                            <li>
                                <p class="dj" th:text="'¥'+${#numbers.formatDecimal(item.price,3,2)}">¥4599.00</p>

                            </li>
									<li>
										<p style="width:80px" th:attr="skuId=${item.skuId}">
											<span class="countOpsBtn">-</span>
											<span class="countOpsNum" th:text="${item.count}">5</span>
											<span class="countOpsBtn">+</span>
										</p>
									</li>
                            <li style="font-weight:bold"><p class="zj">¥[[${#numbers.formatDecimal(item.totalPrice,3,2)]]</p></li>
                            <li>
                                <p class="deleteItemBth" th:attr="skuId=${item.skuId}">删除</p>

                            </li>
                        </ol>
                    </div>
                </li>

					</ul>
				</div>

6.是否选中购物项

controller类:

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

service类

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

Impl实现类:

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

前端页面修改
在这里插入图片描述

$(".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.修改购物项数量

controller类

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

service类

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

Impl实现类

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

前端修改

<li>
    <p style="width:80px" th:attr="skuId=${item.skuId}">
        <span class="countOpsBtn">-</span>
        <span class="countOpsNum" th:text="${item.count}">5</span>
        <span class="countOpsBtn">+</span>
    </p>
</li>

$(“.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;
});

8.删除购物项

controller类

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

service类
在这里插入图片描述

Impl实现类

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

前端修改
在这里插入图片描述

四、整体测试

选中商品进行添加(现在是未登录的离线购物车)
在这里插入图片描述

添加成功

在这里插入图片描述

查看购物车页面:(点击去购物车结算)

在这里插入图片描述
登录账户合并购物车
登录完成自动添加商品到购物车
在这里插入图片描述
再添加一个商品到购物车 测试删除
在这里插入图片描述
点击删除按钮删除商品
在这里插入图片描述
测试商品加减
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值