04谷粒商城-高级篇四

前言

偏我来时不逢春

11.商城业务-消息队列

11.1MQ简介

主要步骤:

  • 异步处理
  • 应用解耦
  • 流量控制

异步处理

image-20240807213730824

应用解耦

image-20240807213738527

流量控制

image-20240807213758027

11.2RabbitMQ简介

11.2.1概述

image-20240807215047415

消息代理(message broker)

消息代理:指安装了消息中间件的服务器,用于接收消息和发送消息

目的地(destination)

通俗解释:消息代理接收到消息后会将消息继续发给目的地(生产者发送消息)
目的地主要有两种形式:队列、主题

队列(queue)

  • 点对点消息通信(point-to-point)
  • 1.消息发送者发送消息,消息代理将其放入一个队列中,消息接受者从队列中获取消息内容,消息读取后被移出队列
  • 2.队列可以被多个消费者监听,但一条消息只会被一个消费者成功消费

主题(topic)

  • 发布(publish)/订阅(subscribe)消息通信
  • 1.发送者发送消息到主题,多个订阅者订阅该主题,多个消费者会同时收到消息

11.2.3两种规范

JMS(JAVA消息服务)

JMS:(Java Message Service)
JAVA消息服务,基于JVM信息代理的规范。ActiveMQ、HornetMQ是JMS实现

AMQP(高级消息队列协议)

AMQP:(Advanced Message Queuing Protocol)
高级消息队列协议,也是一个消息代理的规范,兼容JMS
RabbitMQ是AMQP的实现

image-20240807215614554

Spring支持与SpringBoot自动装配

image-20240807215728890

11.3RabbitMQ工作流程

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

image-20240807220813036

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-20240807221501490

11.4RabbitMQ安装

官网地址:https://www.rabbitmq.com/networking.html

端口映射:

  • **4369, 25672 (Erlang发现&集群端口) **

  • **5672, 5671 (AMQP端口) **

  • **15672 (web管理后台端口) **

  • **61613, 61614 (STOMP协议端口) **

  • **1883, 8883 (MQTT协议端口) **

11.5Exchange类型

RabbitMQ运行机制

AMQP 中的消息路由

AMQP 中消息的路由过程和 Java 开 发者熟悉的 JMS 存在一些差别, AMQP 中增加了 Exchange 和 Binding 的角色。生产者把消息发布 到 Exchange 上,消息最终到达队列 并被消费者接收,而 Binding 决定交 换器的消息应该发送到那个队列

image-20240808160134659

Exchange 类型

Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、 fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键, headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接 看另外三种类型

Direct Exchange

消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器 就将消息发到对应的队列中。路由键与队 列名完全匹配,如果一个队列绑定到交换 机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发 “dog.puppy”,也不会转发“dog.guard” 等等。它是完全匹配、单播的模式。

image-20240808160314900

Fanout Exchange

每个发到 fanout 类型交换器的消息都 会分到所有绑定的队列上去。fanout 交 换器不处理路由键,只是简单的将队列 绑定到交换器上,每个发送到交换器的 消息都会被转发到与该交换器绑定的所 有队列上。很像子网广播,每台子网内 的主机都获得了一份复制的消息。 fanout 类型转发消息是最快的。

image-20240808160436196

Topic Exchange

topic 交换器通过模式匹配分配消息的 路由键属性,将路由键和某个模式进行 匹配,此时队列需要绑定到一个模式上。 它将路由键和绑定键的字符串切分成单 词,这些单词之间用点隔开。它同样也 会识别两个通配符:符号“#”和符号 “”。#匹配0个或多个单词,匹配一 个单词。

image-20240808160542338

11.6Direct-Exchange

创建队列

pengmall
pengmall.news
pengmall.emps
other.news

image-20240809212612978

创建direct类型交换机,并绑定创建的4个队列,Routing key就是队列名

pengmall.exchange.direct

image-20240809212936508

Routing keypengmall发送消息,发现队列pengmall收到消息

image-20240809213019961

Get Message获取消息,Nack messahe requeue true获取消息但是消息不丢失

image-20240809213139236

可以选择Automatic ack,获取消息自动回复ack

image-20240809213359668

11.7Fanout-Exchange

创建fanout类型交换机,并绑定4个队列,Routing key就是队列名

pengmall.exchange.fanout

发送消息,发现4个队列都收到了消息

image-20240809213950724

指定Routing key4个队列依然可以收到消息

image-20240809214053900

11.8Topic-Exchange

创建topic类型交换机,并绑定4个队列,需要设置4个队列的Routing key

pengmall.exchange.topic
*.news
pengmall.#

image-20240809214534602

pengmall.exchange.topic发送消息的时候设置Routing keypengmall.news,发现匹配4个的队列的Routing key,4个队列都收到了消息

image-20240809214822903

pengmall.exchange.topic发送消息的时候设置Routing keyhello.news,只有other.news队列匹配Routing key

