谷粒商城--购物车--高级篇笔记九

谷粒商城–购物车–高级篇笔记九

0.购物车需求

  • 用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车
    • 放入数据库
    • mongodb
    • 放入 redis(采用)
    • 登录以后, 会将临时购物车的数据全部合并过来, 并清空临时购物车
  • 用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
    • 放入 localstorage(客户端存储, 后台不存)
    • cookie
    • WebSQL
    • 放入 redis(采用)
      • 浏览器即使关闭, 下次进入, 临时购物车数据都在
  • 用户可以使用购物车一起结算下单
  • 给购物车添加商品
  • 用户可以查询自己的购物车
  • 用户可以在购物车中修改购买商品的数量
  • 用户可以在购物车中删除商品。
  • 选中不选中商品
  • 在购物车中展示商品优惠信息
  • 提示购物车商品价格变化

1. 数据模型分析

1.1 数据存储方式

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

1.2 数据存储结构

一个购物车是由多个购物项组成的,但是我们用List进行存储并不合适,因为使用List查找某个购物项时需要挨个遍历每个购物项,会造成大量时间损耗,为保证查找速度,我们使用hash进行存储,

  • 首先不同用户应该有独立的购物车, 因此购物车应该以用户的作为 key 来存储, Value 是用户的所有购物车信息。 这样看来基本的k-v结构就可以了。
  • 但是, 我们对购物车中的商品进行增、 删、 改操作, 基本都需要根据商品 id 进行判断,为了方便后期处理, 我们的购物车也应该是k-v结构, key 是商品 id, value 才是这个商品的购物车信息。
  • 综上所述, 我们的购物车结构是一个双层 Map: Map<String,Map<String,String>>
    • 第一层 Map, Key 是用户 id
    • 第二层 Map, Key 是购物车中商品 id, 值是购物项数据

image-20211213100831761

1.3 流程

参照京东

image-20211213100940648user-key 是随机生成的 id, 不管有没有登录都会有这个 cookie 信息。
两个功能: 新增商品到购物车、 查询购物车。
新增商品: 判断是否登录

  • 是: 则添加商品到后台 Redis 中, 把 user 的唯一标识符作为 key。
  • 否: 则添加商品到后台 redis 中, 使用随机生成的 user-key 作为 key。
  • 查询购物车列表: 判断是否登录
    • 否: 直接根据 user-key 查询 redis 中数据并展示
    • 是: 已登录, 则需要先根据 user-key 查询 redis 是否有数据。
      • 有: 需要提交到后台添加到 redis, 合并数据, 而后查询。
      • 否: 直接去后台查询 redis, 而后返回

2. 环境搭建

2.1 新建模块

image-20211213101549786

image-20211213101654810

2.1.1 pom

并且导入gulimall-common

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>site.zhourui</groupId>
    <artifactId>gulimall-cart</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-cart</name>
    <description>购物车</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR6</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.zhourui.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <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>
    </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>
        </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>

</project>

2.2 新增host

image-20211213102150203

2.3 静态资源分离

image-20211213104417803

image-20211213104544445

2.4 配置网关

gulimall-gateway/src/main/resources/application.yml

        #购物车
        - id: gulimall_cart_route
          uri: lb://gulimall-cart
          predicates:
            - Host=cart.gulimall.com

2.5 ThreadLocal用户身份鉴别

2.5.1 ThreadLocal 同一个线程共享数据

image-20211213140458901

2.5.2 整合spring session
2.5.2.1 导入依赖

gulimall-cart/pom.xml

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
2.5.2.2 自定义springsession

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/config/GulimallSessionConfig.java

package site.zhourui.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.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

/**
 * @author zr
 * @date 2021/12/12 10:29
 */
@Configuration
public class GulimallSessionConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        //放大作用域
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        cookieSerializer.setCookieMaxAge(60*60*24*7);
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}
2.5.2.3 配置redis作为session
server:
  port: 30000
spring:
  application:
    name: gulimall-cart
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  thymeleaf:
    cache: false
  redis:
    host: 192.168.157.128
    port: 6379
  session:
    store-type: redis
  servlet:
    session:
      timeout: 30m

2.5.2.4 开启redis-spring session

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/GulimallCartApplication.java

