文章目录
十、购物车模块
1.需求分析
用户在登录状态下将商品添加到购物车
- 放入数据库(经常访问、修改不显示)
- 放入 mongodb(Nosql 数据库 比上面的好一点点)
- 放入 redis (采用)
用户在未登录状态下点击购物车会转到登录页面
存储:
- 使用 hash 数据类型来存储商品信息,方便商品的CRUD
用户在购物车中可以
- 添加商品
- 查询购物车
- 修改购买商品的数量
- 删除商品
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;
}
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;
}
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.场景分析
前面的代码中,为了节省时间,使用了异步编排的操作。而现在,通过中间件消息队列,可以快速地相应客户并把待处理的消息放入队列供其他消费者消费。
**应用解耦:**在之前,订单系统会直接调用库存系统的库存接口,但是如果库存系统升级库存接口改变,那么修改订单系统会显得非常麻烦。所以我们在这俩中间加个中间消息队列,将订单系统调用的消息发送给RabbitMQ,库存系统直接订阅消息队列,完成操作。
**流量控制:**在秒杀业务中,后端接收到大量的请求导致资源耗尽,现在可以使用消息队列存储这些消息,慢慢放给后台,保证后台不会宕机。
2.概述
-
大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力
-
消息服务中两个重要概念: 消息代理(message broker)和目的地(destination) 当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
-
消息队列主要有两种形式的目的地
- 队列(queue):点对点消息通信(point-to-point)
- 主题(topic):发布(publish)/订阅(subscribe)消息通信
-
点对点式:
• 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获 取消息内容,消息读取后被移出队列
• 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者
-
发布订阅式:
• 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个 主题,那么就会在消息到达时同时收到消息
-
JMS(Java Message Service)JAVA消息服务:
• 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现
-
AMQP(Advanced Message Queuing Protocol)
• 高级消息队列协议,也是一个消息代理的规范,兼容JMS
• RabbitMQ是AMQP的实现
-
Spring支持
• spring-jms提供了对JMS的支持 • spring-rabbit提供了对AMQP的支持
• 需要ConnectionFactory的实现来连接消息代理
• 提供JmsTemplate、RabbitTemplate来发送消息
• @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息 代理发布的消息
• @EnableJms、@EnableRabbit开启支持
-
Spring Boot自动配置
• JmsAutoConfiguration
• RabbitAutoConfiguration
-
市面的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
表示消息队列服务器实体
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博客