购物车功能实现(拦截器Interceptor,ThreadLocal,多线程的使用)-53

一:购物车功能分析

1. 功能需求

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

2.数据结构

在这里插入图片描述

1)每一个购物车信息,都是一个对象,基本字段包括

{
    id: 1,
    userId: '2',
    skuId: 2131241,
    check: true, // 选中状态
    title: "Apple iphone.....",
    image: "...",
    price: 4999,
    count: 1,
    store: true, // 是否有货
    saleAttrs: [{..},{..}], // 销售属性
    sales: [{..},{..}] // 营销信息
}

2)购物车中不止一条数据,因此最终会是对象的数组

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

3.怎么保存

由于购物车是一个读多写多的场景,为了应对高并发场景,所有购物车采用的存储方案也和其他功能,有所差别。

1)主流的购物车数据存储方案

  1. redis(登录/未登录):性能高,代价高,不利于数据分析
  2. mysql(登录/未登录):性能低,成本低,利于数据分析
  3. cookie(未登录):未登录时,不需要和服务器交互,性能提高。其他请求会占用带宽
  4. localStorage/IndexedDB/WebSQL(未登录):不需要和服务器交互,不占用带宽

2)一般情况下,企业级购物车通常采用组合方案:

  1. cookie(未登录时) + mysql(登录时)
  2. cookie(未登录) + redis(登录时)
  3. localStorage/IndexedDB/WebSQL(未登录) + redis(登录)
  4. localStorage/IndexedDB/WebSQL(未登录) + mysql(登录)
    随着数据价值的提升,企业越来越重视用户数据的收集,现在以上4种方案使用的越来越少。

3)当前大厂普遍采用:redis + mysql

不管是否登录都把数据保存到mysql,为了提高性能可以搭建mysql集群,并引入redis。查询时,从redis查询提高查询速度,写入时,采用双写模式,mysql保存购物车很简单,创建一张购物车表即可。Redis有5种不同数据结构,这里选择哪一种比较合适呢?Map<String, List<String>>。首先不同用户应该有独立的购物车,因此购物车应该以用户的作为key来存储,Value是用户的所有购物车信息。这样看来基本的k-v结构就可以了。但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品id进行判断,为了方便后期处理,我们的购物车也应该是k-v结构,key是商品id,value才是这个商品的购物车信息。

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

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

4. 流程分析

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

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

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

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

二:搭建购物车服务

1.表设计

CREATE TABLE `cart_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(30) NOT NULL COMMENT '用户id或者userKey',
  `sku_id` bigint(20) NOT NULL COMMENT 'skuId',
  `check` tinyint(4) NOT NULL COMMENT '选中状态',
  `title` varchar(255) NOT NULL COMMENT '标题',
  `default_image` varchar(255) DEFAULT NULL COMMENT '默认图片',
  `price` decimal(18,2) NOT NULL COMMENT '加入购物车时价格',
  `count` int(11) NOT NULL COMMENT '数量',
  `store` tinyint(4) NOT NULL COMMENT '是否有货',
  `sale_attrs` varchar(100) DEFAULT NULL COMMENT '销售属性(json格式)',
  `sales` varchar(255) DEFAULT NULL COMMENT '营销信息(json格式)',
  PRIMARY KEY (`id`),
  KEY `idx_uid_sid` (`user_id`,`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.创建工程

pom依赖:

<?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>com.atguigu</groupId>
        <artifactId>gmall-1010</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.atguigu</groupId>
    <artifactId>gmall-cart</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gmall-cart</name>
    <description>谷粒商城购物车系统</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>gmall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>gmall-pms-interface</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>gmall-sms-interface</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>gmall-wms-interface</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>gmall-cart-interface</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.properties:

#应用名称
spring.application.name=gulimail-cart
#注册发现中心地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#服务端口号
server.port=30000
#redis的连接信息
spring.redis.host=47.97.50.125
spring.redis.port=6389

网关配置:

spring:
  cloud:
    gateway:
      routes:
        - id: gulimail_cart_route
          uri: lb://gulimail-cart
          predicates:
            - Host=cart.gulimail.com

3.CartItem——购物项内容