image-20240809214956782

11.9Spring整合Fanout-Exchange

主要步骤:

  • 1.导入依赖
  • 2.RabbitAutoConfiguration生效后自动注入多个容器
    • CachingConnectionFactory
    • RabbitTemplate
    • AmqpAdmin
    • RabbitMessagingTemplate
  • 3.配置application.yaml

导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

image-20240809220301566

配置application.yaml

spring:
  rabbitmq:
    host: 192.168.188.180
    port: 5672
    virtual-host: /
    username: guest
    password: guest

image-20240809220519231

11.10AmqpAdmin使用

主要步骤:

  • 创建交换机
  • 创建队列
  • 创建绑定关系

代码

@Test
public void createExchange(){
    Exchange directExchange = new DirectExchange("hello.exchange.direct", true, false);
    amqpAdmin.declareExchange(directExchange);
    log.info("Exchange[{}]创建成功:","hello.exchange.direct");
}

@Test
public void createQueue() {
    Queue queue = new Queue("hello.queue",true,false,false);
    amqpAdmin.declareQueue(queue);
    log.info("Queue[{}]创建成功:","hello.queue");
}

@Test
public void createBinding() {
    Binding binding = new Binding("hello.queue",
            Binding.DestinationType.QUEUE,
            "hello.exchange.direct",
            "hello",
            null);
    amqpAdmin.declareBinding(binding);
    log.info("Binding[{}]创建成功:","hello.binding");

}

image-20240809222209350

11.11RabbitTemplate使用

主要步骤:

  • 发送字符串消息
  • 发送序列化消息,对象必须实现Serializable接口
  • 发送json消息

发送字符串消息

@Test
public void sendStringMessage() {
    String msg = "Hello World";
    rabbitTemplate.convertAndSend("hello.exchange.direct","hello.queue",
            msg,new CorrelationData(UUID.randomUUID().toString()));
    log.info("消息发送完成:{}",msg);
}

image-20240809223914176

发送序列化消息,对象必须实现Serializable接口

@Test
public void sendEntityMessage() {
    OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
    reasonEntity.setId(1L);
    reasonEntity.setCreateTime(new Date());
    reasonEntity.setName("reason");
    reasonEntity.setStatus(1);
    reasonEntity.setSort(2);
    String msg = "Hello World";
    //1、发送消息,如果发送的消息是个对象,会使用序列化机制,将对象写出去,对象必须实现Serializable接口

    //2、发送的对象类型的消息,可以是一个json
    rabbitTemplate.convertAndSend("hello.exchange.direct","hello", reasonEntity);
    log.info("消息发送完成:{}",reasonEntity);
}

image-20240809224048867

添加rabbitmq配置

@Configuration
public class MyRabbitConfig {

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

image-20240809224321619

再次发送消息,发现消息变成了json字符串

image-20240809224355217

11.12RabbitListener&RabbitHandler接收消息

@RabbitListener

  • 1.用于标注在监听类或监听方法上,接收消息,需要指定监听的队列(数组)
  • 2.使用该注解之前,需要在启动类加上该注解:@EnableRabbit
  • 3.@RabbitListener即可以标注在方法上又可以标注在类上
    • 标注在类上:表示该类是监听类,使得@RabbitHandler注解生效
    • 标注在方法上:表示该方法时监听方法,会监听指定队列获得消息
  • 4.一般只标注在方法上,并配合@RabbitHandler使用,重载的方式接收不同消息对象

测试集群多客户端监听接收消息

  • 1.多个客户端可以共同监听同一队列
  • 2.一条消息同时只能被一个客户端接收
  • 3.同一个客户端接收消息是串行的,revieveMessage方法执行完后才会继续接收下一条消息

@RabbitHandler

  • 1.用于标注在监听方法上,接收消息,不需要指定监听的队列
  • 2.使用该注解之前,需要在启动类加上该注解:@EnableRabbit
  • 3.@RabbitListener只可以标注在方法,重载的方式接收不同消息对象

主要步骤:

