谷粒商城12——购物车模块、消息队列RabbitMQ

十、购物车模块

1.需求分析

用户在登录状态下将商品添加到购物车

  • 放入数据库(经常访问、修改不显示)
  • 放入 mongodb(Nosql 数据库 比上面的好一点点)
  • 放入 redis (采用)

用户在未登录状态下点击购物车会转到登录页面

存储:

  • 使用 hash 数据类型来存储商品信息,方便商品的CRUD

用户在购物车中可以

  • 添加商品
  • 查询购物车
  • 修改购买商品的数量
  • 删除商品

image-20220803203852882

2.封装vo

package com.henu.soft.merist.cart.vo;

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

/**
 * 单个购物项内容
 */
public class CartItemVo {

    private Long skuId;

    private Boolean check = true;

    private String title;

    private String image;

    private List<String> skuAttrValue;

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

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

    /**
     * 计算当前项的总价
     * @return
     */
    public BigDecimal getTotalPrice(){
        return this.price.multiply(new BigDecimal(""+this.count));
    }
}

package com.henu.soft.merist.cart.vo;

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

/**
 * 整个购物车
 */
public class CartVo {

    List<CartItemVo> items;

    private Integer countNum;//商品数量

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

    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 void setCountNum(Integer countNum) {
        this.countNum = countNum;
    }

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

    public void setCountType(Integer countType) {
        this.countType = countType;
    }

    public BigDecimal getTotalAmount() {
        BigDecimal total = new BigDecimal(0);
        if (items != null && items.size() > 0){
            for (CartItemVo item : items) {
                total = total.add(item.getTotalPrice());
            }
        }
        total = total.subtract(reduce);
        return total;
    }

    public void setTotalAmount(BigDecimal totalAmount) {
        this.totalAmount = totalAmount;
    }

    public BigDecimal getReduce() {
        return reduce;
    }

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

package com.henu.soft.merist.cart.vo;
import lombok.Data;

import java.math.BigDecimal;

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

}

3.添加商品

添加完成之后重定向避免刷新页面重复添加商品

若是将添加、跳转的逻辑都放在addCartItem一个controller的话,刷新页面会重复提交cartService.addCartItem(skuId, num);

因此需要添加完成重定向到 addCartItemSuccess 的controller 查询redis 在跳转到显示页面

/**
 * 添加商品到购物车
 * RedirectAttributes.addFlashAttribute():将数据放在session中,可以在页面中取出,但是只能取一次
 * RedirectAttributes.addAttribute():将数据放在url后面
 * @return
 */
@GetMapping("/addCartItem")
public String addCartItem(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num, RedirectAttributes attributes,HttpSession session){
    MemberResponseTo memberResponseTo = (MemberResponseTo) session.getAttribute(AuthServerConstant.LOGIN_USER);
    cartService.addCartItem(skuId,num,memberResponseTo.getId());
    attributes.addAttribute("skuId",skuId);
    attributes.addAttribute("userId",memberResponseTo.getId());
    return "redirect:http://cart.gulimall.com/addCartItemSuccess";
}
@GetMapping("/addCartItemSuccess")
public String addCartItemSuccess(@RequestParam("skuId") Long skuId,@RequestParam("userId") Long userId, Model model){
    //添加成功 返回根据skuid在购物车中查到的数据
    CartItemVo cartItemVo = cartService.getCartItem(skuId,userId);
    model.addAttribute("cartItem",cartItemVo);
    return "success";
}

使用线程池 异步查询sku 的属性(远程调用 product模块),节省时间,CompletableFuture.allOf(future1,future2).get();在两个异步操作完成之前阻塞线程,确保获取到数据再添加。

