一、环境搭建

1、创建gulimall-cart模块

image.pngimage.png

2、修改域名

修改C:\Windows\System32\drivers\etc\hosts里的域名
image.png

3、复制静态资源

在/mydata/nginx/html/static/目录创建cart文件夹,将所有的静态资源全部都传到虚拟机/mydata/nginx/html/static/cart目录下
image.png
将两个静态页面加入gulimall-cart服务里
image.png

4、添加配置

  1. server.port=30000
  2. spring.application.name=gulimall-cart
  3. spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
  4. spring.redis.host=192.168.195.128
  5. gulimall.thread.core-size=20
  6. gulimall.thread.max-size=200
  7. gulimall.thread.keep-alive-time=10

5、为主启动类添加注解

  1. package com.atguigu.gulimall.cart;
  2. @EnableFeignClients
  3. @EnableDiscoveryClient
  4. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
  5. public class GulimallCartApplication {
  6. public static void main(String[] args) {
  7. SpringApplication.run(GulimallCartApplication.class, args);
  8. }
  9. }

6、修改网关,给购物车配置路由

image.png

7、测试

先把cartList.html改成index.html启动项目
http://cart.gulimall.com/
image.png

二、数据模型分析

1、需求描述

1.1 用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
- 放入数据库
购物车是一个读多写多的场景,因此放入数据库并不合适
- mongodb
- 放入 redis(采用)
登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车
Redis的好处:

  1. - **数据结构好组织**
  2. - **Redis具有极高的读写并发性能**
  3. - **Redis是内存数据库,一旦宕机就丢失数据,可以在安装Redis的时候指定好持久化策略(损失部分性能,保证数据不丢失)**
  4. - <br />

1.2 用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
- 放入 localstorage(客户端存储,后台不存)
- cookie(客户端存储)
- WebSQL (客户端存储)
- 放入 redis(采用)
浏览器即使关闭,下次进入,临时购物车数据都在

1.3 其他功能
- 用户可以使用购物车一起结算下单
- 给购物车添加商品
- 用户可以查询自己的购物车
- 用户可以在购物车中修改购买商品的数量
- 用户可以在购物车中删除商品
- 选中不选中商品
- 在购物车中展示商品优惠信息
- 提示购物车商品价格变化

2、数据结构

一个购物车是由各个购物项组成的,但是我们用List进行存储并不合适,因为使用List查找某个购物项时需要逐个遍历每个购物项,会造成大量时间损耗,为保证查找速度,我们使用hash进行存储
image.png
因此每一个购物项信息,都是一个对象,基本字段包括:

  1. {
  2. skuId: 2131241,
  3. check: true, // 是否被选中
  4. title: "Apple iphone.....", // 商品标题
  5. defaultImage: "...", // 商品默认图片
  6. price: 4999,
  7. count: 1,
  8. totalPrice: 4999,
  9. skuSaleVO: {...} // 商品销售属性组合
  10. }

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

  1. [
  2. {...},
  3. {...},
  4. {...}
  5. ]

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

3、VO编写

3.1 购物项VO

  1. package com.atguigu.gulimall.cart.vo;
  2. /**
  3. * 购物车中的每个购物项内容
  4. */
  5. //@Data 不用@Data,避免把get、set方法内容覆盖了。totalPrice需要计算得到
  6. public class CartItem {
  7. private Long skuId;
  8. private Boolean check=true; // 购物项是否被选中,默认为选中状态
  9. private String title ; // 商品标题
  10. private String image; // 商品图片
  11. private List<String> skuAttr; // 套餐信息
  12. private BigDecimal price; // 商品价格
  13. private Integer count; // 商品数量
  14. private BigDecimal totalPrice; // 商品总价
  15. /**
  16. * 计算当前购物项的总价
  17. * new BigDecimal("" + this.count)把count变成字符串
  18. * @return
  19. */
  20. public BigDecimal getTotalPrice() {
  21. return this.price.multiply(new BigDecimal("" + this.count));
  22. }
  23. public void setTotalPrice(BigDecimal totalPrice) {
  24. this.totalPrice = totalPrice;
  25. }
  26. ***其他getset***
  27. }

