一:购物车功能分析
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)主流的购物车数据存储方案:
- redis(登录/未登录):性能高,代价高,不利于数据分析
- mysql(登录/未登录):性能低,成本低,利于数据分析
- cookie(未登录):未登录时,不需要和服务器交互,性能提高。其他请求会占用带宽
- localStorage/IndexedDB/WebSQL(未登录):不需要和服务器交互,不占用带宽
2)一般情况下,企业级购物车通常采用组合方案:
- cookie(未登录时) + mysql(登录时)
- cookie(未登录) + redis(登录时)
- localStorage/IndexedDB/WebSQL(未登录) + redis(登录)
- 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自定义拦截器:
- 编写自定义拦截器类实现HandlerInterceptor接口(前置方法 后置方法 完成方法)
- 编写配置类(添加@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
拦截器定义好了,将来怎么把拦截器中获取的用户信息传递给后续的每个业务逻辑:
- public类型的公共变量。线程不安全
- request对象。不够优雅
- 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