@Override
public CartItemVo addCartItem(Long skuId, Integer num, Long userId) {
    BoundHashOperations<String,Object,Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX + userId);
    // 判断当前商品是否已经存在购物车
    String cartJson = (String) ops.get(skuId.toString());
    // 1.已经存在购物车,将数据提取出并添加商品数量
    if (!StringUtils.isEmpty(cartJson)){
        //1.1 将json 转对象 并数据加
        CartItemVo cartItemVo = JSON.parseObject(cartJson, CartItemVo.class);
        cartItemVo.setCount(cartItemVo.getCount() + num);
        //1.2 将更新后的对象转为json 并存入redis
        String jsonString = JSON.toJSONString(cartItemVo);
        ops.put(skuId.toString(),jsonString);
        return cartItemVo;
    }else {
        //不存在 添加新商品
        CartItemVo cartItemVo = new CartItemVo();
        CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
            //2.1 远程查询sku基本信息
            R info = productFeignService.info(skuId);
            SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
            });
            cartItemVo.setCheck(true);
            cartItemVo.setCount(num);
            cartItemVo.setImage(skuInfo.getSkuDefaultImg());
            cartItemVo.setPrice(skuInfo.getPrice());
            cartItemVo.setSkuId(skuId);
            cartItemVo.setTitle(skuInfo.getSkuTitle());
        }, executor);
        //2.2 远程查询sku属性组合信息
        CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
            List<String> attrValuesAsString = productFeignService.getSkuSaleAttrValuesAsString(skuId);
            cartItemVo.setSkuAttrValue(attrValuesAsString);
        }, executor);

        //当前两个异步任务执行完这里才会放行
        try {
            CompletableFuture.allOf(future1,future2).get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //2.3 将该属性封装并存入redis,登录用户使用userId为key
        String jsonString = JSON.toJSONString(cartItemVo);
        ops.put(skuId.toString(),jsonString);
        return cartItemVo;
    }
}

@Override
public CartItemVo getCartItem(Long skuId, Long userId) {
    BoundHashOperations<String,Object,Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX + userId);
    String s = (String) ops.get(skuId.toString());
    CartItemVo cartItemVo = JSON.parseObject(s, CartItemVo.class);
    return cartItemVo;
}

image-20220806143213377

image-20220806152734902

4.查询购物车

需要说的是,这里购物车的总价部分,应该是由前端来计算的,因为前端可以统计用户此时选中的商品。

由于博主对前端几乎是一窍不通,所以只是简单的在后端vo里修改了get方法进行计算。

@GetMapping("/cart.html")
public String cartListPage(HttpSession session,Model model){
    MemberResponseTo memberResponseTo = (MemberResponseTo) session.getAttribute(AuthServerConstant.LOGIN_USER);
    if (memberResponseTo == null){
        //未登录 转到登录页面
        return "redirect:http://gulimall.com/login.html";
    }else {
        //查询购物车
        CartVo cartVo = cartService.getCart(memberResponseTo.getId());
        model.addAttribute("cart",cartVo);
        return "cartList";
    }
}
@Override
public CartVo getCart(Long userId) {
    BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX + userId);
    CartVo cartVo = new CartVo();
    //查询 userId 对应的购物车
    //对购物车的货物进行遍历封装到CartVo
    List<Object> values = ops.values();//list 中每个数据代表一件商品
    if (values != null && values.size() > 0){
        List<CartItemVo> cartItemVos = values.stream().map(obj -> {
            String json = (String) obj;
            return JSON.parseObject(json, CartItemVo.class);
        }).collect(Collectors.toList());
        cartVo.setItems(cartItemVos);
    }
    return cartVo;
}

image-20220806153814883

5.选中商品

@GetMapping("/checkCart")
public String checkCart(@RequestParam("isChecked") Integer isChecked,@RequestParam("skuId") Long skuId,HttpSession session){
    MemberResponseTo memberResponseTo = (MemberResponseTo) session.getAttribute(AuthServerConstant.LOGIN_USER);
    cartService.checkCart(skuId,isChecked,memberResponseTo.getId());
    return "redirect:http://cart.gulimall.com/cart.html";
}
@Override
public void checkCart(Long skuId, Integer isChecked, Long userId) {
    BoundHashOperations<String,Object,Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX + userId);
    String cartJson = (String) ops.get(skuId.toString());
    CartItemVo cartItemVo = JSON.parseObject(cartJson, CartItemVo.class);
    cartItemVo.setCheck(isChecked == 1);
    ops.put(skuId.toString(),JSON.toJSONString(cartItemVo));
}

6.在购物车修改商品数量

@GetMapping("/countItem")
public String changeItemCount(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num, HttpSession session){
    MemberResponseTo memberResponseTo = (MemberResponseTo) session.getAttribute(AuthServerConstant.LOGIN_USER);
    cartService.changeItemCount(skuId,num,memberResponseTo.getId());
    return "redirect:http://cart.gulimall.com/cart.html";
}
@Override
public void changeItemCount(Long skuId, Integer num, Long userId) {
    BoundHashOperations<String,Object,Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX + userId);
    String cartJson = (String) ops.get(skuId.toString());
    CartItemVo cartItemVo = JSON.parseObject(cartJson, CartItemVo.class);
    cartItemVo.setCount(num);
    ops.put(skuId.toString(),JSON.toJSONString(cartItemVo));
}

