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

目录

  • 一、环境搭建
  • 二、购物车需求
  • 三、流程
    • 1. 身份鉴别
    • 2. 临时与登录购物车
    • 3. 添加购物车


一、环境搭建

创建购物车模块。。。

谷粒商城 高级篇 (十八) --------- 购物车_第1张图片

添加依赖。。。

谷粒商城 高级篇 (十八) --------- 购物车_第2张图片

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

谷粒商城 高级篇 (十八) --------- 购物车_第3张图片

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

谷粒商城 高级篇 (十八) --------- 购物车_第4张图片
将页面复制到项目的 template 文件夹下。。。注意改静态资源路径

谷粒商城 高级篇 (十八) --------- 购物车_第5张图片
将服务注册到注册中心。。。

谷粒商城 高级篇 (十八) --------- 购物车_第6张图片
排除数据库的相关配置。。。
谷粒商城 高级篇 (十八) --------- 购物车_第7张图片
配置 nacos 注册中心地址。。。

谷粒商城 高级篇 (十八) --------- 购物车_第8张图片
开启服务的注册与发现。。。

谷粒商城 高级篇 (十八) --------- 购物车_第9张图片

网关配置如下。。。
谷粒商城 高级篇 (十八) --------- 购物车_第10张图片

二、购物车需求

A、需求描述

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

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

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

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

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

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

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

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

B、数据结构

谷粒商城 高级篇 (十八) --------- 购物车_第11张图片
因此每一个购物项信息,都是一个对象,基本字段包括:

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

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

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

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

package com.fancy.gulimall.cart.vo;

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

/**
 * 整个购物车
 * 需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算
 */
public class Cart {

    List<CartItem> items;

    private Integer countNum;//商品数量

    private Integer countType;//商品类型数量

    private BigDecimal totalAmount;//商品总价

    private BigDecimal reduce = new BigDecimal("0.00");//减免价格

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

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

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


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


    public BigDecimal getTotalAmount() {
        BigDecimal amount = new BigDecimal("0");
        //1、计算购物项总价
        if (items != null && items.size() > 0) {
            for (CartItem item : items) {
                if(item.getCheck()){
                    BigDecimal totalPrice = item.getTotalPrice();
                    amount = amount.add(totalPrice);
                }
            }
        }

        //2、减去优惠总价
        BigDecimal subtract = amount.subtract(getReduce());

        return subtract;
    }


    public BigDecimal getReduce() {
        return reduce;
    }

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

CartItem 购物项封装成的 VO 如下

package com.fancy.gulimall.cart.vo;

import lombok.Data;

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

/**
* 购物项内容
*/

public class CartItem {
    private Long skuId;
    private Boolean check = true;
    private String title;
    private String image;
    private List<String> skuAttr;
    private BigDecimal price;
    private Integer count;
    private BigDecimal totalPrice;

    public Long getSkuId() {
        return skuId;
    }

    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }

    public Boolean getCheck() {
        return check;
    }

    public void setCheck(Boolean check) {
        this.check = check;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public List<String> getSkuAttr() {
        return skuAttr;
    }

    public void setSkuAttr(List<String> skuAttr) {
        this.skuAttr = skuAttr;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

    /**
     * 计算当前项的总价
     * @return
     */
    public BigDecimal getTotalPrice() {

        return this.price.multiply(new BigDecimal("" + this.count));
    }

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

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

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

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

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

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

三、流程

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

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

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

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

1. 身份鉴别

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

谷粒商城 高级篇 (十八) --------- 购物车_第12张图片

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

package com.fancy.gulimall.cart.interceptor;

import com.alibaba.nacos.client.utils.StringUtils;
import com.fancy.common.constant.AuthServerConstant;
import com.fancy.common.constant.CartConstant;
import com.fancy.common.vo.MemberRespVo;
import com.fancy.gulimall.cart.vo.UserInfoTo;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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


// 在执行目标方法之前,判断用户的登录状态。并封装传递(用户信息)给controller
public class CartInterceptor implements HandlerInterceptor {
    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserInfoTo userInfoTo = new UserInfoTo();

        HttpSession session = request.getSession();
        MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(member != null){
            //用户登录
            userInfoTo.setUserId(member.getId());
        }

        Cookie[] cookies = request.getCookies();
        if(cookies!=null && cookies.length>0){
            for (Cookie cookie : cookies) {
                //user-key
                String name = cookie.getName();
                if(name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
                    userInfoTo.setUserKey(cookie.getValue());
                    userInfoTo.setTempUser(true);
                }
            }
        }

        //如果没有临时用户一定分配一个临时用户
        if(StringUtils.isEmpty(userInfoTo.getUserKey())){
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }
        //目标方法执行之前
        threadLocal.set(userInfoTo);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserInfoTo userInfoTo = threadLocal.get();
        //如果没有临时用户一定保存一个临时用户
        if(!userInfoTo.isTempUser()){
            //持续的延长临时用户的过期时间
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
            cookie.setDomain("gulimall.com");
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
            response.addCookie(cookie);
        }
    }
    
}

拦截器配置:

package com.fancy.gulimall.cart.config;


import com.fancy.gulimall.cart.interceptor.CartInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}

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

2. 临时与登录购物车

A、临时购物车

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

B、登录购物车

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

3. 添加购物车

@Override
public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();

    String res = (String) cartOps.get(skuId.toString());
    if(StringUtils.isEmpty(res)){
        //购物车无此商品
        //2、添加新商品到购物车
        //1、远程查询当前要添加的商品的信息
        CartItem cartItem = new CartItem();
        CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
            R skuInfo = productFeignService.getSkuInfo(skuId);
            SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
            });

            cartItem.setCheck(true);
            cartItem.setCount(num);
            cartItem.setImage(data.getSkuDefaultImg());
            cartItem.setTitle(data.getSkuTitle());
            cartItem.setSkuId(skuId);
            cartItem.setPrice(data.getPrice());
        },executor);


        //2、远程查询sku的组合信息
        CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
            List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
            cartItem.setSkuAttr(values);
        }, executor);


        CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValues).get();
        String s = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),s);

        return cartItem;
    }
    else{
        //购物车有此商品,修改数量
        CartItem cartItem = JSON.parseObject(res, CartItem.class);
        cartItem.setCount(cartItem.getCount()+num);

        cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
        return cartItem;

    }

}

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

你可能感兴趣的:(微服务项目,数据库,redis,java)