3.2 整个购物车VO

  1. package com.atguigu.gulimall.cart.vo;
  2. /**
  3. * 整个购物车
  4. * 需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算
  5. */
  6. public class Cart {
  7. private List<CartItem> Items; // 所有的购物项
  8. private Integer countNum; // 商品数量
  9. private Integer countType; // 商品类型
  10. private BigDecimal totalAmount; // 商品总价
  11. private BigDecimal reduce = new BigDecimal(0); // 商品减免价格
  12. // 获取商品总数量
  13. public Integer getCountNum() {
  14. int count = 0;
  15. if (Items != null && Items.size() > 0) {
  16. for (CartItem item : Items) {
  17. count += item.getCount();
  18. }
  19. }
  20. return count;
  21. }
  22. // 获取商品总类型
  23. public Integer getCountType() {
  24. int count = 0;
  25. if (Items != null && Items.size() > 0) {
  26. for (CartItem item : Items) {
  27. count += 1;
  28. }
  29. }
  30. return count;
  31. }
  32. // 获取商品总价
  33. public BigDecimal getTotalAmount() {
  34. BigDecimal amount = new BigDecimal(0);
  35. // 1.计算购物车所有商品总价
  36. if (Items != null && Items.size() > 0) {
  37. for (CartItem item : Items) {
  38. if(item.getCheck()){
  39. BigDecimal totalPrice = item.getTotalPrice();
  40. amount = amount.add(totalPrice);
  41. }
  42. }
  43. }
  44. // 2.减去优惠价格
  45. BigDecimal amountFinal = amount.subtract(getReduce());
  46. return amountFinal;
  47. }
  48. ***其他getset方法***
  49. }

4、导入redis和SpringSession依赖

  1. <!--整合SpringSession完成session共享问题-->
  2. <dependency>
  3. <groupId>org.springframework.session</groupId>
  4. <artifactId>spring-session-data-redis</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-data-redis</artifactId>
  9. </dependency>

5、添加SpringSession配置类

  1. package com.atguigu.gulimall.cart.config;
  2. @EnableRedisHttpSession // 自动开启RedisHttpSession
  3. @Configuration
  4. public class GulimallSessionConfig {
  5. @Bean
  6. public CookieSerializer cookieSerializer(){
  7. DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
  8. //session作用域
  9. cookieSerializer.setDomainName("gulimall.com");
  10. cookieSerializer.setCookieName("GULISESSION");
  11. return cookieSerializer;
  12. }
  13. @Bean
  14. public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
  15. //序列化机制
  16. return new GenericJackson2JsonRedisSerializer();
  17. }
  18. }

三、ThreadLocal用户身份鉴别

1、功能分析

image.png
image.png
user-key 是随机生成的 id,不管有没有登录都会有这个 cookie 信息,有效期为一个月
两个功能:新增商品到购物车、查询购物车。
新增商品:判断是否登录
- 是:则添加商品到后台 Redis 中,把 user 的唯一标识符作为 key。
- 否:则添加商品到后台 redis 中,使用随机生成的 user-key 作为 key。
查询购物车列表:判断是否登录
- 否:直接根据 user-key 查询 redis 中数据并展示
- 是:已登录,则需要先根据 user-key 查询 redis 是否有数据。
- 有:需要提交到后台添加到 redis,合并数据,而后查询。
- 否:直接去后台查询 redis,而后返回。

(1) 用户身份鉴别方式
参考京东,在点击购物车时,会为临时用户生成一个name为user-key的cookie临时标识,过期时间为一个月,如果手动清除user-key,那么临时购物车的购物项也被清除,所以user-key是用来标识和存储临时购物车数据

(2) 使用ThreadLocal进行用户身份鉴别信息传递
在调用购物车的接口前,先通过session信息判断是否登录,并分别进行用户身份信息的封装,并把user-key放在cookie中
image.png

2、登录拦截器

2.1 UserInfoTo

  1. package com.atguigu.gulimall.cart.vo;
  2. // 传输对象to
  3. @ToString
  4. @Data
  5. public class UserInfoTo {
  6. private Long userId; // 登录后使用userId
  7. private String userKey; // 未登录使用用户临时键
  8. private boolean tempUser = false; // 判断是否存在临时用户信息
  9. }

2.2 CartConstant常量

  1. package com.atguigu.common.constant;
  2. public class CartConstant {
  3. public static final String TEMP_USER_COOKIE_NAME="user-key";
  4. public static final int TEMP_USER_COOKIE_TIMEOUT=60*60*24*30;
  5. }