package com.sysg.gulimail.cart.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;

/**
 * 单个购物项内容
 */
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;
    }
}

4.Cart——整个购物车

计算的属性,必须重写get方法,保证每次获取到的属性就是最新计算的

package com.sysg.gulimail.cart.vo;

import lombok.Data;

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

/**
 * 整个购物车
 * 计算的属性,必须重写get方法,保证每次获取到的属性就是最新计算的
 */
public class Cart {
    /**
     * 购物项列表
     */
    List<CartItem> items;
    /**
     * 商品数量
     */
    private Integer countNum;
    /**
     * 商品类型数量
     */
    private Integer countType;
    /**
     * 购物车商品总价
     */
    private BigDecimal totalAmount;
    /**
     * 购物车减免价格
     */
    private BigDecimal reduce = new BigDecimal("0.00");

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

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

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


    public Integer getCountType() {
        int count = 0;
        if( items != null && items.size() > 0 ){
            for (CartItem item : items) {
                count = count + 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;
    }
}

三:通过拦截器校验用户登录状态

购物车系统根据用户的登录状态,购物车的增删改处理方式不同,因此需要添加登录校验。而登录状态的校验如果在每个方法中进行校验,会造成代码的冗余,不利于维护。所以这里使用拦截器统一处理。
springboot自定义拦截器:

  1. 编写自定义拦截器类实现HandlerInterceptor接口(前置方法 后置方法 完成方法)
  2. 编写配置类(添加@Configuration注解)实现WebMvcConfigurer接口(重写addInterceptors方法)

1)配置拦截器

/**
 * 拦截器配置
 */
@Configuration
public class GulimailWebConfig implements WebMvcConfigurer {
    
    @Autowired
    private CartInterceptor cartInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有路径
        registry.addInterceptor(cartInterceptor).addPathPatterns("/**");
    }
}

2)编写拦截器

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        return true;
    }

}
  • preHandle——在目标方法执行之前执行
  • return true;表示放行。反之就是拦截

四:传递登录信息——ThreadLocal

拦截器定义好了,将来怎么把拦截器中获取的用户信息传递给后续的每个业务逻辑:

  1. public类型的公共变量。线程不安全
  2. request对象。不够优雅
  3. ThreadLocal线程变量。推荐

1)编辑LoginInterceptor

@Component
public class LoginInterceptor implements HandlerInterceptor {

    // 声明线程的局部变量
    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserInfoTo userInfoTo = new UserInfoTo();
        HttpSession session = request.getSession();
        MemberRespVo member = (MemberRespVo)session.getAttribute(AuthServerConstant.LOGIN_USER);
        if( member != null ){
            //用户登录
            userInfoTo.setUserId(member.getId());
        } else {
            //用户未登录
            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());
                    }
                }
            }
        }
        //如果没有临时用户,一定分配一个临时用户
        if(StringUtils.isEmpty(userInfoTo.getUserKey())){
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
            userInfoTo.setTempUser(true);
        }
        //在目标方法执行之前,将信息封装到threadLocal里
        threadLocal.set(userInfoTo);
        return true;
    }

    /**
     * 封装了一个获取线程局部变量值的静态方法
     * @return
     */
    public static UserInfo getUserInfo(){
        return THREAD_LOCAL.get();
    }

    /**
     * 在视图渲染完成之后执行,经常在完成方法中释放资源
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        // 调用删除方法,是必须选项。因为使用的是tomcat线程池,请求结束后,线程不会结束。
        // 如果不手动删除线程变量,可能会导致内存泄漏
        THREAD_LOCAL.remove();
    }
}

2)声明ThreadLocal中的载荷对象userInfoTo

package com.sysg.gulimail.cart.vo;
import lombok.Data;
@Data
public class UserInfoTo {
    /**
     * 用户的id
     */
    private Long userId;
    /**
     * 用户的临时key
     */
    private String userKey;
    /**
     * 是否有临时用户
     */
    private Boolean tempUser = false;
}

3)在controller中尝试获取登录信息:

@Controller
public class CartController {

    @GetMapping("test")
    @ResponseBody
    public String test(){
        UserInfo userInfo = LoginInterceptor.getUserInfo();
        System.out.println(userInfo);
        return "hello cart!";
    }
}