@EnableRedisHttpSession
2.5.3 新增用户鉴别拦截器

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/Interceptor/CartInterceptor.java

package site.zhourui.gulimall.cart.Interceptor;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.constant.CartConstant;
import site.zhourui.common.to.UserInfoTo;
import site.zhourui.common.vo.MemberResponseVo;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;

/**
 * @author zr
 * @date 2021/12/13 14:47
 */
public class CartInterceptor implements HandlerInterceptor {
    public static ThreadLocal<UserInfoTo> toThreadLocal = new ThreadLocal<>();

    /***
     * 拦截所有请求给ThreadLocal封装UserInfoTo对象
     * 1、从session中获取MemberResponseVo != null,登录状态,为UserInfoTo设置Id
     * 2、从request中获取cookie,找到user-key的value,
     * 目标方法执行之前:在ThreadLocal中存入用户信息【同一个线程共享数据】
     * 从session中获取数据【使用session需要cookie中的GULISESSION 值】
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserInfoTo userInfoTo = new UserInfoTo();
        HttpSession session = request.getSession();
        //获得当前登录用户的信息
        MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (memberResponseVo != null) {
            //用户登录了
            userInfoTo.setUserId(memberResponseVo.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());
                    // 标识客户端已经存储了 user-key
                    userInfoTo.setTempUser(true);
                }
            }
        }
        //如果没有临时用户一定分配一个临时用户
        if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }
        //目标方法执行之前
        toThreadLocal.set(userInfoTo);
        return true;
    }

    /**
     * 业务执行之后,让浏览器保存临时用户user-key
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 获取当前用户的值
        UserInfoTo userInfoTo = toThreadLocal.get();
        // 1、判断是否登录;2、判断是否创建user-token的cookie
        if (userInfoTo != null && !userInfoTo.isTempUser()) {
            //创建一个cookie
            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);
        }
    }

}

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/config/GulimallWebConfig.java

package site.zhourui.gulimall.cart.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.zhourui.gulimall.cart.Interceptor.CartInterceptor;

/**
 * @author zr
 * @date 2021/12/13 14:45
 */
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor())
                .addPathPatterns("/**");
    }
}

2.6 封装所需vo/to

2.6.1 购物项CartItemVo

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/vo/CartItemVo.java

package site.zhourui.gulimall.cart.vo;

/**
 * @author zr
 * @date 2021/12/13 11:51
 */

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

/**
 * 购物项内容
 */
public class CartItemVo {
    private Long skuId;             // skuId
    private Boolean check = true;   // 是否选中
    private String title;           // 标题
    private String image;           // 图片
    private List<String> skuAttrValues;// 商品销售属性
    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> getSkuAttrValues() {
        return skuAttrValues;
    }

    public void setSkuAttrValues(List<String> skuAttrValues) {
        this.skuAttrValues = skuAttrValues;
    }

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

    /**
     * 计算当前购物项总价
     */
    public BigDecimal getTotalPrice() {
        return this.price.multiply(new BigDecimal("" + this.count));
    }

    public void setTotalPrice(BigDecimal totalPrice) {
        this.totalPrice = totalPrice;
    }
}

2.6.2 购物车CartVo

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/vo/CartVo.java

package site.zhourui.gulimall.cart.vo;

/**
 * @author zr
 * @date 2021/12/13 11:52
 */

import org.springframework.util.CollectionUtils;

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

/**
 * 购物车Vo
 * 需要计算的属性需要重写get方法,保证每次获取属性都会进行计算
 */
public class CartVo {

    private List<CartItemVo> items; // 购物项集合
    private Integer countNum;       // 商品件数【例如购物项1 2件,购物项2 3件,一共5件】
    private Integer countType;      // 商品数量,items的size()
    private BigDecimal totalAmount; // 商品总价
    private BigDecimal reduce = new BigDecimal("0.00");// 减免价格

    public List<CartItemVo> getItems() {
        return items;
    }

    public void setItems(List<CartItemVo> items) {
        this.items = items;
    }

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

    public Integer getCountType() {
        return items != null ? items.size() : 0;
    }


    public BigDecimal getTotalAmount() {
        BigDecimal amount = new BigDecimal("0");
        // 1、计算购物项总价
        if (!CollectionUtils.isEmpty(items)) {
            for (CartItemVo cartItem : items) {
                if (cartItem.getCheck()) {
                    amount = amount.add(cartItem.getTotalPrice());
                }
            }
        }
        // 2、计算优惠后的价格
        return amount.subtract(getReduce());
    }