  • @RabbitListener接收消息
  • @RabbitListenerMessage
  • 多个服务监听同一个队列,同一个消息只能一个客户端收到,只有一个消息完全处理完,才可以接收下一个消息
  • @RabbitHandler

@RabbitListener接收消息

@RabbitListener(queues = {"hello.queue"})
public void receiveMsg(Object msg) {
    System.out.println("接收到消息,内容:" + msg + ",类型:" + msg.getClass());
}

image-20240809234608974

@RabbitListenerMessage

@RabbitListener(queues = {"hello.queue"})
public void receiveMsg(Message msg, OrderReturnReasonEntity content) {
    byte[] body = msg.getBody();
    MessageProperties properties = msg.getMessageProperties();
    System.out.println("接收到消息,内容:" + msg + ",类型:" + content);
}

image-20240809234301630

在启动一个gulimall-order

image-20240810000524843

OrderItemServiceImpl添加@RabbitListener(queues = {"hello.queue"})

添加2个监听方法,但接收消息的类型不一样

image-20240810000749016

多个服务监听同一个队列,同一个消息只能一个客户端收到,只有一个消息完全处理完,才可以接收下一个消息

image-20240809235123106

@RabbitHandler处理不同类型参数的消息

image-20240810000427602

添加一个发送消息的RabbitMqController方便测试

@RestController
public class RabbitMqController {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMq")
    public String sendMq(@RequestParam(value = "num", defaultValue = "10") Integer num) {

        // for (int i = 0; i < num; i++) {
        //     OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
        //     reasonEntity.setId(1L);
        //     reasonEntity.setCreateTime(new Date());
        //     reasonEntity.setName("hello" + i);
        //     rabbitTemplate.convertAndSend("hello.exchange.direct", "hello", reasonEntity);
        // }

        for (int i = 0; i < num; i++) {
            if (i % 2 == 0) {
                OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
                reasonEntity.setId(1L);
                reasonEntity.setCreateTime(new Date());
                reasonEntity.setName("hello"+i);
                rabbitTemplate.convertAndSend("hello.exchange.direct","hello", reasonEntity);
            }else{
                OrderEntity orderEntity = new OrderEntity();
                orderEntity.setOrderSn(UUID.randomUUID().toString());
                rabbitTemplate.convertAndSend("hello.exchange.direct","hello", orderEntity);
            }
        }

        return "ok";
    }
}

image-20240810000628531

11.13可靠投递-发送端确认

可靠抵达

  • 保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
  • publisher confirmCallback 确认
  • publisher returnCallback 未投递到 queue 退回模式
  • consumer ack机制

主要步骤:

  • 配置rabbitmq
  • 设置确认回调setConfirmCallback
  • 发送消息时设置new CorrelationData(UUID.randomUUID().toString())
  • 设置失败回调setReturnCallback,只要消息没有投递给指定的队列,就触发这个失败回调setReturnCallback

配置rabbitmq

spring:
  rabbitmq:
    host: 192.168.188.180
    port: 5672
    virtual-host: /
    username: guest
    password: guest
    # 开启发送端发送确认,无论是否到达broker都会触发回调【发送端确认机制+本地事务表】
    publisher-confirm-type: correlated
    # 开启发送端抵达队列确认,消息未被队列接收时触发回调【发送端确认机制+本地事务表】
    publisher-returns: true
    # 消息在没有被队列接收时是否强行退回
    template:
        mandatory: true

设置确认回调setConfirmCallback

设置失败回调setReturnCallback

image-20240810011740403

只要消息抵达Broker就ack=true

  • correlationData:当前消息的唯一关联数据(这个是消息的唯一id)
  • ack:消息是否成功收到
  • cause:失败的原因

image-20240810010841814

发送消息时设置new CorrelationData(UUID.randomUUID().toString())

并且设置设置错误的routingKey模拟未投递到 queue 退回模式

image-20240810011120064

11.14可靠投递-消费端确认

主要步骤:

消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)

  • 默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
    • 问题:我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。发生消息丢失;
    • 手动确认模式。只要我们没有明确告诉MQ,货物被签收。没有Ack,消,息就一直是unacked状态。即使consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的consumer连接进来就发给他
  • 如何签收
    • channel.basicAck(deliveryTag, false);:签收;业务成功完成就应该签收
    • channel.basicNack(deliveryTag, false,true);:拒签

配置,手动签收

rabbitmq:
  host: 192.168.188.180
  port: 5672
  virtual-host: /
  username: guest
  password: guest
  # 开启发送端发送确认,无论是否到达broker都会触发回调【发送端确认机制+本地事务表】
  publisher-confirm-type: correlated
  # 开启发送端抵达队列确认,消息未被队列接收时触发回调【发送端确认机制+本地事务表】
  publisher-returns: true
  # 消息在没有被队列接收时是否强行退回
  template:
      mandatory: true
  listener:
    simple:
      acknowledge-mode: manual

image-20240812212055286

代码

@RabbitHandler
public void receiveMsg(Message message,
                       OrderReturnReasonEntity content,
                       Channel channel) throws InterruptedException, IOException {
    System.out.println("接收到消息... " + content);
    byte[] body = message.getBody();
    // 消息头属性信息
    MessageProperties properties = message.getMessageProperties();
    Thread.sleep(3000);
    System.out.println("消息处理完成=>" + content.getName());
    // 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
            System.out.println("没有收了货物..." + deliveryTag);
    } catch (Exception e) {
        // 网络中断
        // 拒签
        channel.basicNack(deliveryTag, false,true);
    }
}

12.商城业务-订单服务

12.1页面环境搭建

