本文重点记录老师讲的话 和 一些配置流程,笔记中有的内容尽量少记录。
-
官方笔记-基础篇:https://blog.csdn.net/hancoder/article/details/107612619
-
官方笔记-高级篇:https://blog.csdn.net/hancoder/article/details/107612746
-
官方笔记-集群篇:https://blog.csdn.net/hancoder/article/details/107612802
-
官方笔记下载地址:https://download.csdn.net/download/hancoder/1273468
目录
7、RabbitListener&RabbitHandler接收消息
因为linux出现问题,时间紧迫没处理,现在各个数据服务器连接不起来 ,只是跟着老师敲,没法展示
九、消息队列
1、MQ简介
- 异步任务
- 应用解耦
- 流量控制
概述:
2、RabbitMQ概念
3、docker安装RabbitMQ
不下载镜像,直接安装。默认会帮你下载
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自动重启rabbitMQ
docker update rabbitmq --restart=always
账号:guest
密码:guest
创建一个交换机
创建队列
交换机绑定队列
删除交换机,先双击点击要删除的交换机,接着
4、SpringBoot整合RabbitMQ
RabbitMQ的使用
1、引入amqp;RabbitAutoConfiguration就会自动生效
2、给容器中自动配置了RabbitTemplate、AmqpAdmin、CachingConnectionFactory、RabbitMessagingTemplate
所有的属性都是
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitProperties
3、给配置文件中配置 spring.rabbitmq 信息
4、@EnableRabbit 开启功能
1)、导入amqp依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2)、添加配置(@ConfigurationProperties(prefix = "spring.rabbitmq"))注意配置前缀一定是spring.rabbitmq
spring.rabbitmq.host=172.20.10.3
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
3)、主启动类添加@EnableRabbit注解()
@EnableRabbit
@SpringBootApplication
public class GulimallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallOrderApplication.class, args);
}
}
5、AmqpAdmin使用
@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {
@Autowired
AmqpAdmin amqpAdmin;
/**
* 1、如何创建Exchange[hello-java-exchange]、Queue、Binding
* 1)、使用AmqpAdmin进行创建
* 2、如何收发消息
*/
@Test
void contextLoads() {
DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
amqpAdmin.declareExchange(directExchange);
log.info("Exchange[{}]创建成功","hello-java-exchange");
}
@Test
public void createQueue(){
Queue queue = new Queue("hello-java-queue",true,false,false);
amqpAdmin.declareQueue(queue);
log.info("Queue[{}]创建成功","hello-java-queue");
}
@Test
public void createBinding(){
Binding binding = new Binding("hello-java-queue", Binding.DestinationType.QUEUE,"hello-java-exchange","hello.java",null);
amqpAdmin.declareBinding(binding);
log.info("Binding[{}]创建成功","hello-java-binding");
}
}
6、RabbitTemplate使用
@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {
@Autowired
AmqpAdmin amqpAdmin;
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 发送消息
*/
@Test
public void sendMessageTest(){
OrderReturnApplyEntity orderReturnApplyEntity = new OrderReturnApplyEntity();
orderReturnApplyEntity.setId(1L);
orderReturnApplyEntity.setCreateTime(new Date());
orderReturnApplyEntity.setReturnName("哈哈哈");
//1、发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable
String msg = "hello word";
//2、配置MyRabbitConfig,让发送的对象类型的消息,可以是一个json
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity);
log.info("消息发送完成{}",orderReturnApplyEntity);
}
}
注意:
配置MyRabbitConfig,让发送的对象类型的消息,可以是一个json
添加“com.atguigu.gulimall.order.config.MyRabbitConfig”类,代码如下:
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
7、RabbitListener&RabbitHandler接收消息
监听消息:使用@RabbitListener; 主启动类必须有@EnableRabbit
@RabbitListener: 类+方法上(监听哪些队列即可)
@RabbitHandler: 标在方法上(重载区分不同的消息)
/**
* queues:声明需要监听的所有队列
*
* org.springframework.amqp.core.Message
* @param message
*
* 参数可以写以下内容
* 1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息类型> OrderReturnApplyEntity content
* 3、Channel channel 当前传输数据的通道
*
* Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
* 场景:
* 1)、订单服务启动多个;同一个消息,只能有一个客户端收到
* 2)、只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
*
*/
添加“com.atguigu.gulimall.order.controller.RabbitController”类,代码如下:
@RestController
public class RabbitController {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendMq")
public String sendMq(@RequestParam(value = "num",defaultValue = "10") Integer num){
for (int i = 0; i < num; i++){
if (i%2==0){
OrderReturnApplyEntity orderReturnApplyEntity = new OrderReturnApplyEntity();
orderReturnApplyEntity.setId(1L);
orderReturnApplyEntity.setCreateTime(new Date());
orderReturnApplyEntity.setReturnName("哈哈哈");
//配置MyRabbitConfig,让发送的对象类型的消息,可以是一个json
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity, new CorrelationData(UUID.randomUUID().toString()));
}else {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",entity, new CorrelationData(UUID.randomUUID().toString()));
}
}
return "OK";
}
}
修改“com.atguigu.gulimall.order.service.impl.OrderItemServiceImpl”类,代码如下:
package com.atguigu.gulimall.order.service.impl;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.entity.OrderReturnApplyEntity;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.order.dao.OrderItemDao;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import com.atguigu.gulimall.order.service.OrderItemService;
import javax.swing.*;
@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderItemEntity> page = this.page(
new Query<OrderItemEntity>().getPage(params),
new QueryWrapper<OrderItemEntity>()
);
return new PageUtils(page);
}
/**
* queues:声明需要监听的所有队列
*
* org.springframework.amqp.core.Message
* @param message
*
* 参数可以写以下内容
* 1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息类型> OrderReturnApplyEntity content
* 3、Channel channel 当前传输数据的通道
*
* Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
* 场景:
* 1)、订单服务启动多个;同一个消息,只能有一个客户端收到
* 2)、只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
*
*/
//@RabbitListener(queues = {"hello-java-queue"})
@RabbitHandler
public void receiverMessage(Message message,OrderReturnApplyEntity content,
Channel channel) throws InterruptedException {
//消息体
byte[] body = message.getBody();
//消息头属性信息
MessageProperties properties = message.getMessageProperties();
System.out.println("接收到消息...内容:" + content);
// Thread.sleep(3000);
System.out.println("消息处理完成=》"+content.getReturnName());
}
@RabbitHandler
public void receiverMessage(OrderEntity orderEntity){
System.out.println("接收到消息...内容:" + orderEntity);
}
}
8、可靠投递-发送端确认
定制RabbitTemplate
服务器收到消息就回调
1、开启发送端确认
1、spring.rabbitmq.publisher-confirms=true
2、设置确认回调
2、消息抵达队列就回调
1、#开启发送端抵达队列确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步发送优先回调我们这个returnConfirm
spring.rabbitmq.template.mandatory=true
2、设置确认回调ReturnCallback
3、消费端确认(保证每个消息被正确消费,此时才可以保证broker删除这个消息)
修改application.properties
#开启发送端确认
spring.rabbitmq.publisher-confirms=true
#开启发送端抵达队列确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步发送优先回调我们这个returnConfirm
spring.rabbitmq.template.mandatory=true
添加“com.atguigu.gulimall.order.config.MyRabbitConfig”类,代码如下:
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @Description: MyRabbitConfig
* @Author: WangTianShun
* @Date: 2020/11/23 21:58
* @Version 1.0
*/
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 定制RabbitTemplate
* 服务器收到消息就回调
* 1、开启发送端确认
* 1、spring.rabbitmq.publisher-confirms=true
* 2、设置确认回调
* 2、消息抵达队列就回调
* 1、#开启发送端抵达队列确认
* spring.rabbitmq.publisher-returns=true
* #只要抵达队列,以异步发送优先回调我们这个returnConfirm
* spring.rabbitmq.template.mandatory=true
* 2、设置确认回调ReturnCallback
* 3、消费端确认(保证每个消息被正确消费,此时才可以保证broker删除这个消息)
*/
@PostConstruct //MyRabbitConfig对象创建完以后,执行这个方法
public void initRabbitTemplate(){
//设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 只要消息抵达Broker就b = true
* @param correlationData 当前消息的唯一关联数据(这个消息的唯一id)
* @param b 消息是否成功收到
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("confirm...correlationData["+correlationData+"]==>b["+b+"]s==>["+s+"]");
}
});
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* @param message 投递失败的消息详细信息
* @param i 回复的状态码
* @param s 回复的文本内容
* @param s1 当时这个消息发给哪个交换机
* @param s2 当时这个消息用哪个路由键
*/
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("Fail Message["+message+"]==>i["+i+"]==>s["+s+"]==>s1["+s1+"]==>s2["+s2+"]");
}
});
}
}
9、可靠投递-消费端确认
消费端确认(保证每个消息被正确消费,此时才可以保证broker删除这个消息)
1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
问题:
我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。发生消息丢失
手动确认模式。只要我们没有明确告诉MQ,货物被签收,没有ACK,消息就一直unacked状态,
即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
2、
1)、#手动确认收货(ack)
spring.rabbitmq.listener.simple.acknowledge-mode=manual
2)、channel.basicAck(deliveryTag,false);签收;业务成功完成就应该签收
channel.basicNack(deliveryTag,false,true);拒签:业务失败,拒签
添加application.properties
#手动确认收货(ack)
spring.rabbitmq.listener.simple.acknowledge-mode=manual
@RabbitHandler
public void receiverMessage(Message message,OrderReturnApplyEntity content,
Channel channel) throws InterruptedException {
//消息体
byte[] body = message.getBody();
//消息头属性信息
MessageProperties properties = message.getMessageProperties();
System.out.println("接收到消息...内容:" + content);
// Thread.sleep(3000);
System.out.println("消息处理完成=》"+content.getReturnName());
//channel内按顺序自增的
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("deliveryTag:"+deliveryTag);
//签收货物,非批量模式
try{
if (deliveryTag % 2 == 0){
//收货
channel.basicAck(deliveryTag,false);
System.out.println("签收了货物。。。"+deliveryTag);
}else {
//退货requeue=false 丢弃 requeue=true发挥服务器,服务器重新入队。
channel.basicNack(deliveryTag,false,true);
System.out.println("没有签收货物..."+deliveryTag);
}
}catch (Exception e){
//网络中断
}
}
十、订单服务
1、页面环境搭建
1)、把静态资源放到虚拟机的nginx里,在/mydata/nginx/html/static/目录先创建order文件夹,在创建detail文件夹,并把静态资源上传到这个文件夹
把index.html放到gulimall-order服务,改名为detail.html
2)、在/mydata/nginx/html/static/order目录下创建list文件夹,并把静态资源上传到这个文件夹
把index.html放到gulimall-order服务,改名为list.html
3)、在/mydata/nginx/html/static/order目录下创建confirm文件夹,并把静态资源上传到这个文件夹
把index.html放到gulimall-order服务,改名为confirm.html
4)、在/mydata/nginx/html/static/order目录下创建pay文件夹,并把静态资源上传到这个文件夹
把index.html放到gulimall-order服务,改名为pay.html
5)、在C:\Windows\System32\drivers\etc\hosts文件里添加域名(把属性只读模式去掉,用记事本打开)
6)、在gulimal-gateway添加路由
- id: gulimall_order_route
uri: lb://gulimall-order
predicates:
- Host=order.gulimall.com
7)、修改每个html的资源访问路径
confirm.html
加上thymeleaf模板空间
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
接下来几个html以此类推进行修改
8)、引入thymeleaf模板引擎
<!--模板引擎 thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
关闭thymeleaf缓存
spring:
thymeleaf:
cache: false
测试
创建“com.atguigu.gulimall.order.web.HelloController”类,代码如下
@RestController
public class HelloController {
@GetMapping("{page}.html")
public String listPage(@PathVariable("page") String page){
return page;
}
}
启动gulimall-order和guliall-gateway
问题:访问http://order.gulimall.com/confirm.html访问失败,报503
原因:gulimall-order服务没有加入到注册中心
解决:1)、pom文件已经导入gulimall-common依赖,说明gulimall-order服务包含注册中心nacos的依赖
2)、在主启动类添加@EnableDiscoveryClient注解
3)、配置应用名和注册中心地址(如果不配置应用名,注册到注册中心不会成功)
spring:
application:
name: gulimall-order
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
confirm.html页面报错,搜素/*把它去掉即可
http://order.gulimall.com/confirm.html
2、整合SpringSession
添加依赖
<!--属性配置的提示工具-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--整合SpringSession完成session共享问题-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--引入redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
修改application.properties
#SpringSession的存储类型
spring.session.store-type=redis
#线程池属性的配置
gulimall.thread.core= 20
gulimall.thread.max-size= 200
gulimall.thread.keep-alive-time= 10
#reidis地址
spring.redis.host=172.20.10.3
添加SpringSession的配置,添加“com.atguigu.gulimall.order.config.GulimallSessionConfig”类,代码如下
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
添加线程池的配置,添加“com.atguigu.gulimall.order.config.MyThreadConfig”类,代码如下
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
return new ThreadPoolExecutor(pool.getCore(),
pool.getMaxSize(),
pool.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
线程池配置需要的属性
添加“com.atguigu.gulimall.order.config.ThreadPoolConfigProperties”类,代码如下
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer core;
private Integer maxSize;
private Integer keepAliveTime;
}
主启动类是上添加SpingSession自动启动的注解
页面调整
修改商城首页我的订单地链接地址
获取用户信息
3、订单登录拦截
1)、订单流程
订单生成 -> 支付订单 -> 卖家发货 -> 确认收货 -> 交易成功
修改“去结算”的链接地址
添加“com.atguigu.gulimall.order.web.OrderWebController”类,代码如下:
@Controller
public class OrderWebController {
@GetMapping("/toTrade")
public String toTrade(){
return "confirm";
}
}
添加登录拦截器类“com.atguigu.gulimall.order.interceptor.LoginUserInterceptor”,代码如下:
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
添加拦截器的配置“com.atguigu.gulimall.order.config.OrderWebConfiguration”类,代码如下:
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
修改gulimall-auth-server的login.html页面
4、订单确认页数据获取
修改“com.atguigu.gulimall.order.web.OrderWebController”类,代码如下:
@Controller
public class OrderWebController {
@Autowired
OrderService orderService;
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo confirmVo = orderService.confirmOrder();
//展示订单确认的数据
model.addAttribute("orderConfirmData",confirmVo);
return "confirm";
}
}
添加“com.atguigu.gulimall.order.vo.OrderConfirmVo”类,代码如下:
//订单确认页需要用的数据
public class OrderConfirmVo {
//收获地址,ums_member_receive_address表
List<MemberAddressVo> address;
//所有选中的购物项
List<OrderItemVo> items;
//发票。。。
//优惠券信息。。。
//积分
Integer integration;
//订单总额
BigDecimal total;
//应付价格
BigDecimal payPrice;
//防重令牌
String orderToken;
//总件数
public Integer getCount(){
Integer i = 0;
if (items != null){
for (OrderItemVo item : items) {
i += item.getCount();
}
}
return i;
}
public List<MemberAddressVo> getAddress() {
return address;
}
public void setAddress(List<MemberAddressVo> address) {
this.address = address;
}
public List<OrderItemVo> getItems() {
return items;
}
public void setItems(List<OrderItemVo> items) {
this.items = items;
}
public Integer getIntegration() {
return integration;
}
public void setIntegration(Integer integration) {
this.integration = integration;
}
public BigDecimal getTotal() {
BigDecimal total = new BigDecimal("0");
if (items != null){
for (OrderItemVo item : items) {
BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
total = total.add(multiply);
}
}
return total;
}
public BigDecimal getPayPrice() {
return getTotal();
}
public String getOrderToken() {
return orderToken;
}
public void setOrderToken(String orderToken) {
this.orderToken = orderToken;
}
}
修改“com.atguigu.gulimall.order.service.OrderService”类,代码如下:
/**
* 订单确认页返回需要用到的数据
* @return
*/
OrderConfirmVo confirmOrder();
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//3、查询用户积分
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
return confirmVo;
}
添加“com.atguigu.gulimall.order.vo.MemberAddressVo”类,代码如下
@Data
public class MemberAddressVo {
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 收货人姓名
*/
private String name;
/**
* 电话
*/
private String phone;
/**
* 邮政编码
*/
private String postCode;
/**
* 省份/直辖市
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址(街道)
*/
private String detailAddress;
/**
* 省市区代码
*/
private String areacode;
/**
* 是否默认
*/
private Integer defaultStatus;
}
添加“com.atguigu.gulimall.order.vo.OrderItemVo”类,代码如下
@Data
public class OrderItemVo {
private Long skuId;
//标题
private String title;
//图片
private String image;
//商品套餐属性
private List<String> skuAttr;
//价格
private BigDecimal price;
//数量
private Integer count;
//总价
private BigDecimal totalPrice;
//TODO 查询库存状态
//是否有货
private boolean hasStock;
//重量
private BigDecimal weight;
}
远程调用要开启fegin客户端
//1、远程查询所有的收货地址列表
添加“com.atguigu.gulimall.order.feign.MemberFeignService”类,代码如下:
@FeignClient("gulimall-member")
public interface MemberFeignService {
@GetMapping("/member/memberreceiveaddress/{memberId}/addresses")
List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
gulimall-member
修改“com.atguigu.gulimall.member.controller.MemberReceiveAddressController”类,代码如下
@GetMapping("/{memberId}/addresses")
public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId){
return memberReceiveAddressService.getAddress(memberId);
}
修改“com.atguigu.gulimall.member.service.MemberReceiveAddressService”类,代码如下:
List<MemberReceiveAddressEntity> getAddress(Long memberId);
修改“com.atguigu.gulimall.member.service.impl.MemberReceiveAddressServiceImpl”类,代码如下:
@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
List<MemberReceiveAddressEntity> memberAddress = this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
return memberAddress;
}
//2、远程查询购物车所有选中的购物项
添加“com.atguigu.gulimall.order.feign.CartFeignService”类,代码如下:
@FeignClient("gulimall-cart")
public interface CartFeignService {
@GetMapping("/currentUserCartItems")
List<OrderItemVo> getCurrentUserCartItems();
}
gulimall-cart
修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下
@GetMapping("/currentUserCartItems")
@ResponseBody
public List<CartItem> getCurrentUserCartItems(){
return cartService.getUserCartItems();
}
修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:
/**
* 获取用户购物车里购物项的所有数据
* @return
*/
List<CartItem> getUserCartItems();
修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下
@Override
public List<CartItem> getUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo.getUserId() == null){
return null;
}else {
String cartKey = CART_PREFIX + userInfoTo.getUserId();
List<CartItem> cartItems = getCartItems(cartKey);
//获取所有被选中的购物项
List<CartItem> collect = cartItems.stream().filter(item -> item.getCheck())
.map(item->{
R price = productFeignService.getPrice(item.getSkuId());
//更新为最新价格
String data = (String) price.get("data");
item.setPrice(new BigDecimal(data));
return item;})
.collect(Collectors.toList());
return collect;
}
}
添加“com.atguigu.gulimall.cart.feign.ProductFeignService”类,代码如下:
@GetMapping("/product/skuinfo/{skuId}/price")
R getPrice(@PathVariable("skuId") Long skuId);
gulimall-product
修改“com.atguigu.gulimall.product.app.SkuInfoController”类,代码如下:
@GetMapping("/{skuId}/price")
public R getPrice(@PathVariable("skuId") Long skuId){
SkuInfoEntity byId = skuInfoService.getById(skuId);
return R.ok().setData(byId.getPrice().toString());
}
5、Feign远程调用丢失请求头问题
feign
远程调用的请求头中没有含有JSESSIONID
的cookie
,所以也就不能得到服务端的session
数据,cart认为没登录,获取不了用户信息- 但是在
feign
的调用过程中,会使用容器中的RequestInterceptor
对RequestTemplate
进行处理,因此我们可以通过向容器中导入定制的RequestInterceptor
为请求加上cookie
。
RequestContextHolder
为SpingMVC*享request
数据的上下文,底层由ThreadLocal
实现。经过RequestInterceptor
处理后的请求如下,已经加上了请求头的Cookie
信息
添加“com.atguigu.gulimall.order.config.GuliFeignConfig”类,代码如下:
package com.atguigu.gulimall.order.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @Description: 请求拦截器
* @Author: WangTianShun
* @Date: 2020/11/26 9:10
* @Version 1.0
*/
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
//1、RequestContextHolder拿到刚进来的请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();//老请求
//同步请求头数据。Cookie
String cookie = request.getHeader("Cookie");
//给新请求同步了老请求的cookie
requestTemplate.header("Cookie",cookie);
System.out.println("feign远程之前先执行RequestInterceptor.apply()");
}
};
}
}
6、Feign异步调用丢失请求头问题
- 查询购物项、库存和收货地址都要调用远程服务,串行会浪费大量时间,因此我们使用
CompletableFuture
进行异步编排
- 由于
RequestContextHolder
使用ThreadLocal
共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie
了。在这种情况下,我们需要在开启异步的时候将老请求的RequestContextHolder
的数据设置进去
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
System.out.println("主线程..."+Thread.currentThread().getId());
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//异步任务编排
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("member线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cart线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多拦截器RequestInterceptor interceptor: requestInterceptors
}, executor);
//3、查询用户积分
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//5、TODO 防重令牌想·
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
7、页面调整
<!--主体部分-->
<p class="p1">填写并核对订单信息</p>
<div class="section">
<!--收货人信息-->
<div class="top-2">
<span>收货人信息</span>
<span>新增收货地址</span>
</div>
<!--地址-->
<div class="top-3" th:each="addr:${orderConfirmData.address}">
<p>[[${addr.name}]]</p><span>[[${addr.name}]] [[${addr.province}]] [[${addr.city}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
</div>
<p class="p2">更多地址︾</p>
<div class="hh1"/></div>
<!--********************************************************************************************-->
<!--谷粒学院自提-->
<div class="top-4">
<p>谷粒学院自提</p>
<p>省运费·无续重·随时取</p>
<p class="xiang">详情</p>
</div>
<!--地址-->
<!--支付方式-->
<h4 class="h4">支付方式</h4>
<div class="top-6">
<p>货到付款</p>
<p><span>惠</span>在线支付</p>
</div>
<div class="hh1"></div>
<!--送货清单-->
<h4 class="h4" style="margin-top: 5px;">送货清单</h4>
<div class="top_1">
<div class="to_left">
<h5><span class="peisong">配送方式</span><span class="dui"><img src="/static/order/confirm/img/i_03.png"/> 对应商品</span></h5>
<div class="box">
谷粒学院快递
</div>
<p class="biao">
<span class="til">标 准 达 :</span>
<span class="con">预计 12月16日[今天] 15:00-19:00 送达</span>
<a href="/static/order/confirm/#">修改</a>
</p>
<div class="updata-1">
<img src="/static/order/confirm/img/im_06.png" />
<span>京准达 标准达</span>
<span style="color: black;"> 配送服务全面升级</span>
</div>
<div class="hh1"></div>
<p class="tui">
<span class="til">退换无忧:</span>
<span class="con">
<input type="checkbox" />
自签收后7天内退货,15天内换<span style="font-size: 12px;margin-left: 5px"> ¥ 0.50</span><br />
<span class="nul">货,</span>可享1次上门取件服务 ﹀
</span>
<div class="updata-2">
<img src="/static/order/confirm/img/im_11.png" />
<span>京准达运费大促(限自营中小件)</span>
</div>
</p>
<p class="kg" style="color:#666666;margin-top: 13px;font-size: 12px">总重量 :<span style="color:#999999;font-size: 12px">0.095kg</span></p>
</div>
<div class="to_right">
<h5>商家:谷粒学院自营</h5>
<div><button>换购</button><span>已购满20.00元,再加49.90元,可返回购物车领取赠品</span></div>
<!--图片-->
<div class="yun1" th:each="item:${orderConfirmData.items}">
<img style="width: 150px;height: 100px;" th:src="${item.image}" class="yun"/>
<div class="mi">
<p>[[${item.title}]] <span style="color: red;"> ¥[[${#numbers.formatDecimal(item.price, 1, 2)}]]</span> <span> x[[${item.count}]]</span> <span>[[${item.hasStock?"有货":"无货"}]]</span></p>
<p><span>0.095kg</span></p>
<p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
</div>
</div>
<div class="hh1"></div>
<p>退换无忧 <span class="money">¥ 0.00</span></p>
</div>
</div>
<div class="bto">
<div class="hh2"></div>
<h4 class="float">发票信息</h4>
<div class="note float"><img src="/static/order/confirm/img/i_11.png" /> <span>开企业抬头发票须填写纳税人识别号,以免影响报销</span></div>
<ul style="clear: both;">
<li>电子普通发票 <img src="/static/order/confirm/img/i_14.png" /></li>
<li>个人</li>
<li>商品明细</li>
<li>
<a href="/static/order/confirm/">修改</a>
</li>
</ul>
<div class="hh3"></div>
<h4 class="clear">使用优惠/礼品卡/抵用 ^</h4>
<ul>
<li class="red">优惠卡</li>
<li>礼品卡</li>
<li>京豆</li>
<li>余额</li>
<li>领奖码</li>
</ul>
<div class="tuijian clear">
<input type="checkbox" />
<span>优惠组合推荐</span>
</div>
</div>
<div class="xia">
<div class="qian">
<p class="qian_y">
<span>[[${orderConfirmData.count}]]</span>
<span>件商品,总商品金额:</span>
<span class="rmb">¥[[${#numbers.formatDecimal(orderConfirmData.total, 1, 2)}]]</span>
</p>
<p class="qian_y">
<span>返现:</span>
<span class="rmb"> -¥0.00</span>
</p>
<p class="qian_y">
<span>运费: </span>
<span class="rmb">   ¥0.00</span>
</p>
<p class="qian_y">
<span>服务费: </span>
<span class="rmb">   ¥0.00</span>
</p>
<p class="qian_y">
<span>退换无忧: </span>
<span class="rmb">   ¥0.00</span>
</p>
</div>
<div class="yfze">
<p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥[[${#numbers.formatDecimal(orderConfirmData.payPrice, 1, 2)}]]</span></p>
<p class="yfze_b">寄送至: 北京 朝阳区 三环到四环之间 朝阳北路复兴国际大厦23层麦田房产 IT-中心研发二部 收货人:赵存权 188****5052</p>
</div>
<button class="tijiao">提交订单</button>
</div>
</div>
http://order.gulimall.com/toTrade
8、订单确认页库存查询
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
System.out.println("主线程..."+Thread.currentThread().getId());
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//异步任务编排
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("member线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cart线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多拦截器RequestInterceptor interceptor: requestInterceptors
}, executor).thenRunAsync(()->{
//查询库存信息
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wmsFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null){
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
},executor);
添加“com.atguigu.gulimall.order.vo.SkuStockVo” 类,代码如下:
@Data
public class SkuStockVo {
private Long skuId;
private Boolean hasStock;
}
修改“com.atguigu.gulimall.order.vo.OrderConfirmVo”类,添加stocks参数,并添加getter,setter方法
修改“com.atguigu.gulimall.order.vo.OrderItemVo”类,去掉hasStock属性
//远程调用库存,查询是否有库存gulimall-ware
添加“com.atguigu.gulimall.order.feign.WmsFeignService”类,代码如下
@FeignClient("gulimall-ware")
public interface WmsFeignService {
//查询sku是否有库存
@PostMapping("/ware/waresku/hasStock")
public R getSkusHasStock(@RequestBody List<Long> skuIds);
}
修改confirm.html页面
<!--图片-->
<div class="yun1" th:each="item:${orderConfirmData.items}">
<img style="width: 150px;height: 100px;" th:src="${item.image}" class="yun"/>
<div class="mi">
<p>[[${item.title}]] <span style="color: red;"> ¥[[${#numbers.formatDecimal(item.price, 1, 2)}]]</span> <span> x[[${item.count}]]</span> <span>[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]</span></p>
<p><span>0.095kg</span></p>
<p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
</div>
</div>
9、订单确认页模拟运费效果
添加“com.atguigu.gulimall.ware.vo.FareVo”类,代码如下
@Data
public class FareVo {
//收货人地址信息
private MemberAddressVo address;
//费用
private BigDecimal fare;
}
修改“com.atguigu.gulimall.ware.controller.WareInfoController”类,代码如下:
@GetMapping("/fare")
public R getFare(@RequestParam("addrId") Long addrId){
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
修改“com.atguigu.gulimall.ware.service.WareInfoService”类,代码如下
/**
* 根据用户的收获地址计算运费
* @param attrId
* @return
*/
FareVo getFare(Long attrId);
修改“com.atguigu.gulimall.ware.service.impl.WareInfoServiceImpl”类,代码如下:
@Override
public FareVo getFare(Long attrId) {
FareVo fareVo = new FareVo();
R r = memberFeignService.addrInfo(attrId);
MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
});
if (data != null){
//模拟计算运费
String phone = data.getPhone();
String substring = phone.substring(phone.length() - 1, phone.length());
BigDecimal bigDecimal = new BigDecimal(substring);
fareVo.setAddress(data);
fareVo.setFare(bigDecimal);
return fareVo;
}
return null;
}
添加“com.atguigu.gulimall.ware.vo.MemberAddressVo”类,代码如下:
@Data
public class MemberAddressVo {
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 收货人姓名
*/
private String name;
/**
* 电话
*/
private String phone;
/**
* 邮政编码
*/
private String postCode;
/**
* 省份/直辖市
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址(街道)
*/
private String detailAddress;
/**
* 省市区代码
*/
private String areacode;
/**
* 是否默认
*/
private Integer defaultStatus;
}
添加“com.atguigu.gulimall.ware.feign.MemberFeignService”类,代码如下:
@FeignClient("gulimall-member")
public interface MemberFeignService {
@RequestMapping("/member/memberreceiveaddress/info/{id}")
R addrInfo(@PathVariable("id") Long id);
}
10、接口幂等性讨论
1)、什么是接口幂等性
2)、哪些情况需要防止
3)、幂等性解决方案
1、token机制
2、各种锁机制
3、各种唯一性约束
4、防重表
5、全球请求唯一id
11、添加防重令牌
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
System.out.println("主线程..."+Thread.currentThread().getId());
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//异步任务编排
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("member线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cart线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多拦截器RequestInterceptor interceptor: requestInterceptors
}, executor).thenRunAsync(()->{
//查询库存信息
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wmsFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null){
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
},executor);
//3、查询用户积分
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//5、TODO 防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberResponseVO.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
12、订单提交
confirm.html
页面提交数据 添加“com.atguigu.gulimall.order.vo.OrderSubmitVo”类,代码如下:
/**
* @Description: 封装订单提交的数据
* @Author: WangTianShun
* @Date: 2020/11/26 22:27
* @Version 1.0
*/
@Data
public class OrderSubmitVo {
// 收获地址的id
private Long addrId;
// 支付方式
private Integer payType;
//无需提交需要购买的商品,去购物车在获取一遍
// 防重令牌
private String orderToken;
// 应付价格 验价
private BigDecimal payPrice;
// 订单备注
private String note;
}
- 提交订单成功,则携带返回数据转发至支付页面
- 提交订单失败,则携带错误信息重定向至确认页
添加“com.atguigu.gulimall.order.web.OrderWebController”类,代码如下:
/**
* 下单功能
* @param submitVo
* @param model
* @param redirectAttributes
* @return
*/
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes redirectAttributes) {
// 下单 去创建订单 验证令牌 核算价格 锁定库存
try {
SubmitOrderResponseVo responseVo = orderService.submitOrder(submitVo);
if (responseVo.getCode() == 0) {
// 下单成功到选择支付方式页面
model.addAttribute("submitOrderResp", responseVo);
return "pay";
} else {
// 订单失败返回到订单确认页面
String msg = "下订单失败: ";
switch (responseVo.getCode()) {
case 1 : msg += "订单信息过期, 请刷新后再次提交."; break;
case 2 : msg += "订单中的商品价格发生变化, 请刷新后再次提交."; break;
case 3 : msg += "库存锁定失败, 商品库存不足."; break;
}
redirectAttributes.addFlashAttribute("msg", msg);
return "redirect:http://order.gulimall.com/toTrade";
}
} catch (Exception e) {
if (e instanceof NoStockException) {
String message = e.getMessage();
redirectAttributes.addFlashAttribute("msg", message);
}
return "redirect:http://order.gulimall.com/toTrade";
}
}
添加“com.atguigu.gulimall.order.service.OrderService”类,代码如下:
/**
* 下单
* @param submitVo
* @return
*/
SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo);
添加“com.atguigu.gulimall.order.vo.SubmitOrderResponseVo”类,代码如下:
@Data
public class SubmitOrderResponseVo {
private OrderEntity order;
private Integer code;
}
1、验证令牌【令牌的对比和删除必须保证原子性】
2、令牌验证成功 下单 去创建订单 验证令牌 核算价格 锁定库存
添加“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
confirmVoThreadLocal.set(submitVo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
response.setCode(0);
// 1、验证令牌【令牌的对比和删除必须保证原子性】
// 0令牌失败 -1删除成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = submitVo.getOrderToken();
// 原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId()), orderToken);
if (result == 0L) {
// 令牌验证失败
response.setCode(1);
return response;
} else {
// 令牌验证成功 下单 去创建订单 验证令牌 核算价格 锁定库存
// 1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
// 2、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = submitVo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) <0.01){
// 金额对比成功
// 3、保持订单
saveOrder(order);
// 4、库存锁定,只要有异常回滚订单数据。订单号,订单项信息(skuId,skuName,num)
WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map(item -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
wareSkuLockVo.setLocks(orderItemVos);
// TODO 远程锁库存
R r = wmsFeignService.orderLockStock(wareSkuLockVo);
if (r.getCode() == 0){
//锁成功了
response.setOrder(order.getOrder());
return response;
}else {
//锁定失败
throw new NoStockException((String) r.get("msg"));
}
}else {
response.setCode(2);
return response;
}
}
// String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId());
// if (orderToken != null && orderToken.equals(redisToken)){
// //令牌验证通过
// redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId());
// }else {
// //不通过
// }
}
订单返回参数 添加“com.atguigu.gulimall.order.to.OrderCreateTo”类,代码如下:
@Data
public class OrderCreateTo {
// 订单
private OrderEntity order;
// 订单项
private List<OrderItemEntity> orderItems;
// 订单应付的价格
private BigDecimal payPrice;
//运费
private BigDecimal fare;
}
1、生成一个订单
/**
* 生成一个订单
* @return
*/
public OrderCreateTo createOrder(){
OrderCreateTo createTo = new OrderCreateTo();
// 1、生成一个订单号
String orderSn = IdWorker.getTimeId();
// 创建订单
OrderEntity orderEntity = buildOrder(orderSn);
createTo.setOrder(orderEntity);
// 2、获取所有的订单项
List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
createTo.setOrderItems(itemEntities);
// 3、计算价格、积分等相关
computePrice(orderEntity,itemEntities);
return createTo;
}
1.1、创建订单
/**
* 创建订单
* @param orderSn
* @return
*/
private OrderEntity buildOrder(String orderSn) {
MemberResponseVO memberResponseVo = LoginUserInterceptor.loginUser.get();
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(orderSn);
orderEntity.setMemberId(memberResponseVo.getId());
OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
// 获取收获地址信息
R r = wmsFeignService.getFare(orderSubmitVo.getAddrId());
FareVo fareResp = r.getData(new TypeReference<FareVo>() {
});
// 设置运费信息
orderEntity.setFreightAmount(fareResp.getFare());
// 设置收货人信息
orderEntity.setReceiverCity(fareResp.getAddress().getCity());
orderEntity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
orderEntity.setReceiverName(fareResp.getAddress().getName());
orderEntity.setReceiverPhone(fareResp.getAddress().getPhone());
orderEntity.setReceiverPostCode(fareResp.getAddress().getPostCode());
orderEntity.setReceiverRegion(fareResp.getAddress().getRegion());
// 设置订单的相关状态信息
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setAutoConfirmDay(7);
return orderEntity;
}
//远程获取收获地址信息(gulimall-ware)
修改“com.atguigu.gulimall.order.feign.WmsFeignService”类,代码如下:
@GetMapping("/ware/wareinfo/fare")
public R getFare(@RequestParam("addrId") Long addrId);
封装远程返回的数据
添加“com.atguigu.gulimall.order.vo.FareVo”类,代码如下:
@Data
public class FareVo {
//收货人地址信息
private MemberAddressVo address;
//费用
private BigDecimal fare;
}
1.2、获取所有的订单项
/**
* 构建所有订单项数据
* @return
*/
private List<OrderItemEntity> buildOrderItems(String orderSn) {
// 最后确定每个购物项的价格
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
if (currentUserCartItems != null && currentUserCartItems.size()>0){
List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
OrderItemEntity itemEntity = buildOrderItem(cartItem);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
return itemEntities;
}
return null;
}
1.2.1、构建某一个订单项
/**
* 构建某一个订单项
* @param cartItem
* @return
*/
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
OrderItemEntity orderItemEntity = new OrderItemEntity();
// 1 订单信息 订单号
// 2 SPU信息
Long skuId = cartItem.getSkuId();
R r = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>(){});
orderItemEntity.setSpuId(data.getId());
orderItemEntity.setSpuBrand(data.getBrandId().toString());
orderItemEntity.setSpuName(data.getSpuName());
orderItemEntity.setCategoryId(data.getCatalogId());
// 3 SKU信息
orderItemEntity.setSkuId(cartItem.getSkuId());
orderItemEntity.setSkuName(cartItem.getTitle());
orderItemEntity.setSkuPic(cartItem.getImage());
orderItemEntity.setSkuPrice(cartItem.getPrice());
String skuAttrs = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";"); //将集合转换成字符串
orderItemEntity.setSkuAttrsVals(skuAttrs);
orderItemEntity.setSkuQuantity(cartItem.getCount());
// 4 优惠信息 [不做]
// 5 积分信息
orderItemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
orderItemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
// 6 订单项的价格信息
orderItemEntity.setPromotionAmount(new BigDecimal("0"));
orderItemEntity.setIntegrationAmount(new BigDecimal("0"));
orderItemEntity.setCouponAmount(new BigDecimal("0"));
// 当前订单项的实际金额
BigDecimal origin = orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity().toString()));
// 总额减去各种优惠后的价格
BigDecimal subtract = origin.subtract(orderItemEntity.getCouponAmount()).subtract(orderItemEntity.getIntegrationAmount()).subtract(orderItemEntity.getPromotionAmount());
orderItemEntity.setRealAmount(subtract);
return orderItemEntity;
}
//远程获取spu信息
封装远程spu的返回信息 添加“com.atguigu.gulimall.order.vo.SpuInfoVo”类,代码如下
@Data
public class SpuInfoVo {
/**
* 商品id
*/
private Long id;
/**
* 商品名称
*/
private String spuName;
/**
* 商品描述
*/
private String spuDescription;
/**
* 所属分类id
*/
private Long catalogId;
/**
* 品牌id
*/
private Long brandId;
/**
*
*/
private BigDecimal weight;
/**
* 上架状态[0 - 下架,1 - 上架]
*/
private Integer publishStatus;
/**
*
*/
private Date createTime;
/**
*
*/
private Date updateTime;
}
添加“com.atguigu.gulimall.order.feign.ProductFeignService”类,代码如下“:
@FeignClient("gulimall-product")
public interface ProductFeignService {
@GetMapping("/product/spuinfo/skuId/{id}")
R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
}
gulimall-product
修改“com.atguigu.gulimall.product.app.SpuInfoController”类,代码如下:
@GetMapping("skuId/{id}")
public R getSpuInfoBySkuId(@PathVariable("id") Long skuId){
SpuInfoEntity entity = spuInfoService.getSpuInfoBySkuId(skuId);
return R.ok().setData(entity);
}
添加“com.atguigu.gulimall.product.service.SpuInfoService”类,代码如下:
SpuInfoEntity getSpuInfoBySkuId(Long skuId);
修改“com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl”类,代码如下:
@Override
public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
SkuInfoEntity byId = skuInfoService.getById(skuId);
Long spuId = byId.getSpuId();
SpuInfoEntity spuInfoEntity = getById(spuId);
return spuInfoEntity;
}
1.3、计算价格、积分等相关
/**
* 计算价格相关
* @param orderEntity
* @param itemEntities
*/
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
BigDecimal total = new BigDecimal("0.0");
BigDecimal coupon = new BigDecimal("0.0");
BigDecimal integration = new BigDecimal("0.0");
BigDecimal promotion = new BigDecimal("0.0");
BigDecimal gift = new BigDecimal("0.0");
BigDecimal growth = new BigDecimal("0.0");
// 订单的总额,叠加每一个订单项的总额信息。
for (OrderItemEntity entity : itemEntities) {
coupon = coupon.add(entity.getCouponAmount());
integration = integration.add(entity.getIntegrationAmount());
promotion = promotion.add(entity.getPromotionAmount());
total = total.add(entity.getRealAmount());
gift = gift.add(new BigDecimal(entity.getGiftIntegration().toString()));
growth = growth.add(new BigDecimal(entity.getGiftGrowth().toString()));
}
// 订单价格相关
orderEntity.setTotalAmount(total);
// 应付金额
orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
orderEntity.setPromotionAmount(promotion);
orderEntity.setIntegrationAmount(integration);
orderEntity.setCouponAmount(coupon);
// 设置积分信息
orderEntity.setIntegration(gift.intValue());
orderEntity.setGrowth(growth.intValue());
// 设置删除状态 0未删除
orderEntity.setDeleteStatus(0);
}
2、保存订单
/**
* 保存订单数据
* @param order
*/
private void saveOrder(OrderCreateTo order) {
OrderEntity orderEntity = order.getOrder();
orderEntity.setModifyTime(new Date());
this.save(orderEntity);
List<OrderItemEntity> orderItems = order.getOrderItems();
orderItemService.saveBatch(orderItems);
}
3、库存锁定,只要有异常回滚订单数据。订单号,订单项信息(skuId,skuName,num)
- 找出所有库存大于商品数的仓库
- 遍历所有满足条件的仓库,逐个尝试锁库存,若锁库存成功则退出遍历
修改“com.atguigu.gulimall.order.feign.WmsFeignService”类,代码如下:
@PostMapping("/ware/waresku/lock/order")
public R orderLockStock(@RequestBody WareSkuLockVo vo);
gulimall-ware
修改“com.atguigu.gulimall.ware.controller.WareSkuController”类,代码如下:
@PostMapping("/lock/order")
public R orderLockStock(@RequestBody WareSkuLockVo vo){
try{
Boolean stock = wareSkuService.orderLockStock(vo);
return R.ok();
}catch (NoStockException e){
return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(),BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());
}
}
添加“com.atguigu.common.exception.BizCodeEnume”类,代码如下:
修改“com.atguigu.gulimall.ware.service.WareSkuService”类,代码如下:
Boolean orderLockStock(WareSkuLockVo vo);
添加“com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl”类,代码如下:
/**
* 为某个订单锁定库存
* (rollbackFor = NoStockException.class)
* 默认只要都是运行异常都会回滚
* @param vo
* @return
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
// 1、按照下单的收货地址,找到一个就近仓库,锁定库存
// 1、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪个仓库有库存
List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
// 2、锁定库存
for (SkuWareHasStock hasStock : collect) {
Boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (wareIds == null || wareIds.size() == 0){
//没有任何库存有这个商品的库存
throw new NoStockException(skuId);
}
for (Long wareId : wareIds) {
//成功返回1;否则就是0
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
if (count == 1){
skuStocked = true;
break;
//当仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false){
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
// 3、肯定全部都是锁定成功的
return true;
}
@Data
class SkuWareHasStock{
private Long skuId;
private Integer num;
private List<Long> wareId;
}
}
这里通过异常机制控制事务回滚,如果在锁定库存失败则抛出NoStockException
s,订单服务和库存服务都会回滚。
修改“com.atguigu.gulimall.ware.dao.WareSkuDao”类,代码如下:
List<Long> listWareIdHasSkuStock(@Param("skuId") Long skuId);
Long lockSkuStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);
<select id="listWareIdHasSkuStock" resultType="java.lang.Long">
select ware_id from wms_ware_sku where sku_id =#{skuId} and stock - stock_locked > 0
</select>
<update id="lockSkuStock">
update wms_ware_sku set stock_locked = stock_locked + #{num}
where sku_id = #{skuId} and ware_id = #{wareId} and stock - stock_locked >= #{num}
</update>
13、分布式事务
本地事务
分布式事务
分布式情况下,可能出现一些服务事务不一致的情况
- 远程服务假失败
- 远程服务执行完成后,下面其他方法出现异常
分布式CAP&BASE理论
1)、cap定理
14、分布式事务常见解决方案
15、Seata
1)、概念
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
2)、Seata术语
- TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
3)、SEATA 的分布式交易解决方案
我们只需要使用一个 @GlobalTransactional
注解在业务方法上:
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
......
}
4)、seata控制分布式事务需要
* Seata控制分布式事务 * 1)、每一个微服务先必须创建undo_logo; * 2)、安装事务协调器;seata-server: https://github.com/seata/seata/releases * 3)、整合 * 1、导入依赖 spring-cloud-starter-alibaba-seata seata-all-1.0.0.jar * 2、解压并启动seata-server * registry.conf注册中心相关的配置,修改registry type=nacos * file.conf * 3、所有想要用到分布式事务的微服务使用seata DatasourceProxy代理自己的数据源 * 4、每个微服务,都必须导入registry.cof * file.conf vgroup_mapping.{application.name}-fescar-service-group = "default" * 5、启动测试 * 6、给分布式大事务的路口标注@GlobalTransactional * 7、每一个远程的小事务用 @Transactional
5)、每个微服务数据库加上undo_log(回滚日志表)
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
因为linux出现问题,时间紧迫没处理,现在各个数据服务器连接不起来 ,只是跟着老师敲,没法展示
6)、环境搭建
从 https://github.com/seata/seata/releases,下载服务器软件包,将其解压缩。
下载senta-server-1.0.0并修改register.conf
,使用nacos作为注册中心(这里根据自己maven版本下载)
修改registry.conf,把nacos作为seata的注册中
添加“com.atguigu.gulimall.order.config.MySeataConfig”类,代码如下:
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
//得到数据源
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())){
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
gulimall-ware
修改“com.atguigu.gulimall.ware.config.WareMybatisConfig”类,代码如下:
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
//得到数据源
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())){
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
分别给gulimall-order和gulimall-ware加上file.conf和registry.conf这两个配置,并修改file.conf
给分布式大事务的路口标注@GlobalTransactional; 每一个远程的小事务用 @Transactional
@GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo)
16、RabbitMQ延时队列(实现定时任务)
添加“com.atguigu.gulimall.order.config.MyMQConfig”类,代码如下:
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 使用JSON序列化机制,进行消息转换
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
// @RabbitListener(queues = "stock.release.stock.queue")
// public void handle(Message message){
//
// }
@Bean
public Exchange stockEventExchange() {
return new TopicExchange("stock-event-exchange", true, false);
}
@Bean
public Queue stockReleaseStockQueue() {
return new Queue("stock.release.stock.queue", true, false, false);
}
@Bean
public Queue stockDelayQueue() {
// String name, boolean durable, boolean exclusive, boolean autoDelete,
// @Nullable Map<String, Object> arguments
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
arguments.put("x-message-ttl", 120000);
return new Queue("stock.delay.queue", true, false, false, arguments);
}
@Bean
public Binding stockReleaseStockBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
new HashMap<>());
}
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
new HashMap<>());
}
}
修改“com.atguigu.gulimall.order.web.HelloController”类代码如下:
@ResponseBody
@GetMapping("/test/createOrder")
public String createOrderTest(){
//订单下单成功
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
orderEntity.setModifyTime(new Date());
//给MQ发送消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
return "ok";
}
17、库存自动解锁
gulimall-ware 服务添加RabbitMQ
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
添加配置
spring.rabbitmq.host=172.20.10.9
spring.rabbitmq.virtual-host=/
主启动类添加注解
@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallWareApplication.class, args);
}
}
添加“com.atguigu.gulimall.ware.config.MyRabbitConfig”类,代码如下:
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 使用JSON序列化机制,进行消息转换
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
@RabbitListener(queues = "stock.release.stock.queue")
public void handle(Message message){
}
@Bean
public Exchange stockEventExchange() {
return new TopicExchange("stock-event-exchange", true, false);
}
@Bean
public Queue stockReleaseStockQueue() {
return new Queue("stock.release.stock.queue", true, false, false);
}
@Bean
public Queue stockDelayQueue() {
// String name, boolean durable, boolean exclusive, boolean autoDelete,
// @Nullable Map<String, Object> arguments
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "order.release");
arguments.put("x-message-ttl", 120000);
return new Queue("stock.delay.queue", true, false, false, arguments);
}
@Bean
public Binding stockReleaseStockBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
new HashMap<>());
}
@Bean
public Binding orderLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
new HashMap<>());
}
}
修改表wms_ware_order_task_detail
修改“com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity”类,代码 如下:
@AllArgsConstructor
@NoArgsConstructor
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {
/**
* id
*/
@TableId
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
/**
* 仓库id
*/
private long wareId;
/**
* 锁定状态
*/
private Integer lockStatus;
}
修改WareOrderTaskDetailDao.xml
<resultMap type="com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity" id="wareOrderTaskDetailMap">
<result property="id" column="id"/>
<result property="skuId" column="sku_id"/>
<result property="skuName" column="sku_name"/>
<result property="skuNum" column="sku_num"/>
<result property="taskId" column="task_id"/>
<result property="wareId" column="ware_id"/>
<result property="lockStatus" column="lock_status"/>
</resultMap>
1)、库存锁定
在库存锁定是添加以下逻辑
- 由于可能订单回滚的情况,所以为了能够得到库存锁定的信息,在锁定时需要记录库存工作单,其中包括订单信息和锁定库存时的信息(仓库id,商品id,锁了几件...)
- 在锁定成功后,向延迟队列发消息,带上库存锁定的相关信息
修改“com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl”类,代码如下:
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo wareSkuLockVo) {
//因为可能出现订单回滚后,库存锁定不回滚的情况,但订单已经回滚,得不到库存锁定信息,因此要有库存工作单
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(wareSkuLockVo.getOrderSn());
taskEntity.setCreateTime(new Date());
wareOrderTaskService.save(taskEntity);
List<OrderItemVo> itemVos = wareSkuLockVo.getLocks();
List<SkuLockVo> lockVos = itemVos.stream().map((item) -> {
SkuLockVo skuLockVo = new SkuLockVo();
skuLockVo.setSkuId(item.getSkuId());
skuLockVo.setNum(item.getCount());
List<Long> wareIds = baseMapper.listWareIdsHasStock(item.getSkuId(), item.getCount());
skuLockVo.setWareIds(wareIds);
return skuLockVo;
}).collect(Collectors.toList());
for (SkuLockVo lockVo : lockVos) {
boolean lock = true;
Long skuId = lockVo.getSkuId();
List<Long> wareIds = lockVo.getWareIds();
if (wareIds == null || wareIds.size() == 0) {
throw new NoStockException(skuId);
}else {
for (Long wareId : wareIds) {
Long count=baseMapper.lockWareSku(skuId, lockVo.getNum(), wareId);
if (count==0){
lock=false;
}else {
//锁定成功,保存工作单详情
WareOrderTaskDetailEntity detailEntity = WareOrderTaskDetailEntity.builder()
.skuId(skuId)
.skuName("")
.skuNum(lockVo.getNum())
.taskId(taskEntity.getId())
.wareId(wareId)
.lockStatus(1).build();
wareOrderTaskDetailService.save(detailEntity);
//发送库存锁定消息至延迟队列
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo detailTo = new StockDetailTo();
BeanUtils.copyProperties(detailEntity,detailTo);
lockedTo.setDetailTo(detailTo);
rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
lock = true;
break;
}
}
}
if (!lock) throw new NoStockException(skuId);
}
return true;
}
2)、监听队列
- 延迟队列会将过期的消息路由至
"stock.release.stock.queue"
,通过监听该队列实现库存的解锁 - 为保证消息的可靠到达,我们使用手动确认消息的模式,在解锁成功后确认消息,若出现异常则重新归队
@Component
@RabbitListener(queues = {"stock.release.stock.queue"})
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo stockLockedTo, Message message, Channel channel) throws IOException {
log.info("************************收到库存解锁的消息********************************");
try {
wareSkuService.unlock(stockLockedTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
3)、库存解锁
- 如果工作单详情不为空,说明该库存锁定成功
- 查询最新的订单状态,如果订单不存在,说明订单提交出现异常回滚,或者订单处于已取消的状态,我们都对已锁定的库存进行解锁
- 如果工作单详情为空,说明库存未锁定,自然无需解锁
- 为保证幂等性,我们分别对订单的状态和工作单的状态都进行了判断,只有当订单过期且工作单显示当前库存处于锁定的状态时,才进行库存的解锁
@Override
public void unlock(StockLockedTo stockLockedTo) {
StockDetailTo detailTo = stockLockedTo.getDetailTo();
WareOrderTaskDetailEntity detailEntity = wareOrderTaskDetailService.getById(detailTo.getId());
//1.如果工作单详情不为空,说明该库存锁定成功
if (detailEntity != null) {
WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(stockLockedTo.getId());
R r = orderFeignService.infoByOrderSn(taskEntity.getOrderSn());
if (r.getCode() == 0) {
OrderTo order = r.getData("order", new TypeReference<OrderTo>() {
});
//没有这个订单||订单状态已经取消 解锁库存
if (order == null||order.getStatus()== OrderStatusEnum.CANCLED.getCode()) {
//为保证幂等性,只有当工作单详情处于被锁定的情况下才进行解锁
if (detailEntity.getLockStatus()== WareTaskStatusEnum.Locked.getCode()){
unlockStock(detailTo.getSkuId(), detailTo.getSkuNum(), detailTo.getWareId(), detailEntity.getId());
}
}
}else {
throw new RuntimeException("远程调用订单服务失败");
}
}else {
//无需解锁
}
}
远程查询订单状态 gulimall-order
添加“com.atguigu.gulimall.ware.feign.OrderFeignService”类,代码如下:
@FeignClient("gulimall-order")
public interface OrderFeignService {
@GetMapping("/order/order/status/{orderSn}")
R getOrderStatus(@PathVariable("orderSn") String orderSn);
}
添加“com.atguigu.gulimall.order.controller.OrderController”类,代码如下:
@GetMapping("/status/{orderSn}")
public R getOrderStatus(@PathVariable("orderSn") String orderSn){
OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
return R.ok().setData(orderEntity);
}
修改“com.atguigu.gulimall.order.service.OrderService”类,代码如下:
OrderEntity getOrderByOrderSn(String orderSn);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Override
public OrderEntity getOrderByOrderSn(String orderSn) {
OrderEntity order_sn = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
return order_sn;
}
修改“com.atguigu.gulimall.ware.dao.WareSkuDao”类,代码如下:
void unlockStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num, @Param("taskDetailId") Long taskDetailId);
<update id="unlockStock">
update wms_ware_sku set stock_locked = stock_locked - #{num}
where sku_id = #{skuId} and ware_id = #{wareId}
</update>
由于gulimall-order添加了拦截器,只要使用该服务必须登录才行。因为这边需要远程调用订单,但不需要登录,所以给这个路径放行
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
if (match){
return true;
}
MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
18、定时关单
1)、提交订单
添加“com.atguigu.gulimall.order.web.OrderWebController”类,代码如下:
/**
* 下单功能
* @param submitVo
* @param model
* @param redirectAttributes
* @return
*/
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes redirectAttributes) {
// 下单 去创建订单 验证令牌 核算价格 锁定库存
try {
SubmitOrderResponseVo responseVo = orderService.submitOrder(submitVo);
System.out.println("============================="+responseVo.getCode());
if (responseVo.getCode() == 0) {
// 下单成功到选择支付方式页面
model.addAttribute("submitOrderResp", responseVo);
return "pay";
} else {
// 订单失败返回到订单确认页面
String msg = "下订单失败: ";
switch (responseVo.getCode()) {
case 1 : msg += "订单信息过期, 请刷新后再次提交."; break;
case 2 : msg += "订单中的商品价格发生变化, 请刷新后再次提交."; break;
case 3 : msg += "库存锁定失败, 商品库存不足."; break;
}
redirectAttributes.addFlashAttribute("msg", msg);
return "redirect:http://order.gulimall.com/toTrade";
}
} catch (Exception e) {
if (e instanceof NoStockException) {
String message = e.getMessage();
redirectAttributes.addFlashAttribute("msg", message);
}
return "redirect:http://order.gulimall.com/toTrade";
}
2)、监听队列
创建订单的消息会进入延迟队列,最终发送至队列order.release.order.queue
,因此我们对该队列进行监听,进行订单的关闭
@Service
@RabbitListener(queues = "order.release.order.queue")
public class OrderCloseListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
try {
orderService.closeOrder(entity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 修改失败 拒绝消息 使消息重新入队
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
3)、关闭订单
- 由于要保证幂等性,因此要查询最新的订单状态判断是否需要关单
- 关闭订单后也需要解锁库存,因此发送消息进行库存、会员服务对应的解锁
添加“com.atguigu.gulimall.order.service.OrderService”类,代码如下:
void closeOrder(OrderEntity entity);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”,代码如下:
@Override
public void closeOrder(OrderEntity entity) {
//查询当前这个订单地最新状态
OrderEntity orderEntity = this.getById(entity.getId());
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
//关单
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
//发给MQ一个
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}
}
添加“com.atguigu.common.to.mq.OrderTo”类,代码如下:
@Data
public class OrderTo {
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 订单号
*/
private String orderSn;
/**
* 使用的优惠券
*/
private Long couponId;
/**
* create_time
*/
private Date createTime;
/**
* 用户名
*/
private String memberUsername;
/**
* 订单总额
*/
private BigDecimal totalAmount;
/**
* 应付总额
*/
private BigDecimal payAmount;
/**
* 运费金额
*/
private BigDecimal freightAmount;
/**
* 促销优化金额(促销价、满减、阶梯价)
*/
private BigDecimal promotionAmount;
/**
* 积分抵扣金额
*/
private BigDecimal integrationAmount;
/**
* 优惠券抵扣金额
*/
private BigDecimal couponAmount;
/**
* 后台调整订单使用的折扣金额
*/
private BigDecimal discountAmount;
/**
* 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
*/
private Integer payType;
/**
* 订单来源[0->PC订单;1->app订单]
*/
private Integer sourceType;
/**
* 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
*/
private Integer status;
/**
* 物流公司(配送方式)
*/
private String deliveryCompany;
/**
* 物流单号
*/
private String deliverySn;
/**
* 自动确认时间(天)
*/
private Integer autoConfirmDay;
/**
* 可以获得的积分
*/
private Integer integration;
/**
* 可以获得的成长值
*/
private Integer growth;
/**
* 发票类型[0->不开发票;1->电子发票;2->纸质发票]
*/
private Integer billType;
/**
* 发票抬头
*/
private String billHeader;
/**
* 发票内容
*/
private String billContent;
/**
* 收票人电话
*/
private String billReceiverPhone;
/**
* 收票人邮箱
*/
private String billReceiverEmail;
/**
* 收货人姓名
*/
private String receiverName;
/**
* 收货人电话
*/
private String receiverPhone;
/**
* 收货人邮编
*/
private String receiverPostCode;
/**
* 省份/直辖市
*/
private String receiverProvince;
/**
* 城市
*/
private String receiverCity;
/**
* 区
*/
private String receiverRegion;
/**
* 详细地址
*/
private String receiverDetailAddress;
/**
* 订单备注
*/
private String note;
/**
* 确认收货状态[0->未确认;1->已确认]
*/
private Integer confirmStatus;
/**
* 删除状态【0->未删除;1->已删除】
*/
private Integer deleteStatus;
/**
* 下单时使用的积分
*/
private Integer useIntegration;
/**
* 支付时间
*/
private Date paymentTime;
/**
* 发货时间
*/
private Date deliveryTime;
/**
* 确认收货时间
*/
private Date receiveTime;
/**
* 评价时间
*/
private Date commentTime;
/**
* 修改时间
*/
private Date modifyTime;
}
4)、解锁库存
修改“com.atguigu.gulimall.ware.listener.StockReleaseListener”类,代码如下:
@Slf4j
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/**
* 1、库存自动解锁
* 库存解锁的场景
* 1)、下订单成功,库存锁定成功,接下来的业务调用失效,导致订单回滚。之前锁定的库存就要自动回滚
* 2)、订单失败
* 锁库存失败
*
* 只要解锁库存的消息失败。一定澳告诉服务解锁失败。
*
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTO to, Message message, Channel channel) throws IOException {
log.info("************************收到库存解锁的消息********************************");
try {
wareSkuService.unLockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
@RabbitHandler
public void handleOrderCloseRelease(OrderTo to, Message message, Channel channel) throws IOException {
log.info("************************订单关闭准备解锁库存********************************");
try {
wareSkuService.unLockStockForOrder(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
修改“com.atguigu.gulimall.ware.service.WareSkuService”类,代码如下:
void unLockStockForOrder(OrderTo to);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
/**
* 防止订单服务卡顿,导致订单状态一直改变不了,库存消息优先到期,查订单状态新建状态,什么都不做就走了
* 导致卡顿的订单,永远不能解锁库存
* @param to
*/
@Transactional
@Override
public void unLockStockForOrder(OrderTo to) {
String orderSn = to.getOrderSn();
//查一下最新的库存解锁状态,防止重复解锁库存
R r = orderFeignService.getOrderStatus(orderSn);
WareOrderTaskEntity task = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
Long id = task.getId();
//按照工作单找到所有 没有解锁的库存,进行解锁
List<WareOrderTaskDetailEntity> entities = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1));
for (WareOrderTaskDetailEntity entity : entities) {
unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
}
}
修改“com.atguigu.gulimall.ware.service.WareOrderTaskService”类,代码如下:
WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn);
修改“com.atguigu.gulimall.ware.service.impl.WareOrderTaskServiceImpl”类,代码如下:
@Override
public WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {
WareOrderTaskEntity orderTaskEntity = this.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
return orderTaskEntity;
}
总结:
19、消息丢失、积压、重复等解决方案
修改“com.atguigu.gulimall.order.config.MyRabbitConfig”类,代码如下:
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 使用JSON序列化机制,进行消息转换
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
@PostConstruct //MyRabbitConfig对象创建完以后,执行这个方法
public void initRabbitTemplate(){
//设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 只要消息抵达Broker就b = true
* @param correlationData 当前消息的唯一关联数据(这个消息的唯一id)
* @param ack 消息是否成功收到
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
/**
*1、做好消息确认机制(publisher,consumer【手动ack】)
* 2、每一个发送的消息都在数据库做好记录。定期将失效的消息再次发送
*/
//服务器收到了
System.out.println("confirm...correlationData["+correlationData+"]==>ack["+ack+"]s==>["+s+"]");
}
});
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* @param message 投递失败的消息详细信息
* @param i 回复的状态码
* @param s 回复的文本内容
* @param s1 当时这个消息发给哪个交换机
* @param s2 当时这个消息用哪个路由键
*/
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
//报错误了。修改数据库当前消息的错误状态-》错误
System.out.println("Fail Message["+message+"]==>i["+i+"]==>s["+s+"]==>s1["+s1+"]==>s2["+s2+"]");
}
});
}
}