    public BigDecimal getReduce() {
        return reduce;
    }

    public void setReduce(BigDecimal reduce) {
        this.reduce = reduce;
    }
}

2.6.3 商品信息SkuInfoVo

用于封装远程调用gulimall-product获取的商品信息

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/vo/SkuInfoVo.java

package site.zhourui.gulimall.cart.vo;

import lombok.Data;

import java.math.BigDecimal;

/**
 * @author zr
 * @date 2021/12/13 15:45
 */
@Data
public class SkuInfoVo {

    private Long skuId;
    /**
     * spuId
     */
    private Long spuId;
    /**
     * sku名称
     */
    private String skuName;
    /**
     * sku介绍描述
     */
    private String skuDesc;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     * 默认图片
     */
    private String skuDefaultImg;
    /**
     * 标题
     */
    private String skuTitle;
    /**
     * 副标题
     */
    private String skuSubtitle;
    /**
     * 价格
     */
    private BigDecimal price;
    /**
     * 销量
     */
    private Long saleCount;

}

2.6.4 用户信息UserInfoTo

用户封装ThreadLocal中传输的用户信息

package site.zhourui.common.to;

/**
 * @author zr
 * @date 2021/12/13 15:18
 */

import lombok.Data;
import lombok.ToString;

/**
 * 用户信息
 **/
@ToString
@Data
public class UserInfoTo {
    private Long userId;
    private String userKey; // 关联购物车
    private boolean tempUser = false;// 客户端是否需要存储cookie:user-key
}

2.7 线程池相关配置

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/config/ThreadPoolConfigProperties.java

package site.zhourui.gulimall.cart.config;

/**
 * @author zr
 * @date 2021/12/13 15:50
 */

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

/**
 * 线程池属性类
 **/

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

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/config/MyThreadConfig.java

package site.zhourui.gulimall.cart.config;

/**
 * @author zr
 * @date 2021/12/13 15:52
 */

import org.springframework.boot.context.properties.EnableConfigurationProperties;
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;

/**
 * 线程池配置类
 **/

@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor executor(ThreadPoolConfigProperties pool) {
        return new ThreadPoolExecutor(
                pool.getCoreSize(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
    }
}

gulimall-cart/src/main/resources/application.yaml

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

2.8 购物车相关常量

gulimall-common/src/main/java/site/zhourui/common/constant/CartConstant.java

package site.zhourui.common.constant;

/**
 * @author zr
 * @date 2021/12/13 15:20
 */
/**
 * 购物车常量
 **/
public class CartConstant {
    public final static String TEMP_USER_COOKIE_NAME = "user-key";
    public final static int TEMP_USER_COOKIE_TIMEOUT = 60*60*24*30;
    public final static String CART_PREFIX = "gulimall:cart:";
}

2.9 封装商品模块相关接口(用于远程调用)

​ gulimall-product/src/main/java/site/zhourui/gulimall/product/app/SkuInfoController.java

    /**
     * 根据skuId查询当前商品的最新价格
     * @param skuId
     * @return
     */
    @GetMapping(value = "/{skuId}/price")
    BigDecimal getPrice(@PathVariable("skuId") Long skuId){
        //获取当前商品的信息
        SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
        //获取商品的价格
        BigDecimal price = skuInfo.getPrice();
        return price;
    }

gulimall-product/src/main/java/site/zhourui/gulimall/product/app/SkuSaleAttrValueController.java

    /**
     * 根据skuId查询当前商品的最新价格
     * @param skuId
     * @return
     */
    @GetMapping(value = "/{skuId}/price")
    BigDecimal getPrice(@PathVariable("skuId") Long skuId){
        //获取当前商品的信息
        SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
        //获取商品的价格
        BigDecimal price = skuInfo.getPrice();
        return price;
    }


    /**
     * 信息
     */
    @RequestMapping("/info/{skuId}")
    public R info(@PathVariable("skuId") Long skuId){
		SkuInfoEntity skuInfo = skuInfoService.getById(skuId);

        return R.ok().put("skuInfo", skuInfo);
    }

2.10 远程调用接口

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/feign/ProductFeignService.java

package site.zhourui.gulimall.cart.feign;

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 site.zhourui.common.utils.R;

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

/**
 * @author zr
 * @date 2021/12/13 15:46
 */
@FeignClient("gulimall-product")
public interface ProductFeignService {
    /**
     * 根据skuId查询sku信息
     * @param skuId
     * @return
     */
    @RequestMapping("/product/skuinfo/info/{skuId}")
    R getInfo(@PathVariable("skuId") Long skuId);

    /**
     * 根据skuId查询pms_sku_sale_attr_value表中的信息
     * @param skuId
     * @return
     */
    @GetMapping(value = "/product/skusaleattrvalue/stringList/{skuId}")
    List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);

    /**
     * 根据skuId查询当前商品的最新价格
     * @param skuId
     * @return
     */
    @GetMapping(value = "/product/skuinfo/{skuId}/price")
    BigDecimal getPrice(@PathVariable("skuId") Long skuId);
}