2.3 实现拦截器

  1. package com.atguigu.gulimall.cart.interceptor;
  2. /**
  3. * 在执行目标方法前,先判断用户登录状态。并封装用户信息传递给Controller
  4. */
  5. public class CartInterceptor implements HandlerInterceptor {
  6. // 同一线程共享数据
  7. public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
  8. /**
  9. * 目标方法执行之前拦截,判断用户是否已登录
  10. * @return return true表示放行该方法;return false表示拒绝该方法;
  11. */
  12. @Override
  13. public boolean preHandle(HttpServletRequest request,
  14. HttpServletResponse response, Object handler) throws Exception {
  15. UserInfoTo userInfoTo = new UserInfoTo();
  16. HttpSession session = request.getSession();
  17. MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
  18. if (member != null) {
  19. // 用户登录
  20. userInfoTo.setUserId(member.getId());
  21. }
  22. Cookie[] cookies = request.getCookies();
  23. if (cookies != null && cookies.length > 0) {
  24. for (Cookie cookie : cookies) {
  25. String name = cookie.getName();
  26. // 判断获取到的cookie是否存在临时用户标识user-key
  27. if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)) {
  28. userInfoTo.setUserKey(cookie.getValue());
  29. userInfoTo.setTempUser(true);
  30. }
  31. }
  32. }
  33. // 如果没有临时用户,分配临时用户
  34. if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
  35. String uuid = UUID.randomUUID().toString();
  36. userInfoTo.setUserKey(uuid);
  37. }
  38. // 目标方法执行之前,将用户信息放入ThreadLocal
  39. threadLocal.set(userInfoTo);
  40. return true;
  41. }
  42. // 业务执行之后,将临时用户user-key保存至浏览器的cookie中
  43. @Override
  44. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
  45. UserInfoTo userInfoTo = threadLocal.get();
  46. // 如果没有任何信息,创建临时用户,保存cookie
  47. if (!userInfoTo.isTempUser()) {
  48. Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
  49. cookie.setDomain("gulimall.com");
  50. // 单位秒
  51. cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
  52. response.addCookie(cookie);
  53. }
  54. }
  55. }

2.4 添加拦截器的配置

不能只把拦截器加入容器中,不然拦截器不生效的

  1. package com.atguigu.gulimall.cart.config;
  2. @Configuration
  3. public class GulimallWebConfig implements WebMvcConfigurer{
  4. @Override
  5. public void addInterceptors(InterceptorRegistry registry) {
  6. // addInterceptor:添加拦截器
  7. // addPathPatterns:被拦截的请求,/**:拦截所有请求
  8. registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
  9. }
  10. }

四、添加商品到购物车

业务逻辑:
若当前商品已经存在购物车,只需增添数量
否则需要查询商品购物项所需信息,并添加新商品至购物车

1、CartController

  1. /**
  2. * 添加商品到购物车
  3. * RedirectAttributes attributes
  4. * attributes.addFlashAttribute():将数据放在session里面可以在页面取出,但只能取一次
  5. * attributes.addAttribute("skuId",skuId):将数据放在url后面
  6. * @return
  7. */
  8. @GetMapping("/addToCart")
  9. public String addToCart(@RequestParam("skuId")Long skuId,
  10. @RequestParam("num") Integer num,
  11. RedirectAttributes ra) throws ExecutionException, InterruptedException {
  12. cartService.addToCart(skuId,num);
  13. ra.addAttribute("skuId",skuId);
  14. // 重定向到成功页面,防止刷新页面重复提交商品,同时将商品ID传递过去
  15. return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
  16. }

image.png

image.png
image.png

  1. @GetMapping("/addToCartSuccess.html")
  2. public String addToCartSuccessPage(@RequestParam("skuId")Long skuId, Model model){
  3. // 重定向到成功页面,再次查询购物车数据
  4. CartItem items = cartService.getCartItem(skuId);
  5. model.addAttribute("items",items);
  6. return "success";
  7. }