主要步骤:

  • 上传等待付款(detail)、订单页(list)、结算页(confirm)、收银页(pay)静态资源到/root/mall/nginx/html/static/order/
  • 把等待付款(detail)、订单页(list)、结算页(confirm)、收银页(pay)的index页面拷贝到订单服务项目gulimall-ordersrc/main/resources/templates目录下
  • 修改nginx配置,这里之前都配置好了,只是检查一下
  • 管理员启动SwitchHosts,添加订单服务gulimall-order的域名域名映射
  • 添加订单服务gulimall-order的网关配置
  • gulimall-order添加thymeleaf的依赖,并配置取消页面缓存
  • 添加页面测试Controller
  • 访问页面测试

上传等待付款(detail)、订单页(list)、结算页(confirm)、收银页(pay)静态资源到/root/mall/nginx/html/static/order/

image-20240812214650096

上传完成后查看一下

image-20240812214738895

把等待付款(detail)、订单页(list)、结算页(confirm)、收银页(pay)的index页面拷贝到订单服务项目gulimall-ordersrc/main/resources/templates目录下

image-20240812222618228

修改nginx配置,这里之前都配置好了,只是检查一下

image-20240812214932937

管理员启动SwitchHosts,添加订单服务gulimall-order的域名域名映射

192.168.188.180     order.gulimall.com

image-20240812215332653

添加订单服务gulimall-order的网关配置,注意不要有空格

image-20240812222704439

gulimall-order添加thymeleaf的依赖,并配置取消页面缓存

image-20240812215759909

添加静态页面thymeleaf命名空间,修改页面静态资源地址

confirm页面

href="
href="/static/order/confirm/

src="
src="/static/order/confirm/

image-20240812220026323

detail页面

href="
href="/static/order/detail/

src="
src="/static/order/detail/

image-20240812220349543

list页面

href="
href="/static/order/list/

src="
src="/static/order/list/

image-20240812220437698

pay页面

href="
href="/static/order/pay/

src="
src="/static/order/pay/

image-20240812220605492

添加页面测试Controller

@Controller
public class IndexController {

    @GetMapping("/{page}.html")
    public String getPage(@PathVariable("page") String page) {
        return page;
    }
}

image-20240812220959267

detail页面的错误,搜索xuanxiangka,然后删除多余"即可

image-20240812222247915

confirm页面的错误,搜索/*,然后删除即可

image-20240812222146763

访问页面测试

image-20240812222854007

12.2整合SpringSession

主要步骤:

  • 导入SpringSession依赖,EnableRedisHttpSession开启EnableRedisHttpSession配置Session
  • application.yaml配置RedisSpringSession、线程池
  • 创建GulimallSessionConfigMyRedisConfigMyThreadConfigThreadPoolConfigProperties配置
  • 修改页面登录信息
  • 测试,登录之后查看页面登录信息展示即可

导入SpringSession依赖,EnableRedisHttpSession开启EnableRedisHttpSession配置Session

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

image-20240812230450252

application.yaml配置RedisSpringSession、线程池

server:
  port: 8203
  servlet:
    session:
      timeout: 30m
spring:
  redis:
    host: 192.168.188.180
    port: 6379
  session:
    store-type: redis
gulimall:
  thread:
    coreSize: 20
    maxSize: 200
    keepAliveTime: 10   

image-20240812230602011

创建GulimallSessionConfigMyRedisConfigMyThreadConfigThreadPoolConfigProperties配置

之前gulimall-product服务都有,拷贝过来即可

image-20240812230621357

修改页面登录信息

confirm.html

image-20240812230713766

detail.html

image-20240812230733229

pay.html

image-20240812230756687

测试,登录之后查看页面登录信息展示即可

image-20240812230844362

12.3订单基本概念

订单中心

电商系统涉及到 3 流,分别时信息流,资金流,物流,而订单系统作为中枢将三者有机的集 合起来。 订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这 些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。

订单构成

image-20240812231841293

订单状态

  • 1.待付款 用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支 付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超 时后将自动取消订单,订单变更关闭状态。
  • 2.已付款/待发货 用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到 WM系统,仓库进行调拨,配货,分拣,出库等操作。
  • 3.待收货/已发货 仓储将商品出库后,订单进入物流环节,订单系5统需要同步物流信息,便于用户实时知悉物 品物流状态
  • 4.已完成 用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态
  • 5.已取消 付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。
  • 6.售后中 用户在付款后申请退款,或商家发货后用户申请退换货。

订单流程

image-20240812232225218

12.4订单登录拦截

主要步骤:

  • 1.购物车结算时跳转confirm.html
  • 2.购物车结算需要登录
    • 添加LoginUserInterceptor拦截器
    • OrderWebConfig使用LoginUserInterceptor拦截器处理所有gulimall-order的请求

gulimall-carttoTrade方法跳转到gulimall-order服务

function toTrade() {
    // window.location.href = "http://cart.gmall.com:8087/toTrade";
    window.location.href = "http://order.gulimall.com/toTrade";
}

image-20240812235819769

OrderWebController处理toTrade请求,跳转到确认页confirm

@Controller
public class OrderWebController {
    /**
     * 去结算确认页
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @GetMapping(value = "/toTrade")
    public String toTrade() {
        return "confirm";
    }
}

image-20240812235918196

添加LoginUserInterceptor拦截器

OrderWebConfig使用LoginUserInterceptor拦截器处理所有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 {

        MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER);
        if (attribute != null) {
            // 把登录后用户的信息放在ThreadLocal里面进行保存
            loginUser.set(attribute);
            return true;
        } else {
            // 未登录,返回登录页面
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.gulimall.com/login.html'</script>");
            return false;
        }   
    }
}

image-20240813000048234

12.5订单确认页模型抽取

主要步骤:

  • 收货地址
  • 所有选中的购物项
  • 发票记录
  • 优惠券信息
  • 订单总额
  • 应付价格
//订单确认页需要用的数据
@Data
public class OrderConfirmVo {
    //// 收货地址,ums member receive address表
    List<MemberAddressVo>address;
    //所有选中的购物项
    List<OrderItemVo> items;
    //发票记录....
    //优惠券信息.
    Integer integration;
    BigDecimal total;//订单总额
    BigDecimal payPrice;//应付价格
}

12.6订单确认页数据获取

主要步骤:

  • 1.远程查询用户的收货地址列表
  • 2.远程查询购物车里所有选中的购物项
    • 获取购物车选中购物项时需要查询最新价格
  • 3.查询用户积分
  • 4.其他数据自动计算
  • 5.防重令牌
@Override
public OrderConfirmVo confirmOrder(){
OrderConfirmVo confirmVo =new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address):
//2、远程查询购物车所有选中的购物项
List<OrderItemVo>items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//3、查询用户积分
Integer integration= memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TOD0 |5、防重令牌
return confirmVo;
}

远程查询用户的收货地址列表
MemberReceiveAddressController

     /**
     * 根据会员id查询会员的所有地址
     * @param memberId
     * @return
     */
    @GetMapping(value = "/{memberId}/address")
    public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
        List<MemberReceiveAddressEntity> addressList = memberReceiveAddressService.getAddress(memberId);
        return addressList;
}