2.11 封装购物车CRUD接口

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/service/CartService.java

package site.zhourui.gulimall.cart.service;

/**
 * @author zr
 * @date 2021/12/13 15:37
 */

import site.zhourui.gulimall.cart.vo.CartItemVo;
import site.zhourui.gulimall.cart.vo.CartVo;

import java.util.List;
import java.util.concurrent.ExecutionException;

/**
 * 购物车服务
 */
public interface CartService {
    /**
     * 将商品添加至购物车
     */
    CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException;

    /**
     * 获取购物车某个购物项
     */
    CartItemVo getCartItem(Long skuId);

    /**
     * 获取购物车里面的信息
     * @return
     */
    CartVo getCart() throws ExecutionException, InterruptedException;

    /**
     * 清空购物车的数据
     * @param cartKey
     */
    public void clearCartInfo(String cartKey);

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

    /**
     * 改变商品数量
     * @param skuId
     * @param num
     */
    void changeItemCount(Long skuId, Integer num);


    /**
     * 删除购物项
     * @param skuId
     */
    void deleteIdCartInfo(Integer skuId);

    /**
     * 获取当前用户的购物车所有商品项
     * @return
     */
    List<CartItemVo> getUserCartItems();
}

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/service/Impl/CartServiceImpl.java

package site.zhourui.gulimall.cart.service.Impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
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 org.springframework.util.StringUtils;
import site.zhourui.common.constant.CartConstant;
import site.zhourui.common.to.UserInfoTo;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.cart.Interceptor.CartInterceptor;
import site.zhourui.gulimall.cart.exception.CartExceptionHandler;
import site.zhourui.gulimall.cart.feign.ProductFeignService;
import site.zhourui.gulimall.cart.service.CartService;
import site.zhourui.gulimall.cart.vo.CartItemVo;
import site.zhourui.gulimall.cart.vo.CartVo;
import site.zhourui.gulimall.cart.vo.SkuInfoVo;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

/**
 * @author zr
 * @date 2021/12/13 15:38
 */