2、CartServiceImpl

  1. @Autowired
  2. StringRedisTemplate redisTemplate;
  3. @Autowired
  4. ProductFeignService productFeignService;
  5. @Autowired
  6. ThreadPoolExecutor executor;
  7. private final String CART_PREFIX = "gulimall:cart:";
  8. @Override
  9. public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
  10. BoundHashOperations<String, Object, Object> cartOps = getCartOps();
  11. String res = (String) cartOps.get(skuId.toString());
  12. if (StringUtils.isEmpty(res)) {
  13. // 购物车中无此商品,新商品添加到购物车
  14. CartItem cartItem = new CartItem();
  15. // 1、远程查询当前要添加的商品信息
  16. CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
  17. R skuInfo = productFeignService.getSkuInfo(skuId);
  18. SkuInfoVo data = skuInfo.getData2("skuInfo", new TypeReference<SkuInfoVo>() {});
  19. cartItem.setCheck(true);
  20. cartItem.setCount(num);
  21. cartItem.setImage(data.getSkuDefaultImg());
  22. cartItem.setTitle(data.getSkuTitle());
  23. cartItem.setSkuId(skuId);
  24. cartItem.setPrice(data.getPrice());
  25. }, executor);
  26. // 2、远程查询sku的组合信息
  27. CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
  28. List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
  29. cartItem.setSkuAttr(values);
  30. }, executor);
  31. // 把cartItem对象转换为json格式
  32. CompletableFuture.allOf(getSkuInfoTask, getSkuSaleAttrValues).get();
  33. String s = JSON.toJSONString(cartItem);
  34. cartOps.put(skuId.toString(), s);
  35. return cartItem;
  36. } else {
  37. // 购物车有此商品,修改数量
  38. // 将json逆转为CartItem
  39. CartItem cartItem = JSON.parseObject(res, CartItem.class);
  40. cartItem.setCount(cartItem.getCount() + num);
  41. // 再将CartItem换回json存入redis
  42. cartOps.put(skuId.toString(), JSON.toJSONString(cartItem));
  43. return cartItem;
  44. }
  45. }
  1. /**
  2. * 获取我们要操作的购物车,临时购物车、用户购物车
  3. */
  4. private BoundHashOperations<String, Object, Object> getCartOps() {
  5. UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
  6. // 判断真实用户还是临时用户
  7. String cartKry = "";
  8. if (userInfoTo.getUserId() != null) {
  9. cartKry = CART_PREFIX + userInfoTo.getUserId();
  10. } else {
  11. cartKry = CART_PREFIX + userInfoTo.getUserKey();
  12. }
  13. // 绑定key,以后所有redis操作都针对此key
  14. BoundHashOperations<String, Object, Object> operation = redisTemplate.boundHashOps(cartKry);
  15. return operation;
  16. }

五、获取购物车

若用户未登录,则直接使用user-key获取购物车数据
否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车

1、CartController

  1. /**
  2. * 浏览器有一个cookies,user-key:标识用户身份,过期时间一个月。
  3. * 浏览器以后添加商品,每次访问都会带上这个cookie
  4. * 第一次登录需要创建一个临时用户
  5. *
  6. * 登录:有session
  7. * 未登录:按照cookie里面带来的user-key来识别用户身份
  8. * 第一次:如果没有临时用户身份,帮忙创建一个临时用户身份
  9. * @param
  10. * @return
  11. */
  12. @GetMapping("/cart.html")
  13. public String cartListPage(Model model) throws ExecutionException, InterruptedException {
  14. // 快速得到用户信息
  15. UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
  16. Cart cart = cartService.getCart();
  17. model.addAttribute("cart",cart);
  18. return "cartList";
  19. }

2、CartServiceImpl

  1. // 获取购物车
  2. @Override
  3. public Cart getCart() throws ExecutionException, InterruptedException {
  4. Cart cart = new Cart();
  5. // 判断登录状态
  6. UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
  7. if (userInfoTo.getUserId() != null) {
  8. // 1.已登录
  9. String cartKey = CART_PREFIX + userInfoTo.getUserId();
  10. String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
  11. // 1.2 如果临时购物车的数据还没有合并?
  12. // 先获取临时购物车,然后合并
  13. List<CartItem> tempCartItems = getCartItems(tempCartKey);
  14. if (tempCartItems != null) {
  15. for (CartItem item : tempCartItems) {
  16. addToCart(item.getSkuId(), item.getCount());
  17. }
  18. }
  19. // 最后清空临时购物车
  20. clearCart(tempCartKey);
  21. // 1.3 最终获取登录后的购物车数据
  22. List<CartItem> cartItems = getCartItems(cartKey);
  23. cart.setItems(cartItems);
  24. } else {
  25. // 2.没登录
  26. String cartKey = CART_PREFIX + userInfoTo.getUserKey();
  27. // 获取临时购物车的所有购物项
  28. List<CartItem> cartItems = getCartItems(cartKey);
  29. cart.setItems(cartItems);
  30. }
  31. return cart;
  32. }
  33. // 清空临时购物车
  34. @Override
  35. public void clearCart(String cartKey) {
  36. redisTemplate.delete(cartKey);
  37. }
  38. /**
  39. * 获取购物车中的购物项
  40. */
  41. private List<CartItem> getCartItems(String cartKey) {
  42. BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(cartKey);
  43. List<Object> values = hashOps.values();
  44. if (values != null && values.size() > 0) {
  45. List<CartItem> collect = values.stream().map((obj) -> {
  46. String str = (String) obj;
  47. CartItem cartItem = JSON.parseObject(str, CartItem.class);
  48. return cartItem;
  49. }).collect(Collectors.toList());
  50. return collect;
  51. }
  52. return null;
  53. }