MemberReceiveAddressServiceImpl

@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
    return this.baseMapper.selectList
            (new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
}

image-20240813003300608

远程查询购物车里所有选中的购物项

CartController

/**
 * 获取当前用户的购物车商品项
 * @return
 */
@GetMapping(value = "/currentUserCartItems")
@ResponseBody
public List<CartItemVo> getCurrentCartItems() {
    List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
    return cartItemVoList;
}

CartServiceImpl

@Override
public List<CartItemVo> getUserCartItems() {

    List<CartItemVo> cartItemVoList = new ArrayList<>();
    //获取当前用户登录的信息
    UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
    //如果用户未登录直接返回null
    if (userInfoTo.getUserId() == null) {
        return null;
    } else {
        //获取购物车项
        String cartKey = CART_PREFIX + userInfoTo.getUserId();
        //获取所有的
        List<CartItemVo> cartItems = getCartItems(cartKey);
        if (cartItems == null) {
            throw new CartExceptionHandler();
        }
        //筛选出选中的
        cartItemVoList = cartItems.stream()
                .filter(items -> items.getCheck())
                .map(item -> {
                    //更新为最新的价格(查询数据库)
                    BigDecimal price = productFeignService.getPrice(item.getSkuId());
                    item.setPrice(price);
                    return item;
                })
                .collect(Collectors.toList());
    }

    return cartItemVoList;
}

image-20240813004105739

获取购物车选中购物项时需要查询最新价格

/**
 * 根据skuId查询当前商品的价格
 * @param skuId
 * @return
 */
@GetMapping(value = "/{skuId}/price")
public BigDecimal getPrice(@PathVariable("skuId") Long skuId) {
    //获取当前商品的信息
    SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
    //获取商品的价格
    BigDecimal price = skuInfo.getPrice();
    return price;
}

image-20240813004126000

结算确认页

@Override
public OrderConfirmVo confirmOrder() {
    OrderConfirmVo confirmVo = new OrderConfirmVo();
    MemberResponseVo memberRespVo = LoginUserInterceptor.loginUser.get();
    // 1、远程查询所有的收货地址列表
    List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
    confirmVo.setMemberAddressVos(address);
    // 2、远程查询购物车所有选中的购物项
    List<OrderItemVo> items = cartFeignService.getCurrentCartItems();
    confirmVo.setItems(items);
    // 3、查询用户积分
    Integer integration = memberRespVo.getIntegration();
    confirmVo.setIntegration(integration);
    // 4、其他数据自动计算
    // TOD0 |5、防重令牌
    return confirmVo;
}

12.7Feign远程调用丢失请求头问题

主要步骤:

  • Feign远程调用时需要同步老请求的Cookie等请求头