@Slf4j
@Service("cartService")
public class CartServiceImpl implements CartService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ProductFeignService productFeignService;
    @Autowired
    private ThreadPoolExecutor executor;

    /**
     * 跳转cartList页面
     * 封装购物车类【所有商品,所有商品的价格】
     * 【整合登录状态与未登录状态】
     */
    @Override
    public CartVo getCart() throws ExecutionException, InterruptedException {
        CartVo cartVo = new CartVo();
        UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
        System.out.println(userInfoTo);
        if (userInfoTo.getUserId() != null) {
            //  1)、登录后购物车的key
            String cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
            //  2)、临时购物车的key
            String temptCartKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
            //2、如果临时购物车的数据还未进行合并
            List<CartItemVo> tempCartItems = getCartItems(temptCartKey);
            if (tempCartItems != null) {
                //临时购物车有数据需要进行合并操作
                for (CartItemVo item : tempCartItems) {
                    addToCart(item.getSkuId(),item.getCount());
                }
                //清除临时购物车的数据
                clearCartInfo(temptCartKey);
            }
            //3、获取登录后的购物车数据【包含合并过来的临时购物车的数据和登录后购物车的数据】
            List<CartItemVo> cartItems = getCartItems(cartKey);
            cartVo.setItems(cartItems);
        } else {
            //没登录
            String cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
            //获取临时购物车里面的所有购物项
            List<CartItemVo> cartItems = getCartItems(cartKey);
            cartVo.setItems(cartItems);
        }
        return cartVo;
    }

    /**
     * 添加商品到购物车
     * @param skuId
     * @param num
     */
    @Override
    public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        //拿到要操作的购物车信息【cartOps就相当于绑定了当前用户购物车数据的hash】
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        //判断Redis是否有该商品的信息
        String productRedisValue = (String) cartOps.get(skuId.toString());
        //如果没有就添加数据【远程查询skuId】
        if (StringUtils.isEmpty(productRedisValue)) {
            //2、添加新的商品到购物车(redis)
            CartItemVo cartItem = new CartItemVo();
            //开启第一个异步任务
            CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
                //1、远程查询当前要添加商品的信息
                R productSkuInfo = productFeignService.getInfo(skuId);
                SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
                //数据赋值操作
                cartItem.setSkuId(skuInfo.getSkuId());
                cartItem.setTitle(skuInfo.getSkuTitle());
                cartItem.setImage(skuInfo.getSkuDefaultImg());
                cartItem.setPrice(skuInfo.getPrice());
                cartItem.setCount(num);
                cartItem.setCheck(true);
            }, executor);

            //开启第二个异步任务
            CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
                //2、远程查询skuAttrValues组合信息
                List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
                cartItem.setSkuAttrValues(skuSaleAttrValues);
            }, executor);
            //等待所有的异步任务全部完成
            CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();
            String cartItemJson = JSON.toJSONString(cartItem);
            cartOps.put(skuId.toString(), cartItemJson);
            return cartItem;
        } else {
            //购物车有此商品,修改数量即可
            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;
        }
    }

    /**
     * 获取到我们要操作的购物车
     * 简化代码:
     *      1、判断是否登录,拼接key
     *      2、数据是hash类型,所以每次要调用两次key【直接绑定外层key】
     *          第一层key:gulimall:cart:2
     *          第二层key:skuId
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
        //先得到当前用户信息
        UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
        String cartKey = "";
        if (userInfoTo.getUserId() != null) {
            //gulimall:cart:1
            cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
        } else {
            cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
        }
        //绑定指定的key操作Redis
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        return operations;
    }

    /**
     * 重定向页面获取当前购物车中sku商品信息
     * @param skuId
     * @return
     */
    @Override
    public CartItemVo getCartItem(Long skuId) {
        //拿到要操作的购物车信息
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        String redisValue = (String) cartOps.get(skuId.toString());
        CartItemVo cartItemVo = JSON.parseObject(redisValue, CartItemVo.class);
        return cartItemVo;
    }

    /**
     * 远程调用:订单服务调用【更新最新价格】
     * 获取当前用户购物车所有选中的商品项check=true【从redis中取】
     */
    @Override
    public List<CartItemVo> getUserCartItems() {
        List<CartItemVo> cartItemVoList = new ArrayList<>();
        //获取当前用户登录的信息
        UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
        //如果用户未登录直接返回null
        if (userInfoTo.getUserId() == null) {
            return null;
        } else {
            //获取购物车项
            String cartKey =CartConstant.CART_PREFIX + userInfoTo.getUserId();
            //获取所有的
            List<CartItemVo> cartItems = getCartItems(cartKey);
            if (cartItems == null) {
                throw new CartExceptionHandler();
            }
            //筛选出选中的
            cartItemVoList = cartItems.stream()
                    .filter(items -> items.getCheck())
                    .map(item -> {
                        //更新为最新的价格(查询数据库)
                        // redis中的价格不是最新的
                        BigDecimal price = productFeignService.getPrice(item.getSkuId());
                        item.setPrice(price);
                        return item;
                    })
                    .collect(Collectors.toList());
        }
        return cartItemVoList;
    }

    /**
     * 获取购物车里面的数据【根据key,包装成List<CartItemVo>】
     * key=【gulimall:cart:2 或 gulimall:cart:lkajkashjghj2989dsj】
     * @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 void clearCartInfo(String cartKey) {
        redisTemplate.delete(cartKey);
    }

    @Override
    public void checkItem(Long skuId, Integer check) {

        //查询购物车里面的商品
        CartItemVo cartItem = getCartItem(skuId);
        //修改商品状态
        cartItem.setCheck(check == 1?true:false);

        //序列化存入redis中
        String redisValue = JSON.toJSONString(cartItem);

        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.put(skuId.toString(),redisValue);

    }

    /**
     * 修改购物项数量
     * @param skuId
     * @param num
     */
    @Override
    public void changeItemCount(Long skuId, Integer num) {
        //查询购物车里面的商品
        CartItemVo cartItem = getCartItem(skuId);
        cartItem.setCount(num);
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        //序列化存入redis中
        String redisValue = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),redisValue);
    }

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

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/controller/CartController.java