4)拦截器代码实现——并且在拦截器中校验token

@Component
@EnableConfigurationProperties({JwtProperties.class})
public class LoginInterceptor implements HandlerInterceptor {

    // 声明线程的局部变量
    private static final ThreadLocal<UserInfo> THREAD_LOCAL = new ThreadLocal<>();

    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 获取登录头信息
        String userKey = CookieUtil.getCookieValue(request, jwtProperties.getUserKey());
        // 如果userKey为空,制作一个userKey放入cookie中
        if (StringUtils.isBlank(userKey)){
            userKey = UUID.randomUUID().toString();
            CookieUtil.setCookie(request, response, jwtProperties.getUserKey(), userKey, jwtProperties.getExpireTime());
        }
        UserInfo userInfo = new UserInfo();
        userInfo.setUserKey(userKey);

        // 获取用户的登录信息
        String token = CookieUtil.getCookieValue(request, jwtProperties.getCookieName());
        if (StringUtils.isNotBlank(token)){
            try {
                // 解析jwt
                Map<String, Object> map = JwtUtil.getInfoFromToken(token, jwtProperties.getPublicKey());
                Long userId = Long.valueOf(map.get("userId").toString());
                userInfo.setUserId(userId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 把信息放入线程的局部变量
        THREAD_LOCAL.set(userInfo);

        // 这里不做拦截,只为获取用户登录信息,不管有没有登录都要放行
        return true;
    }

    /**
     * 封装了一个获取线程局部变量值的静态方法
     * @return
     */
    public static UserInfo getUserInfo(){
        return THREAD_LOCAL.get();
    }

    /**
     * 在视图渲染完成之后执行,经常在完成方法中释放资源
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        // 调用删除方法,是必须选项。因为使用的是tomcat线程池,请求结束后,线程不会结束。
        // 如果不手动删除线程变量,可能会导致内存泄漏
        THREAD_LOCAL.remove();
    }
     /**
     * 业务执行以后,分配临时用户
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserInfoTo userInfoTo = threadLocal.get();
        //如果没有临时用户,需要保存
        if(!userInfoTo.getTempUser()){
            //持续延长过期时间
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
            response.addCookie(cookie);
        }
    }
}

5)JwtProperties读取配置类

@Data
@Slf4j
@ConfigurationProperties(prefix = "auth.jwt")
public class JwtProperties {
    private String pubKeyPath;
    private String cookieName;
    private String userKey;
    private Integer expireTime;
    private PublicKey publicKey;
    @PostConstruct
    public void init(){
        try {
            this.publicKey = RsaUtil.getPublicKey(pubKeyPath);
        } catch (Exception e) {
            log.error("生成公钥和私钥出错");
            e.printStackTrace();
        }
    }
}

对应配置如下:

auth:
  jwt:
    pubKeyPath: D:\\project-1010\\rsa\\rsa.pub
    cookieName: GMALL-TOKEN
    userKey: userKey
    expireTime: 15552000 # userKey的过期时间

五:新增购物车

1)编辑CartController

package com.sysg.gulimail.cart.controller;
import com.sysg.gulimail.cart.service.CartService;
import com.sysg.gulimail.cart.vo.Cart;
import com.sysg.gulimail.cart.vo.CartItem;
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.servlet.mvc.support.RedirectAttributes;
import java.util.concurrent.ExecutionException;

@Controller
public class CartController {
    @Autowired
    private CartService cartService;

    /**
     * 删除购物项
     * @return
     */
    @GetMapping("/deleteItem")
    public String deleteItem(@RequestParam("skuId") Long skuId){
        cartService.deleteItem(skuId);
        return "redirect:/cart.html";
    }

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


    /**
     * 修改选中商品的状态
     * @param skuId
     * @param check
     * @return
     */
    @GetMapping("/checkItem")
    public String checkItem(@RequestParam("skuId") Long skuId,@RequestParam("check")Integer check){
        cartService.checkItem(skuId,check);
        return "redirect:/cart.html";
    }

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

    /**
     * 添加商品到购物车
     * redirectAttributes.addAttribute:将数据放到url后面
     * @param skuId 商品id
     * @param num 商品数量
     * @return
     */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num, RedirectAttributes redirectAttributes) throws ExecutionException, InterruptedException {
        cartService.addToCart(skuId,num);
        redirectAttributes.addAttribute("skuId",skuId);
        return "redirect:/addToCartSuccess.html";
    }

    @GetMapping("/addToCartSuccess.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId, Model model){
        //重定向到成功页面,再次查询购物车数据即可
        CartItem cartItem = cartService.getCartItem(skuId);
        model.addAttribute("item",cartItem);
        return "success";
    }
}

2)创建线程池——线程池的使用

