谷粒商城–购物车–高级篇笔记九
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, 值是购物项数据
1.3 流程
参照京东
user-key 是随机生成的 id, 不管有没有登录都会有这个 cookie 信息。
两个功能: 新增商品到购物车、 查询购物车。
新增商品: 判断是否登录
- 是: 则添加商品到后台 Redis 中, 把 user 的唯一标识符作为 key。
- 否: 则添加商品到后台 redis 中, 使用随机生成的 user-key 作为 key。
- 查询购物车列表: 判断是否登录
- 否: 直接根据 user-key 查询 redis 中数据并展示
- 是: 已登录, 则需要先根据 user-key 查询 redis 是否有数据。
- 有: 需要提交到后台添加到 redis, 合并数据, 而后查询。
- 否: 直接去后台查询 redis, 而后返回
2. 环境搭建
2.1 新建模块
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
2.3 静态资源分离
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 同一个线程共享数据
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