7.在购物车删除商品

@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId, HttpSession session){
    MemberResponseTo memberResponseTo = (MemberResponseTo) session.getAttribute(AuthServerConstant.LOGIN_USER);
    cartService.deleteItem(skuId,memberResponseTo.getId());
    return "redirect:http://cart.gulimall.com/cart.html";
}
@Override
public void deleteItem(Long skuId, Long userId) {
    BoundHashOperations<String,Object,Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX + userId);
    ops.delete(skuId.toString());
}

十一、消息队列RabbitMQ

RabbitMQ 学习参考:RabbitMQ_HotRabbit.的博客-CSDN博客

1.场景分析

前面的代码中,为了节省时间,使用了异步编排的操作。而现在,通过中间件消息队列,可以快速地相应客户并把待处理的消息放入队列供其他消费者消费。

image-20220810135112237

**应用解耦:**在之前,订单系统会直接调用库存系统的库存接口,但是如果库存系统升级库存接口改变,那么修改订单系统会显得非常麻烦。所以我们在这俩中间加个中间消息队列,将订单系统调用的消息发送给RabbitMQ,库存系统直接订阅消息队列,完成操作。

**流量控制:**在秒杀业务中,后端接收到大量的请求导致资源耗尽,现在可以使用消息队列存储这些消息,慢慢放给后台,保证后台不会宕机。

image-20220810135237252

2.概述

  1. 大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力

  2. 消息服务中两个重要概念: 消息代理(message broker)和目的地(destination) 当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。

  3. 消息队列主要有两种形式的目的地

    1. 队列(queue):点对点消息通信(point-to-point)
    2. 主题(topic):发布(publish)/订阅(subscribe)消息通信
  4. 点对点式:

    • 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获 取消息内容,消息读取后被移出队列

    • 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者

  5. 发布订阅式:

    • 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个 主题,那么就会在消息到达时同时收到消息

  6. JMS(Java Message Service)JAVA消息服务:

    • 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现

  7. AMQP(Advanced Message Queuing Protocol)

    • 高级消息队列协议,也是一个消息代理的规范,兼容JMS

    • RabbitMQ是AMQP的实现

  8. Spring支持

    • spring-jms提供了对JMS的支持 • spring-rabbit提供了对AMQP的支持

    • 需要ConnectionFactory的实现来连接消息代理

    • 提供JmsTemplate、RabbitTemplate来发送消息

    • @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息 代理发布的消息

    • @EnableJms、@EnableRabbit开启支持

  9. Spring Boot自动配置

    • JmsAutoConfiguration

    • RabbitAutoConfiguration

  10. 市面的MQ产品

    • ActiveMQ、RabbitMQ、RocketMQ、Kafka

3.核心概念

RabbitMQ简介: RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。

核心概念:

  • Message

消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成, 这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可 能需要持久性存储)等。

  • Publisher

    消息的生产者,也是一个向交换器发布消息的客户端应用程序。

  • Exchange

    交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。 Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别

  • Queue

    消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直 在队列里面,等待消费者连接到这个队列将其取走。

  • Binding

    绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交 换器理解成一个由绑定构成的路由表。 Exchange 和Queue的绑定可以是多对多的关系。

  • Connection

    网络连接,比如一个TCP连接。

  • Channel

    信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道 发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都 是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。

  • Consumer

    消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

  • Virtual Host

    虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加 密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥 有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时 指定,RabbitMQ 默认的 vhost 是 / 。

  • Broker

    表示消息队列服务器实体

image-20220810140550741

image-20220810141824219

4.docker安装

docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
//设置自启动
docker update rabbitmq --restart=always
  • 4369, 25672 (Erlang发现&集群端口)
  • 5672, 5671 (AMQP端口)
  • 15672 (web管理后台端口)
  • 61613, 61614 (STOMP协议端口)
  • 1883, 8883 (MQTT协议端口

5.RabbitMQ 几种消息模式的测试

参考:RabbitMQ-03(实战 、RabbitMQ 的六种消息模式、SpringBoot整合RabbitMQ)_HotRabbit.的博客-CSDN博客

6.SpringBoot整合RabbitMQ

参考:RabbitMQ-04(SpringBoot整合RabbitMQ,基本使用)_HotRabbit.的博客-CSDN博客

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

HotRabbit.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值