六、选中购物项

1、 CartController

  1. // 勾选购物项
  2. @GetMapping("/checkItem")
  3. public String checkItem(@RequestParam("skuId") Long skuId,
  4. @RequestParam("check") Integer check){
  5. cartService.checkItem(skuId,check);
  6. return "redirect:http://cart.gulimall.com/cart.html";
  7. }

2、 CartServiceImpl

  1. // 勾选购物项
  2. @Override
  3. public void checkItem(Long skuId, Integer check) {
  4. BoundHashOperations<String, Object, Object> cartOps = getCartOps();
  5. CartItem cartItem = getCartItem(skuId);
  6. cartItem.setCheck(check == 1 ? true : false);
  7. String s = JSON.toJSONString(cartItem);
  8. cartOps.put(skuId.toString(), s);
  9. }
  10. // 获取我们要操作的购物车,临时购物车、用户购物车
  11. private BoundHashOperations<String, Object, Object> getCartOps() {
  12. UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
  13. // 判断真实用户还是临时用户
  14. String cartKry = "";
  15. if (userInfoTo.getUserId() != null) {
  16. cartKry = CART_PREFIX + userInfoTo.getUserId();
  17. } else {
  18. cartKry = CART_PREFIX + userInfoTo.getUserKey();
  19. }
  20. // 绑定key,以后所有redis操作都针对此key
  21. BoundHashOperations<String, Object, Object> operation = redisTemplate.boundHashOps(cartKry);
  22. return operation;
  23. }
  24. // 查询购物车的某个购物项
  25. @Override
  26. public CartItem getCartItem(Long skuId) {
  27. BoundHashOperations<String, Object, Object> cartOps = getCartOps();
  28. String str = (String) cartOps.get(skuId.toString());
  29. CartItem cartItem = JSON.parseObject(str, CartItem.class);
  30. return cartItem;
  31. }

七、修改购物项数据

1、 CartController

  1. // 修改购物项数量
  2. @GetMapping("/countItem")
  3. public String countItem(@RequestParam("skuId") Long skuId,
  4. @RequestParam("num")Integer num){
  5. cartService.countItem(skuId,num);
  6. return "redirect:http://cart.gulimall.com/cart.html";
  7. }

2、 CartServiceImpl

  1. // 修改购物项数量
  2. @Override
  3. public void countItem(Long skuId, Integer num) {
  4. BoundHashOperations<String, Object, Object> cartOps = getCartOps();
  5. CartItem cartItem = getCartItem(skuId);
  6. cartItem.setCount(num);
  7. String s = JSON.toJSONString(cartItem);
  8. cartOps.put(skuId.toString(), s);
  9. }

八、删除购物项

1、 CartController

  1. // 删除购物项
  2. @GetMapping("/deleteItem")
  3. public String deleteItem(@RequestParam("skuId") Long skuId){
  4. cartService.deleteItem(skuId);
  5. return "redirect:http://cart.gulimall.com/cart.html";
  6. }

2、 CartServiceImpl

  1. // 删除购物项
  2. @Override
  3. public void deleteItem(Long skuId) {
  4. BoundHashOperations<String, Object, Object> cartOps = getCartOps();
  5. cartOps.delete(skuId.toString());
  6. }
  1. <br />[<br />](https://blog.csdn.net/wts563540/article/details/109713677)