原因:

  • 浏览器请求时会带上Cookie: GULISESSION
  • 默认使用feign调用时,会根据拦截器构造请求参数RequestTemplate,而此时请求头没有带上Cookie,导致springsession无法获取用户信息

解决:
拦截器构造请求头

image-20240813005709058

@Configuration
public class GuliFeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {

        RequestInterceptor requestInterceptor = new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //1、使用RequestContextHolder拿到刚进来的请求数据
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

                if (requestAttributes != null) {
                    //老请求
                    HttpServletRequest request = requestAttributes.getRequest();

                    if (request != null) {
                        //2、同步请求头的数据(主要是cookie)
                        //把老请求的cookie值放到新请求上来,进行一个同步
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            }
        };
        return requestInterceptor;
    }
}

12.8Feign异步调用丢失请求头问题

主要步骤:

原因:

  • 使用异步编排时,非同一线程无法取到RequestContextHolder(上下文环境保持器)
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();// 获取controller请求对象
空指针异常

解决:

  • 获取主线程ServletRequestAttributes,给每个异步线程复制一份

image-20240813010721908

代码

 /**
     * 获取结算页(confirm.html)VO数据
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo result = new OrderConfirmVo();
        // 获取当前用户
        MemberResponseVo member = LoginUserInterceptor.loginUser.get();

        // 获取当前线程上下文环境器
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
            // 1.查询封装当前用户收货列表
            // 同步上下文环境器,解决异步无法从ThreadLocal获取RequestAttributes
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> address = memberFeignService.getAddress(member.getId());
            result.setMemberAddressVos(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            // 2.查询购物车所有选中的商品
            // 同步上下文环境器,解决异步无法从ThreadLocal获取RequestAttributes
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 请求头应该放入GULIMALLSESSION(feign请求会根据requestInterceptors构建请求头)
            List<OrderItemVo> items = cartFeignService.getCurrentCartItems();
            result.setItems(items);
        }, executor);

        // 3.查询用户积分
        Integer integration = member.getIntegration();// 积分
        result.setIntegration(integration);

        // 4.金额数据自动计算

        // 5.TODO 防重令牌

        // 阻塞等待所有异步任务返回
        CompletableFuture.allOf(addressFuture, cartFuture).get();

        return result;
    }

image-20240813014442045

12.9bug修改

获取价格接口返回对象改为R

SkuInfoController

/**
 * 根据skuId查询当前商品的价格
 * @param skuId
 * @return
 */
@GetMapping(value = "/{skuId}/price")
public R getPrice(@PathVariable("skuId") Long skuId) {
    //获取当前商品的信息
    SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
    //获取商品的价格
   return R.ok().setData(skuInfo.getPrice().toString());
}

image-20240813015909061

CartServiceImpl

R price = productFeignService.getPrice(item.getSkuId());
String data = (String)price.get("data");
item.setPrice(new BigDecimal(data));
return item;

image-20240813015713621

getCurrentCartItems返回json数据,所以加上ResponseBody注解

image-20240813014906157

12.10订单确认页渲染

有货这里可以先固定

image-20240813022716359

12.11订单确认页库存查询

主要步骤:

  • gulimall-order添加远程调用库存接口(之前gulimall-ware写过,这里Feign直接调用即可)
  • gulimall-orderconfirmOrder方法在查询完购物车之后使用thenRunAsync在查询库存
  • confirm.html页面渲染

gulimall-order添加远程调用库存接口(之前gulimall-ware写过,这里Feign直接调用即可)

image-20240813213516838

gulimall-orderconfirmOrder方法在查询完购物车之后使用thenRunAsync在查询库存

.thenRunAsync(() -> {
    // 3.批量查询库存(有货/无货)
    List<Long> skuIds = result.getItems().stream().map(item -> item.getSkuId()).collect(Collectors.toList());
    R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
    List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {
    });

    if (skuStockVos != null && skuStockVos.size() > 0) {
        // 将skuStockVos集合转换为map
        Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
        result.setStocks(skuHasStockMap);
    }
}, executor);

image-20240813213558861

confirm.html页面渲染

image-20240813213706388

界面

image-20240813213731581

12.12订单确认页模拟运费效果

主要步骤:

  • mall_umsums_member_receive_address添加测试地址数据,默认设置一个默认地址
  • gulimall-ware添加获取运费接口
    • 远程调用gulimall-member获取收货地址信息
    • 计算运费,我这里是随机5-15作为运费
  • 前端渲染
    • 运费p标签自定义特性,绑定地址id和默认地址defaultStatus
    • 运费p标签默认选中状态,并在点击时计算运费

mall_umsums_member_receive_address添加测试地址数据,默认设置一个默认地址

INSERT INTO mall_ums.ums_member_receive_address (member_id,name,phone,post_code,province,city,region,detail_address,areacode,default_status) VALUES
	 (2,'peng','15766668888',NULL,NULL,'上海市',NULL,'上海市黄浦区',NULL,1);