package site.zhourui.gulimall.cart.controller;

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.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import site.zhourui.gulimall.cart.service.CartService;
import site.zhourui.gulimall.cart.vo.CartItemVo;
import site.zhourui.gulimall.cart.vo.CartVo;

import java.util.List;
import java.util.concurrent.ExecutionException;

/**
 * @author zr
 * @date 2021/12/13 10:50
 */
@Controller
public class CartController {

    @Autowired
    private CartService cartService;
    /**
     * 去购物车页面的请求【未登录状态也可以查看】
     * 浏览器有一个cookie:user-key 标识用户的身份,一个月过期
     * 如果第一次使用jd的购物车功能,都会给一个临时的用户身份:
     * 浏览器以后保存,每次访问都会带上这个cookie;
     *
     * 登录:session有
     * 没登录:按照cookie里面带来user-key来做
     * 第一次,如果没有临时用户,自动创建一个临时用户
     */
    @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";
    }

    /**
     * 添加商品到购物车
     * attributes.addFlashAttribute():将数据放在session中,可以在页面中取出,但是只能取一次
     * attributes.addAttribute():将数据放在url后面
     * @return
     */
    @GetMapping(value = "/addCartItem")
    public String addCartItem(@RequestParam("skuId") Long skuId,
                              @RequestParam("num") Integer num,
                              RedirectAttributes attributes) throws ExecutionException, InterruptedException {
        cartService.addToCart(skuId,num);
        attributes.addAttribute("skuId", skuId);// 给重定向请求用的【参数会拼接在下面请求之后】【转发会在请求域中】
        return "redirect:http://cart.gulimall.com/addToCartSuccessPage.html";
    }

    /**
     * 跳转到添加购物车成功页面【防止重复提交】
     */
    @GetMapping(value = "/addToCartSuccessPage.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,
                                       Model model) {
        //重定向到成功页面。再次查询购物车数据即可
        CartItemVo cartItemVo = cartService.getCartItem(skuId);
        model.addAttribute("cartItem",cartItemVo);
        return "success";
    }

    /**
     * 订单服务调用:【购物车页面点击确认订单时】
     * 返回所有选中的商品项【从redis中取】
     * 并且要获取最新的商品价格信息,而不是redis中的数据
     *
     * 获取当前用户的购物车所有商品项
     */
    @GetMapping(value = "/currentUserCartItems")
    @ResponseBody
    public List<CartItemVo> getCurrentCartItems() {
        List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
        return cartItemVoList;
    }

    /**
     * 更改选中状态
     */
    @GetMapping(value = "/checkItem")
    public String checkItem(@RequestParam(value = "skuId") Long skuId,
                            @RequestParam(value = "checked") Integer checked) {
        cartService.checkItem(skuId,checked);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

    /**
     * 改变商品数量
     */
    @GetMapping(value = "/countItem")
    public String countItem(@RequestParam(value = "skuId") Long skuId,
                            @RequestParam(value = "num") Integer num) {
        cartService.changeItemCount(skuId,num);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

    /**
     * 删除商品信息
     */
    @GetMapping(value = "/deleteItem")
    public String deleteItem(@RequestParam("skuId") Integer skuId) {
        cartService.deleteIdCartInfo(skuId);
        return "redirect:http://cart.gulimall.com/cart.html";
    }
}

2.12 cartList.html

https://gitee.com/zhourui815/gulimall/blob/master/gulimall-cart/src/main/resources/templates/cartList.html

2.13 success.html

https://gitee.com/zhourui815/gulimall/blob/master/gulimall-cart/src/main/resources/templates/success.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值