1.编写线程配置类

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

2.配置参数

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

3.编写配置文件

gulimail.thread.core-size=20
gulimail.thread.max-size=200
gulimail.thread.keep-alive-time=10

3)编辑CartService

基本思路:

  • 先查询之前的购物车数据
  • 判断要添加的商品是否存在
    • 存在:则直接修改数量后写回Redis及mysql
    • 不存在:新建一条数据,然后写入Redis及mysql
@Slf4j
@Service
public class CartServiceImpl implements CartService {

    private static final String CART_PREFIX = "gulimail:cart:";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private ProductFeignService productFeignService;
    @Autowired
    private ThreadPoolExecutor executor;

    /**
     * 往购物车添加商品
     * @param skuId
     * @param num
     * @return
     */
    @Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        String res = cartOps.get(skuId.toString()).toString();
        if(StringUtils.isEmpty(res)){
            CartItem cartItem = new CartItem();
            //购物车无此商品
            //1.新商品添加到购物车
            CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
                //2.远程查询我们需要添加的商品信息
                R skuInfo = productFeignService.getSkuInfo(skuId);
                SkuInfoVo data = skuInfo.getData(new TypeReference<SkuInfoVo>() {
                });
                cartItem.setCheck(true);
                cartItem.setCount(num);
                cartItem.setImage(data.getSkuDefaultImg());
                cartItem.setTitle(data.getSkuTitle());
                cartItem.setSkuId(skuId);
                cartItem.setPrice(data.getPrice());
            },executor);
            //3.远程查询sku得到组合信息
            CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
                List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
                cartItem.setSkuAttr(skuSaleAttrValues);
            }, executor);
            //等两个业务同时查询结束再去保存到redis
            CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValues).get();
            cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
            return cartItem;
        } else {
            //购物车有次商品,修改数量即可
            CartItem cartItem = JSON.parseObject(res, CartItem.class);
            cartItem.setCount(cartItem.getCount()+num);
            //更新redis的数据
            cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
            return cartItem;
        }
    }

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

    @Override
    public Cart getCart() throws ExecutionException, InterruptedException {
        Cart cart = new Cart();
        //1.判断是否登录
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if(userInfoTo.getUserId()!=null){
            //登录
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            //如果有临时购物车,需要合并
            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);
            }
            //获取登陆后的购物车的数据,包含临时购物车和登陆后的购物车
            List<CartItem> cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
        } else {
            //没登录
            String cartKey = CART_PREFIX + userInfoTo.getUserKey();
            //获取临时购物车
            List<CartItem> cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
        }
        return null;
    }

    private List<CartItem> getCartItems(String cartKey){
        BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps(cartKey);
        List<Object> values = hashOps.values();
        if( values != null && values.size() > 0 ){
            List<CartItem> collect = values.stream().map( obj -> {
                String str = obj.toString();
                CartItem cartItem = JSON.parseObject(str, CartItem.class);
                return cartItem;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }

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

    /**
     * 清楚临时购物车的数据
     */
    @Override
    public void clearCart(String cartKey){
        stringRedisTemplate.delete(cartKey);
    }

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

    /**
     * 修改数量
     * @param skuId
     * @param num
     */
    @Override
    public void changeItemCount(Long skuId, Integer num) {
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCount(num);
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        //修改后的数据放进redis
        cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
    }

    @Override
    public void deleteItem(Long skuId) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.delete(skuId.toString());
    }

注:获取,删除,修改购物车见上述controller和service

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随意石光

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值