INSERT INTO mall_ums.ums_member_receive_address (member_id,name,phone,post_code,province,city,region,detail_address,areacode,default_status) VALUES
	 (2,'peng','15766668888',NULL,NULL,'上海市',NULL,'上海市黄浦区',NULL,1);

image-20240813221056560

gulimall-ware添加获取运费接口

  • 远程调用gulimall-member获取收货地址信息
  • 计算运费,我这里是随机5-15作为运费
@Override
public FareVo getFare(Long addrId) {
    FareVo fareVo = new FareVo();
    //收获地址的详细信息
    R addrInfo = memberFeignService.info(addrId);
    MemberAddressVo memberAddressVo = addrInfo.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {});
    if (memberAddressVo != null) {
        String phone = memberAddressVo.getPhone();
        //截取用户手机号码最后一位作为我们的运费计算
        //1558022051
        // String fare = phone.substring(phone.length() - 10, phone.length()-8);
        // BigDecimal bigDecimal = new BigDecimal(fare);
        //
        // fareVo.setFare(bigDecimal);

        // 随机5-15作为运费
        Random random = new Random();
        // 生成 5 到 15 之间的随机数
        int randomNumber = 5 + random.nextInt(11);
        BigDecimal bigDecimal = new BigDecimal(randomNumber);
        fareVo.setFare(bigDecimal);

        fareVo.setAddress(memberAddressVo);
        return fareVo;
    }
    return null;
}

image-20240813221140652

前端渲染

  • 运费p标签自定义特性,绑定地址id和默认地址defaultStatus
  • 运费p标签默认选中状态,并在点击时计算运费
			<div class="top-3 addr-item" th:each="addr:${confirmOrderData.memberAddressVos}">
				<p th:attr="def=${addr.defaultStatus},addrId=${addr.id}">[[${addr.name}]]</p><span>[[${addr.name}]]  [[${addr.province}]]  [[${addr.city}]] [[${addr.region}]] [[${addr.detailAddress}]]  [[${addr.phone}]]</span>
			</div>

image-20240813221231148

12.13订单确认页细节显示

主要步骤:

  • 接口添加收货地址详细信息
  • 界面加载渲染
  • 接口获取数据后渲染界面

接口添加收货地址详细信息

image-20240813221908595

界面加载渲染

image-20240813221946201

接口获取数据后渲染界面

image-20240813222000127

12.14接口幂等性讨论

什么是幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因 为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结 果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结 果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口 的幂等性。

哪些情况需要防止

用户多次点击按钮

用户页面回退再次提交

微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制

其他业务情况

什么情况下需要幂等

以 SQL 为例,有些操作是天然幂等的。

SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等。

UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。

DELETE from user where userid=1,多次操作,结果一样,具备幂等性

INSERT into user(userid,name) values(1,'a') 如 userid 为唯一主键,即重复操作上面的业务,只 会插入一条用户数据,具备幂等性。

UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。 insert into user(userid,name) values(1,'a') 如 userid 不是主键,可以重复,那上面业务多次操 作,数据都会新增多条,不具备幂等性。

幂等解决方案

token 机制

  • 服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis中。
  • 然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
  • 服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业 务。
  • 如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样 就保证了业务代码,不被重复执行。
  • 我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。
  • Token 获取、比较和删除必须是原子性,可以在 redis 使用 lua 脚本完成。if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

各种锁机制

  • 数据库悲观锁
    • select * from xxxx where id = 1 for update;
    • 悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。 另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会 非常麻烦。
  • 数据库乐观锁
    • 这种方法适合在更新的场景中
    • update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1 根据 version 版本,也就是在操作库存前先获取当前商品的 version 版本号,然后操作的时候 带上此 version号。我们梳理下,我们第一次操作库存时,得到 version 为 1,调用库存服务 version 变成了 2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订 单服务传如的 version 还是 1,再执行上面的 sql 语句时,就不会执行;因为 version 已经变 为 2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。 乐观锁主要使用于处理读多写少的问题

业务层分布式锁

  • 如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数 据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断 这个数据是否被处理过。

各种唯一约束

  • 数据库唯一约束
    • 插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。 我们在数据库层面防止重复。
    • 这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键 的要求不是自增的主键,这样就需要业务生成全局唯一的主键。
    • 如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要 不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
  • redis set 防重
    • 很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的 set, 每次处理数据,先看这个 MD5 是否已经存在,存在就不处理。

防重表

  • 使用订单号 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且 他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避 免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个 事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。 之前说的 redis

全局请求唯一 id

  • 调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。
  • 可以使用 nginx 设置每一个请求的唯一 id; proxy_set_header X-Request-Id $request_id

12.15订单确认页完成

主要步骤:

  • 服务生成防重令牌,并保存在redis中,然后返回给前端,前端下单时带上防重令牌
  • 渲染提交订单的表单,主要包含地址、应付总额、防重令牌
  • 需要在选择地址和计算总额时重新赋值表单数据

服务生成防重令牌,并保存在redis中,然后返回给前端,前端下单时带上防重令牌

String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+member.getId(),token,30, TimeUnit.MINUTES);
result.setOrderToken(token);

image-20240813233858109

渲染提交订单的表单,主要包含地址、应付总额、防重令牌

image-20240813233930037

需要在选择地址和计算总额时重新赋值表单数据

image-20240813234127661

12.16原子验令牌

主要步骤:

  • 使用redis执行lue脚本保证令牌的对比和删除的原子性
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

    // 登录信息
    MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
    // 返回结果
    SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();

    // 1、验证令牌是否合法【令牌的对比和删除必须保证原子性】
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    String orderToken = vo.getOrderToken();

    // 通过lure脚本原子验证令牌和删除令牌
    Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()), orderToken);
    if (result == 0L) {
        // 令牌验证失败
        responseVo.setCode(1);
        return responseVo;
    } else {
        // 令牌验证成功
    }

    return null;
}

image-20240813235431291

12.17构造订单数据

主要步骤:

  • createOrder生成订单,生成唯一订单号
  • builderOrderItems生成订单项
    • 远程调用gulimall-cart获取当前用户购物车
    • 遍历购物车,远程调用gulimall-ware获取收货地址信息和运费信息

createOrder生成订单,生成唯一订单号

远程调用gulimall-cart获取当前用户购物车

image-20240814001834669

遍历购物车,远程调用gulimall-ware获取收货地址信息和运费信息

image-20240814001921772

12.18构造订单项数据

主要步骤:

  • spu信息,远程调用gulimall-product根据skuId获取spu信息
  • sku信息
  • 商品的优惠信息
  • 商品的积分信息
  • 订单项的价格信息
  • 当前订单项的实际金额

image-20240814003622534

12.19订单验价

主要步骤:

  • 每个订单项优惠卷分解金额
  • 每个订单项商品促销分解金额
  • 每个订单项积分优惠分解金额
  • 每个订单项优惠后的分解金额
  • 应付总额 = 总额 + 运费
  • 如果计算后的应付金额和前端传来的应付金额相减取绝对值小于0.01验价成功

遍历订单项,获取每个订单项优惠后的价格

image-20240814013330418

如果计算后的应付金额和前端传来的应付金额相减取绝对值小于0.01验价成功

image-20240814013443392

12.20保存订单数据

主要步骤:

  • 保存订单数据
  • 远程调用gulimall-ware锁定库存

保存订单数据

image-20240814014937441

远程调用gulimall-ware锁定库存

image-20240814015125700

12.21锁定库存

主要步骤:

  • 找到每个商品在哪个仓库都有库存
    • 库存 - 锁定库存 > 0 代表当前仓库有该商品库存
  • 锁定商品库存
    • 库存 - 锁定库存 > 要锁定数量 代表当前仓库可以锁定该商品库存
//2、找到每个商品在哪个仓库都有库存
        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> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIdList);

            return stock;
        }).collect(Collectors.toList());

        //2、锁定库存
        for (SkuWareHasStock hasStock : collect) {
            boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();

            if (org.springframework.util.StringUtils.isEmpty(wareIds)) {
                //没有任何仓库有这个商品的库存
                throw new NotStockException(skuId.toString());
            }

            //1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
            //2、锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所有就不用解锁
            for (Long wareId : wareIds) {
                //锁定成功就返回1,失败就返回0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                if (count == 1) {
                    skuStocked = true;        
                    break;
                } else {
                    //当前仓库锁失败,重试下一个仓库
                }
            }

            if (skuStocked == false) {
                //当前商品所有仓库都没有锁住
                throw new NotStockException(skuId.toString());
            }

查询有商品库存的仓库和锁定库存sql

库存 - 锁定库存 > 0 代表当前仓库有该商品库存

库存 - 锁定库存 > 要锁定数量 代表当前仓库可以锁定该商品库存

image-20240814021611892

12.22提交订单的问题

主要步骤:

  • 创建完订单后复制订单和订单项

  • 锁定库存失败后抛出异常,保证当前方法的事务回滚,锁定库存失败不能创建订单

  • submitOrder方法根据状态码回复前端消息

  • 计算运费接口改为手机号最后一位为运费

  • 订单提交成功,跳到支付界面

创建完订单后复制订单和订单项

image-20240814223424949

锁定库存失败后抛出异常,保证当前方法的事务回滚,锁定库存失败不能创建订单

image-20240814223455522

submitOrder方法根据状态码回复前端消息

image-20240814223608313

order_sn改为char(64)

oms_order

image-20240814223849612

oms_order_item

image-20240814223925851

计算运费接口改为手机号最后一位为运费

image-20240814224615234

订单提交成功,跳到支付界面

image-20240814224809280

posted @ 2024-10-14 00:59  peng_boke  阅读(8)  评论(0编辑  收藏  举报