1、什么是分布式消息队列？消息队列 是指利用 高效可靠 的 消息传递机制 进行与平台无关的 数据交流，并基于 数据通信 来进行分布式系统的集成。通过提供 消息传递 和 消息排队 模型，它可以在 分布式环境 下提供 应用解耦、弹性伸缩、冗余存储、流量削峰、异步通信、数据同步 等等功能，其作为 分布式系统架构 中的一个重要组件，有着举足轻重的地位。2、消息队列--历程与区别2.1、发布历程下图根据时间线展示了不同时间点产生的消息队列产品，主要的产品有：2.2、各个中间件的区别3、中间件--详解3.1、Apache Kafka简介Apache Kafka 是一个 分布式消息发布订阅 系统。它最初由 LinkedIn 公司基于独特的设计实现为一个 分布式的日志提交系统 (a distributed commit log)，之后成为 Apache 项目的一部分。Kafka 性能高效、可扩展良好 并且 可持久化。它的 分区特性，可复制 和 可容错 都是其不错的特性。架构图基本术语Producer：消息生产者。Topic：Topic是个抽象的虚拟概念，一个集群可以有多个Topic，作为一类消息的标识。Partition：Partition是个物理概念，一个Topic对应一个或多个Partition。Replicas：一个Partition有多个Replicas副本。Consumer：消息读取者。消费者订阅主题，并按照一定顺序读取消息。Offset：偏移量是一种元数据，是不断递增的整数。Broker：独立的Kafka服务器。主要特性快速持久化：可以在 O(1) 的系统开销下进行 消息持久化；高吞吐：在一台普通的服务器上既可以达到 10W/s 的 吞吐速率；完全的分布式系统：Broker、Producer 和 Consumer 都原生自动支持 分布式，自动实现 负载均衡；支持 同步 和 异步 复制两种 高可用机制；支持 数据批量发送 和 拉取；零拷贝技术(zero-copy)：减少 IO 操作步骤，提高 系统吞吐量；数据迁移、扩容 对用户透明；无需停机 即可扩展机器；其他特性：丰富的 消息拉取模型、高效 订阅者水平扩展、实时的 消息订阅、亿级的 消息堆积能力、定期删除机制；优点客户端语言丰富：支持 Java、.Net、PHP、Ruby、Python、Go 等多种语言；高性能：单机写入 TPS 约在 100 万条/秒，消息大小 10 个字节；提供 完全分布式架构，并有 replica 机制，拥有较高的 可用性 和 可靠性，理论上支持 消息无限堆积；支持批量操作；消费者 采用 Pull方式 获取消息。消息有序，通过控制 能够保证所有消息被消费且仅被消费一次；有优秀的第三方 Kafka Web 管理界面 Kafka-Manager；在 日志领域 比较成熟，被多家公司和多个开源项目使用。缺点Kafka 单机超过64个 队列/分区 时，Load 时会发生明显的飙高现象。队列 越多，负载 越高，发送消息 响应时间变长；使用 短轮询方式，实时性 取决于 轮询间隔时间；消费失败 不支持重试；支持 消息顺序，但是 一台代理宕机 后，就会产生 消息乱序；社区更新较慢。使用场景日志收集：大量的日志消息先写入kafka，数据服务通过消费kafka消息将数据落地；消息系统：解耦生产者和消费者、缓存消息等；用户活动跟踪：kafka经常被用来记录web用户或者app用户的各种活动运营指标：记录运营、监控数据，包括收集各种分布式应用的数据，生产各种操作的集中反馈。流式处理：比如spark streaming3.2、RabbitMQ简介RabbitMQ 是实现了 高级消息队列协议（AMQP）的开源消息代理软件（亦称面向消息的中间件（英语：Message-oriented middleware））。RabbitMQ服务器是用 Erlang 语言 编写的，而 群集和故障转移 是构建在开放电信平台框架上的。所有主要的编程语言均有与 代理接口通讯 的客户端函式库。架构图基本术语Broker：接收客户端链接实体，实现AMQP消息队列和路由功能；Virtual Host：是一个虚拟概念，权限控制的最小单位。Exchange：接收消息生产者的消息并将消息转发到队列。Message Queue：消息队列，存储为被消费的消息；Message：由Header和Body组成，Header是生产者添加的各种属性；Binding：Binding连接起了Exchange和Message Queue。Connection：在Broker和客户端之间的TCP连接；Channel：信道。Broker和客户端只有tcp连接是不能发送消息的，必须创建信道Command：AMQP命令，客户端通过Command来完成和AMQP服务器的交互。优点基于AMQP协议：除了Qpid，RabbitMQ 是唯一一个 实现了AMQP标准的消息服务器；健壮、稳定、易用；社区活跃，文档完善；支持定时消息；可插入的身份验证，授权，支持TLS和LDAP；支持根据消息标识查询消息，也支持根据消息内容查询消息。缺点erlang 开发源码难懂，不利于做二次开发和维护；接口和协议复杂，学习和维护成本较高。3.3、Apache ActiveMQ简介ActiveMQ 是由 Apache 出品，ActiveMQ 是一个完全支持 JMS 1.1 和 J2EE 1.4 规范的 JMS Provider 实现。它非常快速，支持 多种语言的客户端 和 协议，而且可以非常容易的嵌入到企业的应用环境中，并有许多高级功能。架构图主要特性服从JMS规范：JMS 规范提供了良好的标准和保证，包括：同步 或 异步 的消息分发，一次和仅一次的消息分发，消息接收 和 订阅 等等。连接灵活性：ActiveMQ 提供了广泛的 连接协议，支持的协议有：HTTP/S，IP多播，SSL，TCP，UDP 等等。支持的协议种类多：OpenWire、STOMP、REST、XMPP、AMQP；持久化插件和安全插件：ActiveMQ 提供了 多种持久化 选择。支持的客户端语言种类多：除了 Java 之外，还有：C/C++，.NET，Perl，PHP，Python，Ruby；代理集群：多个 ActiveMQ代理 可以组成一个 集群 来提供服务；异常简单的管理：ActiveMQ 是以开发者思维被设计的。优点跨平台 (JAVA 编写与平台无关，ActiveMQ 几乎可以运行在任何的 JVM 上)；可以用 JDBC：可以将 数据持久化 到数据库。虽然使用 JDBC 会降低 ActiveMQ 的性能，但是数据库一直都是开发人员最熟悉的存储介质；支持 JMS 规范：支持 JMS 规范提供的 统一接口;支持 自动重连 和 错误重试机制；有安全机制：支持基于 shiro，jaas 等多种 安全配置机制，可以对 Queue/Topic 进行 认证和授权；监控完善：拥有完善的 监控，包括 Web Console，JMX，Shell 命令行，Jolokia 的 RESTful API；界面友善：提供的 Web Console 可以满足大部分情况，还有很多 第三方的组件 可以使用，比如 hawtio；缺点社区活跃度不及 RabbitMQ 高；根据其他用户反馈，会出莫名其妙的问题，会 丢失消息；目前重心放到 activemq 6.0 产品 Apollo，对 5.x 的维护较少；不适合用于 上千个队列 的应用场景；3.4、RocketMQ简介RocketMQ 是一个分布式消息和流数据平台，具有低延迟、高性能、高可靠性、万亿级容量和灵活的可扩展性。RocketMQ 是2012年阿里巴巴开源的第三代分布式消息中间件。架构图基本术语Topic：一个Topic可以有0个、1个、多个生产者向其发送消息，一个生产者也可以同时向不同的Topic发送消息。Tag：消息二级类型，可以为用户提供额外的灵活度，一条消息可以没有tag；Producer：消息生产者；Broker：存储消息，以Topic为纬度轻量级的队列；Consumer：消息消费者，负责接收并消费消息；MessageQueue：消息的物理管理单位，一个Topic可以有多个Queue，Queue的引入实现了水平扩展的能力；NameServer：负责对原数据的管理，包括Topic和路由信息，每个NameServer之间是没有通信的；Group：一个组可以订阅多个Topic，ProducerGroup、ConsumerGroup分别是一类生产者和一类消费者；Offset：通过Offset访问存储单元，RocketMQ中所有消息都是持久化的，且存储单元定长。Consumer：支持PUSH和PULL两种消费模式，支持集群消费和广播消费。优点支持发布/订阅（Pub/Sub）和点对点（P2P）消息模型；顺序队列：在一个队列中可靠的先进先出（FIFO）和严格的顺序传递；支持拉（pull）和推（push）两种消息模式；单一队列百万消息的堆积能力；支持多种消息协议，如 JMS、MQTT 等；分布式横向扩展架构满足至少一次消息传递语义；提供丰富的Dashboard，包含配置、指标和监控等。缺点支持的客户端语言不多，目前是java及c++，其中c++不成熟社区活跃度一般延时消息：开源版不支持任意时间精度，仅支持特定的level3.5、Apache Pulsar简介Apache Pulsar 是 Apache 软件基金会顶级项目，是下一代云原生分布式消息流平台，集消息、存储、轻量化函数式计算为一体，采用计算与存储分离架构设计，支持多租户、持久化存储、多机房跨区域数据复制，具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性，被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。Pulsar 是一个 pub-sub (发布-订阅)模型的消息队列系统。架构图基本术语Property：代表租户，每个property都可以代表一个团队、一个功能、一个产品线。Namespace：Pulsar的基本管理单元，在namaspace级别可设置权限、消息TTL、Retention 策略等。Producer：数据生产方，负责创建消息并将消息投递到 Pulsar 中；Consumer：数据消费方，连接到 Pulsar接收消息并进行相应的处理；Broker：无状态Proxy服务，负责接收消息、传递消息、集群负载均衡等操作；BookKeeper：有状态，负责持久化存储消息。ZooKeeper：存储 Pulsar 、 BookKeeper 的元数据，集群配置等信息，负责集群间的协调、服务发现等；Topic：用作从producer到consumer传输消息。Ledger：即Segment，Pulsar底层数据以Ledger的形式存储在BookKeeper上。Fragment ： 每个 Ledger 由若干 Fragment 组成。优点灵活扩容无缝故障恢复支持延时消息内置的复制功能，用于跨地域复制如灾备支持两种消费模型：流（独享模式）、队列（共享模式）缺点暂无3.6、综上所述Kafka 在于 分布式架构，RabbitMQ 基于 AMQP 协议 来实现，RocketMQ 的思路来源于 Kafka，改成了 主从结构，在 事务性 和 可靠性 方面做了优化。广泛来说，电商、金融 等对 事务一致性 要求很高的，可以考虑 RabbitMQ 和 RocketMQ，对 性能要求高 的可考虑 Kafka。4、举例说明由于仅仅是demo，默认安装部署都会，就不再提了。下面只展示 Kafka 和 RocketMQ 的，其他都大同小异。4.1、Kafka Demo在用户添加成功的时候发送消息，将添加的参数作为消息body发送，控制层防止重复提交添加；监听器里接收消息，使用redisson分布式锁防止重复消费，使用ack手动确认，防止消息丢失；将接收到的消息存到刚插入的那行数据中。4.1.1、部分效果控制台信息数据库4.1.2、配置文件pom.xml<dependencies> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.23.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies>application.ymlserver: port: 9200 spring: application: name: llh-contract # 注册到eureka上面的应用名称 # Kafka配置 kafka: bootstrap-servers: 192.168.126.137:9092 # 指定kafka地址，可以多个 # bootstrap-servers: 192.168.126.134:9092,192.168.126.135:9092,192.168.126.136:9092 # 指定kafka地址，可以多个 consumer: enable-auto-commit: false # 关闭自动提交 group-id: test-consumer-group auto-offset-reset: latest # 从当前时间开始接收 auto-commit-interval: 100 # 指定消息key和value的编码方式 key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer producer: # 0表示不进行消息接收是否成功的确认，1表示当Leader接收成功时确认，-1表示Leader和Follower都接收成功时确认 acks: 1 # 重试机制 retries: 1 # 每次批量发送消息的数量 batch-size: 16384 buffer-memory: 3355443 # 指定消息key和value的编码方式 key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.apache.kafka.common.serialization.StringSerializer # redis配置 redis: host: 127.0.0.1 port: 6379 # 数据源 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/activiti?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&nullCatalogMeansCurrent=true username: root password: root # mybatis plus配置 mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志管理 logging: level: root: infoKafkaConfig.javapackage com.llh.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.listener.ContainerProperties; import org.springframework.stereotype.Component; /** * User: lilinhan * DateTime: 2023/11/14 16:26 */ @Component @Configuration public class KafkaConfig { @Bean("ackContainerFactory") public ConcurrentKafkaListenerContainerFactory ackContainerFactory(ConsumerFactory consumerFactory) { ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory(); factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE); factory.setConsumerFactory(consumerFactory); return factory; } }4.1.3、部分代码控制层：UserController.javapackage com.llh.controller; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.json.JSONUtil; import com.llh.domain.User; import com.llh.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.util.concurrent.ListenableFutureCallback; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; /** * User: lilinhan * DateTime: 2023/11/22 16:26 */ @RestController @RequestMapping("/user") public class UserController { @Autowired RedisTemplate redisTemplate; @Autowired UserService userService; @Autowired KafkaTemplate kafkaTemplate; @RequestMapping("/save") public String save(User user){ String jsonStr = JSONUtil.toJsonStr(user); String md5 = DigestUtil.md5Hex(jsonStr); // 重复提交 Boolean b = redisTemplate.opsForValue().setIfAbsent(md5, md5, 1, TimeUnit.MINUTES); if(!b){ return "一分钟禁止重复提交"; } userService.save(user); // 获取自增的id String id = user.getId()+ ""; // 使用kafka发送消息并处理发送结果 // send参数的key和value值必须要一致！！ kafkaTemplate.send("test",id,jsonStr).addCallback(new ListenableFutureCallback() { @Override public void onFailure(Throwable ex) { System.err.println("Kafka----发送失败"); } @Override public void onSuccess(Object result) { System.err.println("Kafka----发送成功："); } }); return "添加成功"; } }监听器：MyListener.javapackage com.llh.listener; import cn.hutool.json.JSON; import cn.hutool.json.JSONUtil; import com.llh.domain.User; import com.llh.service.UserService; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.kafka.support.Acknowledgment; import org.springframework.stereotype.Component; import java.io.Serializable; /** * User: lilinhan * DateTime: 2023/11/22 16:41 */ @Component public class MyListener { @Autowired RedissonClient redissonClient; @Autowired UserService userService; // 使用redisson分布式锁和ack确认消息 @KafkaListener(topics = "test",containerFactory = "ackContainerFactory") public void msg(ConsumerRecord consumerRecord, Acknowledgment ack){ Object key = consumerRecord.key(); // 使用redisson获取锁 RLock lock = redissonClient.getLock((String) key); // 如果成功获取到锁 if(lock.tryLock()){ try { JSON parse = JSONUtil.parse(consumerRecord.value()); System.err.println("收到的消息----"+parse); // 根据自增id查询数据 User userDB = userService.getById((Serializable) key); System.err.println("刚刚添加的数据----"+userDB); userDB.setKafkaMsg(JSONUtil.toJsonStr(parse)); // 把接收到的json字符串存入mysql userService.updateById(userDB); // 手动确认消息 ack.acknowledge(); }catch (Exception e){ throw new RuntimeException(e); }finally { // 释放锁 if(lock!=null&&lock.isHeldByCurrentThread()){ lock.unlock(); } } }else { System.err.println("重复消费！！"); } } }4.2、RocketMQ Demo同步消息、异步消息、延迟消息、同步有序以及队列消息示例4.2.1、部分效果控制台打印可视化界面订阅主题消费者4.2.2、配置文件pom.xml<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.2.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>application.ymlserver: port: 9203 spring: application: name: llh-mq # 注册导eureka上面的应用名称 # mq的配置 rocketmq: name-server: 127.0.0.1:9876 # 服务地址 # 生产者 producer: group: test #生产者组 retry-times-when-send-failed: 2 # 同步发送 retry-times-when-send-async-failed: 2 # 异步发送 # 消费者 consumer: topic: top pull-batch-size: 10 # 拉取数量 group: test4.2.3、部分代码控制层：ProducerController.javapackage com.llh.controller; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * User: lilinhan * DateTime: 2023/10/27 8:48 */ @RestController @RequestMapping("/producer") public class ProducerController { @Autowired RocketMQTemplate rocketMQTemplate; @RequestMapping("/put") public void put(){ // 构建消息对象 Message<String> message = MessageBuilder.withPayload("hello2").build(); SendResult result = rocketMQTemplate.syncSend("sync", message, 500, 4); if(SendStatus.SEND_OK.equals(result.getSendStatus())){ System.err.println("延迟消息发送成功"); } } // 异步发送 @RequestMapping("/async") public void async(){ // 构建消息对象 Message<String> message = MessageBuilder.withPayload("hello").build(); rocketMQTemplate.asyncSend("async", message, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { SendStatus sendStatus = sendResult.getSendStatus(); // 确认消息 if(SendStatus.SEND_OK.equals(sendStatus)){ System.err.println("发送成功！消息----------"+sendStatus.name()); }else { System.err.println("发送成功！消息----------"+sendStatus.name()); } } @Override public void onException(Throwable throwable) { System.out.println("mq连接失败"); } }); } // 同步发送 @RequestMapping("/sync") public void sync(){ SendResult result = rocketMQTemplate.syncSend("sync", "hello1"); if(SendStatus.SEND_OK.equals(result.getSendStatus())){ System.err.println("发送成功"); } rocketMQTemplate.syncSend("sync","hello2"); rocketMQTemplate.syncSend("sync","hello3"); rocketMQTemplate.syncSend("sync","hello4"); System.err.println("发送成功"); } // 同步发送 有序消息 @RequestMapping("/sync2") public void async2(){ rocketMQTemplate.syncSendOrderly("sync2", "订单1创建", "order1"); rocketMQTemplate.syncSendOrderly("sync2", "订单1支付", "order1"); rocketMQTemplate.syncSendOrderly("sync2", "订单1完成", "order1"); rocketMQTemplate.syncSendOrderly("sync2", "订单2创建", "order2"); rocketMQTemplate.syncSendOrderly("sync2", "订单2支付", "order2"); rocketMQTemplate.syncSendOrderly("sync2", "订单2完成", "order2"); } }实现RocketMQListener接口的监听器package com.llh.mq; import org.apache.rocketmq.spring.annotation.ConsumeMode; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; /** * User: lilinhan * DateTime: 2023/10/26 16:58 */ @Component @RocketMQMessageListener(topic = "sync",consumerGroup = "sdsd",consumeMode = ConsumeMode.ORDERLY) public class ConsumerListener implements RocketMQListener<String> { @Override public void onMessage(String s) { System.err.println("接受的消息："+s); } }实现MessageListenerOrderly接口的监听器package com.llh.mq; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import javax.annotation.PostConstruct; import java.util.List; /** * User: lilinhan * DateTime: 2023/10/27 11:26 */ @Component public class TestListener implements MessageListenerOrderly { // 初始化mq @PostConstruct public void init() throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(); consumer.setNamesrvAddr("127.0.0.1:9876"); // nameserv地址 consumer.subscribe("sync2","*"); // 订阅的主题 consumer.setConsumerGroup("dsf"); //消费者的组 consumer.setInstanceName("topname"); consumer.registerMessageListener(this); // 监听处理的对象 consumer.start(); } @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) { // 判断消息是否为空 if(CollectionUtils.isEmpty(list)){ return ConsumeOrderlyStatus.SUCCESS; } // 遍历消息队列 for (MessageExt messageExt : list) { int queueId = messageExt.getQueueId(); String body = new String(messageExt.getBody()); System.err.println("这个队列："+queueId+"-----消息："+body+"-------"+Thread.currentThread().getId()+":"+Thread.currentThread().getName()); } // 返回成功 return ConsumeOrderlyStatus.SUCCESS; } }
车辆模拟支付、出租、抢单等功能实现
1、部分效果演示车辆列表租车详情订单列表jmeter压力测试抢单错误信息持久化到redis控制台信息2、技术栈功能或特点名称版本父项目依赖spring-boot-starter-parent2.2.5.RELEASE微服务架构spring-cloud-dependenciesHoxton.RELEASEadmin监控服务spring-boot-admin-starter-server2.2.4eureka管理服务spring-cloud-starter-netflix-eureka-server2.2.0.RELEASEconfig配置服务spring-cloud-config-server2.2.0.RELEASEgateway网关服务spring-cloud-starter-gateway2.2.0.RELEASE链路追踪服务spring-cloud-starter-zipkin2.2.0.RELEASEredis限流spring-boot-starter-data-redis-reactive2.2.5.RELEASE组件调用(已集成熔断,负载均衡)spring-cloud-starter-openfeign2.2.0.RELEASEhutool工具类hutool-all5.8.22数据库curdmybatis-plus-boot-starter3.5.3.2短信榛子云短信zhenzisms2.0.2email邮件发送spring-boot-starter-mail2.2.5.RELEASEmq消息队列rocketmq-spring-boot-starter2.2.1阿里云oos对象储存aliyun-sdk-oss3.15.1支付宝支付alipay-sdk-java4.38.111.ALL分布式图片管理minio8.2.23、后端集成3.2、用户登录UserController.javapackage com.llh.controller; import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.CircleCaptcha; import cn.hutool.core.lang.UUID; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.json.JSONUtil; import cn.hutool.jwt.JWTUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.llh.domain.User; import com.llh.service.UserService; import com.llh.utils.ZzyUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; /** * User: lilinhan * DateTime: 2023/10/20 11:57 */ @RestController @RequestMapping("/user") public class UserController { @Autowired UserService userService; @Autowired RedisTemplate redisTemplate; @Autowired JavaMailSender javaMailSender; @RequestMapping("/test") public String test(){ return "hhhhhh"; } // 验证码接口 @RequestMapping("/getCode") public void getCode(HttpServletResponse response) throws IOException { CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(150, 60, 4, 20); // 重新生成code captcha.createCode(); captcha.write(response.getOutputStream()); redisTemplate.opsForValue().set("yzm", captcha.getCode(), 1, TimeUnit.MINUTES); } @RequestMapping("/sendSms") public Map<String,Object> sendSms(String phone){ Map<String,Object> map = new HashMap<>(); boolean tel = phone.matches("(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}"); if (!tel) { map.put("code", 1005); map.put("msg", "手机号格式错误"); return map; } String code = RandomUtil.randomNumbers(4); int time = 5; boolean b = ZzyUtils.sendSms(phone, code, time); if (!b) { map.put("code", 1005); map.put("msg", "短信发送失败"); return map; } else { redisTemplate.opsForValue().set(phone, code, time, TimeUnit.MINUTES); map.put("code", 1001); map.put("msg", "短信发送成功"); return map; } } @RequestMapping("/login") public Map<String,Object> login(User user){ Map<String,Object> map = new HashMap<>(); QueryWrapper wrapper = new QueryWrapper(); wrapper.eq("name",user.getName()); User userDB = userService.getOne(wrapper); if(userDB==null){ map.put("code",1005); map.put("msg","该用户不存在"); return map; } String md5Hex = DigestUtil.md5Hex(user.getPwd() + userDB.getSalt()); if(!userDB.getPwd().equals(md5Hex)){ map.put("code",1005); map.put("msg","密码错误"); return map; } String code = (String) redisTemplate.opsForValue().get("yzm"); if(StrUtil.isNotEmpty(code)){ if (!code.equalsIgnoreCase(user.getCode())) { map.put("code", 1005); map.put("msg", "验证码错误"); return map; } }else { map.put("code", 1005); map.put("msg", "验证码已过期"); return map; } Map<String, Object> map1 = new HashMap<>(); map1.put("user", JSONUtil.toJsonStr(userDB)); map1.put("loginName", userDB.getName()); map1.put("userId", userDB.getId()); String token = JWTUtil.createToken(map1, "llh".getBytes()); map.put("token", token); map.put("loginName", userDB.getName()); map.put("userId", userDB.getId()); map.put("code", 1001); map.put("msg", "登录成功！"); return map; } @RequestMapping("/phoneLogin") public Map<String,Object> phoneLogin(User user){ Map<String,Object> map = new HashMap<>(); QueryWrapper wrapper = new QueryWrapper(); wrapper.eq("name",user.getName()); User userDB = userService.getOne(wrapper); if(userDB==null){ map.put("code",1005); map.put("msg","该用户不存在"); return map; } String md5Hex = DigestUtil.md5Hex(user.getPwd() + userDB.getSalt()); if(!userDB.getPwd().equals(md5Hex)){ map.put("code",1005); map.put("msg","密码错误"); return map; } String code = (String) redisTemplate.opsForValue().get(user.getPhone()); if(StrUtil.isNotEmpty(code)){ if (!code.equals(user.getCode())) { map.put("code", 1005); map.put("msg", "验证码错误"); return map; } }else { map.put("code", 1005); map.put("msg", "验证码已过期"); return map; } Map<String, Object> map1 = new HashMap<>(); map1.put("user", JSONUtil.toJsonStr(userDB)); map1.put("loginName", userDB.getName()); map1.put("userId", userDB.getId()); String token = JWTUtil.createToken(map1, "llh".getBytes()); map.put("token", token); map.put("loginName", userDB.getName()); map.put("userId", userDB.getId()); map.put("code", 1001); map.put("msg", "登录成功！"); return map; } // 注册接口 @RequestMapping("/register") public Map<String, Object> register(User user) { Map<String, Object> map = new HashMap<>(); boolean qq = user.getEmail().matches("[1-9][0-9]{4,10}"); // boolean email = user.getEmail().matches("^[A-Za-z0-9]+([._%+-][A-Za-z0-9]+)*@[A-Za-z0-9]+(\\\\.[A-Za-z0-9]+)*(\\\\.[A-Za-z]{2,})$"); if(!qq){ map.put("code", 1005); map.put("msg", "QQ号格式错误"); return map; } boolean phone2 = user.getPhone().matches("(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}"); if (!phone2) { map.put("code", 1005); map.put("msg", "手机号格式错误"); return map; } QueryWrapper wrapper = new QueryWrapper(); wrapper.eq("phone",user.getPhone()); User userDB = userService.getOne(wrapper); if(userDB!=null){ map.put("code", 1005); map.put("msg", "手机号已存在"); return map; } if(StrUtil.isNotBlank(user.getCode())){ String code = (String) redisTemplate.opsForValue().get("yzm"); if (!code.equalsIgnoreCase(user.getCode())) { map.put("code", 1005); map.put("msg", "验证码错误"); return map; } }else { map.put("code", 1005); map.put("msg", "验证码已过期"); return map; } // 发送短信 // sendSms(user.getPhone()); String initPwd = "123"; String initName = "张三"; String salt = UUID.randomUUID().toString().substring(0, 4); // 邮件发送 SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setFrom("llh.fool@qq.com"); mailMessage.setTo(user.getEmail()+"@qq.com"); mailMessage.setSentDate(new Date()); mailMessage.setSubject("注册成功提示"); mailMessage.setText("恭喜："+user.getEmail()+"注册成功!\n初始用户名："+initName+"，初始密码为："+initPwd); javaMailSender.send(mailMessage); user.setPwd(DigestUtil.md5Hex(initPwd + salt)); user.setName(initName); user.setSalt(salt); boolean save = userService.save(user); if (save) { map.put("code", 1001); map.put("msg", "注册成功"); return map; } return map; } }3.2、车辆基本crud车辆curd，CarController.javapackage com.llh.controller; import cn.hutool.core.lang.Snowflake; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.llh.domain.Car; import com.llh.service.CarService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * User: lilinhan * DateTime: 2023/10/30 18:39 */ @RestController @RequestMapping("/car") public class CarController { @Autowired CarService carService; // 更新车辆 @RequestMapping("/update") public Map<String,Object> update(Car car){ Map<String,Object> map = new HashMap<>(); boolean b = carService.updateById(car); if(b){ map.put("flag",b); map.put("msg","修改成功"); return map; }else { map.put("flag",b); map.put("msg","修改失败"); return map; } } // 车辆下架 @RequestMapping("/del") public Map<String,Object> del(Integer id){ Map<String,Object> map = new HashMap<>(); boolean b = carService.removeById(id); if(b){ map.put("flag",b); map.put("msg","删除成功"); return map; }else { map.put("flag",b); map.put("msg","删除失败"); return map; } } // 通过订单编号获取车辆信息 @RequestMapping("/updateCarByNo") public void updateCarByNo(String no){ carService.updateCarByNo(no); } // 车辆列表+查询 @RequestMapping("/list") public Page<Car> list(Page<Car> page, Car car){ return carService.getList(page,car); } // 车辆录入 @RequestMapping("/save") public Map<String,Object> save(Car car){ Map<String,Object> map = new HashMap<>(); // 编号使用雪花算法 car.setNum(new Snowflake().nextIdStr()); car.setCreateTime(new Date()); car.setSpeed("手自一体"); car.setRent(new BigDecimal(2000)); boolean save = carService.save(car); map.put("flag",save); map.put("msg","添加成功"); return map; } }下拉框数据，DistController.javapackage com.llh.controller; import com.llh.domain.Dist; import com.llh.service.DistService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * User: lilinhan * DateTime: 2023/10/30 18:42 */ @RestController @RequestMapping("/dist") public class DistController { @Autowired DistService distService; @GetMapping("/list") public Map<String, List<Dist>> list(){ Map<String,List<Dist>> map = new HashMap<>(); List<Dist> list = distService.list(); // 获取车系、车型、品牌和颜色下拉框 map.put("models",list.stream().filter(dist -> dist.getDistType().equals("car_model")).collect(Collectors.toList())); map.put("brands",list.stream().filter(dist -> dist.getDistType().equals("car_brand")).collect(Collectors.toList())); map.put("dists",list.stream().filter(dist -> dist.getDistType().equals("car_dist")).collect(Collectors.toList())); map.put("colors",list.stream().filter(dist -> dist.getDistType().equals("car_color")).collect(Collectors.toList())); return map; } }3.3、图片上传使用minio上传，UploadController.javapackage com.llh.controller; import io.minio.MinioClient; import io.minio.PutObjectArgs; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * User: lilinhan * DateTime: 2023/10/24 10:34 */ @RestController @RequestMapping("/upload") public class UploadController { public static final String MINIO_SERVER = "http://127.0.0.1:9000"; private static final String BUCKET_NAME = "images"; @RequestMapping("/minio") public Map<String, Object> minio(MultipartFile file) { Map<String, Object> map = new HashMap<>(); // 创建客户端 MinioClient minioClient = MinioClient.builder() .endpoint(MINIO_SERVER) // 访问url .credentials("EGQVyIa2qbjWAKZzGZ8T","SqEHCH3elRayyazcTXESAsrozAWTOmtrIX7SssSB") // 密钥 .build(); String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename(); try { // 使用自带方法 putObject minioClient.putObject(PutObjectArgs.builder() .bucket(BUCKET_NAME) // 桶的名称 .object(fileName) // 文件名称 .contentType(file.getContentType()) // 文件类型 .stream(file.getInputStream(), file.getSize(), -1) // 文件流传输 .build()); map.put("code",1001); map.put("msg","上传成功"); map.put("url",MINIO_SERVER+"/"+BUCKET_NAME+"/"+fileName); return map; } catch (Exception e) { throw new RuntimeException(e); } } }使用阿里云oss对象储存，AlyOssUtils.javapackage com.llh.utils; import cn.hutool.core.lang.UUID; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.model.CannedAccessControlList; import org.springframework.web.multipart.MultipartFile; import java.util.HashMap; import java.util.Map; /** * User: lilinhan * DateTime: 2023/11/2 10:07 */ public class AlyOssUtils { private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com";// oss实例的地域节点 private static final String ACCESS_KEY_ID = "LTAI5t8M6dewZALaTRb3QEC7";// 阿里云密钥ID private static final String ACCESS_KEY_SECRET = "9iG7iCRwATM0cVB8kPxmPSTbhqotqk";// 阿里云密钥 private static final String BUCKET_NAME = "llh-images";// oss实例的桶名 /** * 图片上传（包含文件夹） * @param file 文件 * @param model 阿里云的文件夹名 * @return map集合 */ public static Map<String,Object> uploadToModel(MultipartFile file,String model){ Map<String,Object> map = new HashMap<>(); String url = ""; try { // 创建ossClient实例 OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID,ACCESS_KEY_SECRET); // 判断oss实例是否存在 if(!ossClient.doesBucketExist(BUCKET_NAME)){ // 创建桶名 ossClient.createBucket(BUCKET_NAME); // 设置oss实例的访问权限：公共读 ossClient.setBucketAcl(BUCKET_NAME, CannedAccessControlList.PublicRead); } // 获取文件名 String filename = UUID.randomUUID() + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")); // 文件根路径 String key = model+"/"+filename; // 将文件上传到阿里云 ossClient.putObject(BUCKET_NAME,key,file.getInputStream()); // 获取url地址 url = "https://"+BUCKET_NAME+"."+ENDPOINT.substring(ENDPOINT.lastIndexOf("//")+2)+"/"+key; // 关闭客户端 ossClient.shutdown(); map.put("code",1001); map.put("url",url); map.put("msg","上传成功"); return map; } catch (Exception e) { throw new RuntimeException(e); } } /** * 图片上传 * @param file 上传的文件 * @return map集合 */ public static Map<String,Object> upload(MultipartFile file){ Map<String,Object> map = new HashMap<>(); String url = ""; try { // 创建ossClient实例 OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID,ACCESS_KEY_SECRET); // 判断oss实例是否存在 if(!ossClient.doesBucketExist(BUCKET_NAME)){ // 创建桶名 ossClient.createBucket(BUCKET_NAME); // 设置oss实例的访问权限：公共读 ossClient.setBucketAcl(BUCKET_NAME, CannedAccessControlList.PublicRead); } // 获取文件名 String filename = UUID.randomUUID() + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")); // 将文件上传到阿里云 ossClient.putObject(BUCKET_NAME,filename,file.getInputStream()); // 获取url地址 url = "https://"+BUCKET_NAME+"."+ENDPOINT.substring(ENDPOINT.lastIndexOf("//")+2)+"/"+filename; // 关闭客户端 ossClient.shutdown(); map.put("code",1001); map.put("url",url); map.put("msg","上传成功"); return map; } catch (Exception e) { throw new RuntimeException(e); } } }3.4、出租功能订单的curd，OrderController.javapackage com.llh.controller; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Snowflake; import cn.hutool.crypto.digest.DigestUtil; import com.llh.domain.Order; import com.llh.service.CarService; import com.llh.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * User: lilinhan * DateTime: 2023/10/31 15:25 */ @RestController @RequestMapping("/order") public class OrderController { @Autowired OrderService orderService; @Resource CarService carService; @Autowired RedisTemplate redisTemplate; // 通过编号更新订单 @RequestMapping("/updateOrderByNo") public void updateOrderByNo(String no){ orderService.updateOrderByNo(no); } // 生成订单 @RequestMapping("/save") public Map<String,Object> save(Order order){ Map<String,Object> map = new HashMap<>(); String md5Hex = DigestUtil.md5Hex(order + ""); Boolean b = redisTemplate.opsForValue().setIfAbsent(md5Hex, md5Hex, 1, TimeUnit.MINUTES); if(!b){ map.put("flag",false); map.put("msg","一分钟禁止重复提交"); return map; } // 订单编号 String no = new Snowflake().nextIdStr(); // 计算租期 long days = DateUtil.betweenDay(order.getCarStart(), order.getCarEnd(), false); // 租金的费用 order.setCarPrice(order.getCarRent().multiply(new BigDecimal(days))); order.setNum(no); order.setCreateTime(new Date()); // 录入订单 boolean save = orderService.save(order); // 修改车辆表状态 carService.updateCarByNo(order.getCarNo()); map.put("flag",save); map.put("msg","生成订单成功"); return map; } // 订单列表 @RequestMapping("/list") public List<Order> list(){ return orderService.list(); } }3.5、支付功能PayController.javapackage com.llh.controller; import cn.hutool.extra.servlet.ServletUtil; import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayClient; import com.alipay.api.AlipayConfig; import com.alipay.api.DefaultAlipayClient; import com.alipay.api.domain.AlipayTradePagePayModel; import com.alipay.api.domain.AlipayTradeQueryModel; import com.alipay.api.domain.AlipayTradeRefundModel; import com.alipay.api.internal.util.AlipaySignature; import com.alipay.api.request.AlipayTradePagePayRequest; import com.alipay.api.request.AlipayTradeQueryRequest; import com.alipay.api.request.AlipayTradeRefundRequest; import com.alipay.api.response.AlipayTradePagePayResponse; import com.alipay.api.response.AlipayTradeQueryResponse; import com.alipay.api.response.AlipayTradeRefundResponse; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.llh.domain.Order; import com.llh.service.OrderService; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.List; import java.util.Map; /** * User: lilinhan * DateTime: 2023/10/31 18:51 */ @RestController @RequestMapping("/pay") public class PayController { String alipayPublicKey = "[binary]"; @Autowired OrderService orderService; @Autowired RocketMQTemplate rocketMQTemplate; @Autowired HttpServletResponse servletResponse; @Autowired HttpServletRequest request; // 前往支付 @RequestMapping("/goPay") public void goPay(String no) throws AlipayApiException, IOException { QueryWrapper<Order> wrapper = new QueryWrapper<>(); wrapper.eq("num", no); Order orderDB = orderService.getOne(wrapper); Message<String> message = MessageBuilder.withPayload(no).build(); // 异步发送mq rocketMQTemplate.asyncSend("car2", message, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) { System.err.println("发送成功"); } } @Override public void onException(Throwable e) { System.err.println("连接失败"); } }); String privateKey = "[binary]"; String alipayPublicKey = "[binary]"; AlipayConfig alipayConfig = new AlipayConfig(); alipayConfig.setServerUrl("https://openapi-sandbox.dl.alipaydev.com/gateway.do"); alipayConfig.setAppId("9021000130615233"); alipayConfig.setPrivateKey(privateKey); alipayConfig.setFormat("json"); alipayConfig.setAlipayPublicKey(alipayPublicKey); alipayConfig.setCharset("UTF-8"); alipayConfig.setSignType("RSA2"); AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig); AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); request.setReturnUrl("http://127.0.0.1:9999/car-api/pay/callback"); AlipayTradePagePayModel model = new AlipayTradePagePayModel(); model.setOutTradeNo(orderDB.getNum()); model.setTotalAmount(orderDB.getCarPrice() + ""); model.setSubject(orderDB.getCarNo()); model.setProductCode("FAST_INSTANT_TRADE_PAY"); request.setBizModel(model); AlipayTradePagePayResponse response = alipayClient.pageExecute(request); String body = response.getBody(); if (response.isSuccess()) { System.err.println("调用成功"); servletResponse.setContentType("text/html;charset=utf8"); PrintWriter writer = servletResponse.getWriter(); writer.write(body); writer.flush(); writer.close(); } else { System.err.println("调用失败"); } } // 回调方法 @RequestMapping("/callback") public void callback(String out_trade_no) throws IOException, AlipayApiException { QueryWrapper<Order> wrapper = new QueryWrapper<>(); wrapper.eq("num", out_trade_no); Order orderDB = orderService.getOne(wrapper); orderDB.setStat(1); orderDB.setRemark("已支付"); orderService.updateById(orderDB); // 使用hutool把参数转换为map集合 Map<String, String> params = ServletUtil.getParamMap(request); // 验证签名 boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayPublicKey, "UTF-8", "RSA2"); //调用SDK验证签名 if (signVerified) { System.err.println("回滚的订单编号：" + out_trade_no); servletResponse.sendRedirect("http://127.0.0.1:8080/order"); } } // 退款 @RequestMapping("/refund") public Map<String,Object> refund(String no) throws AlipayApiException { Map<String,Object> map = new HashMap<>(); QueryWrapper<Order> wrapper = new QueryWrapper<>(); wrapper.eq("num", no); Order orderDB = orderService.getOne(wrapper); String privateKey = "[binary]"; String alipayPublicKey = "[binary]"; AlipayConfig alipayConfig = new AlipayConfig(); alipayConfig.setServerUrl("https://openapi-sandbox.dl.alipaydev.com/gateway.do"); alipayConfig.setAppId("9021000130615233"); alipayConfig.setPrivateKey(privateKey); alipayConfig.setFormat("json"); alipayConfig.setAlipayPublicKey(alipayPublicKey); alipayConfig.setCharset("UTF-8"); alipayConfig.setSignType("RSA2"); AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig); AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); AlipayTradeRefundModel model = new AlipayTradeRefundModel(); model.setRefundAmount(orderDB.getCarPrice()+""); model.setOutTradeNo(orderDB.getNum()); request.setBizModel(model); AlipayTradeRefundResponse response = alipayClient.execute(request); System.out.println(response.getBody()); if (response.isSuccess()) { System.out.println("调用成功"); orderDB.setStat(2); orderDB.setRemark("已退款"); boolean b = orderService.updateById(orderDB); map.put("flag",b); map.put("msg","退款成功"); return map; } else { map.put("flag",false); map.put("msg","调用失败"); return map; } } // 核对订单 @RequestMapping("/check") public Map<String,Object> check() throws AlipayApiException { Map<String,Object> map = new HashMap<>(); List<Order> list = orderService.list(); for (Order order : list) { String no = order.getNum(); if(order.getStat()!=2){ String privateKey = "[binary]"; String alipayPublicKey = "[binary]"; AlipayConfig alipayConfig = new AlipayConfig(); alipayConfig.setServerUrl("https://openapi-sandbox.dl.alipaydev.com/gateway.do"); alipayConfig.setAppId("9021000130615233"); alipayConfig.setPrivateKey(privateKey); alipayConfig.setFormat("json"); alipayConfig.setAlipayPublicKey(alipayPublicKey); alipayConfig.setCharset("UTF-8"); alipayConfig.setSignType("RSA2"); AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig); AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); AlipayTradeQueryModel model = new AlipayTradeQueryModel(); model.setOutTradeNo(no); request.setBizModel(model); AlipayTradeQueryResponse response = alipayClient.execute(request); System.out.println(response.getBody()); if (response.isSuccess()) { // 已支付，状态码未改 System.out.println("调用成功"); if(order.getStat()==1){ order.setRemark("已支付"); }else { order.setRemark("已支付，状态码未改"); } } else { // 未支付，状态码已改 System.out.println("调用失败"); if(order.getStat()==0){ }else { order.setRemark("未支付，状态码已改"); } } } } boolean b = orderService.updateBatchById(list); if(b){ map.put("flag",b); map.put("msg","校验成功"); return map; }else { map.put("flag",false); map.put("msg","校验失败"); return map; } } }mq消费者，MyListener.javapackage com.llh.listener; import cn.hutool.core.collection.CollUtil; import com.llh.service.OrderService; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.List; /** * User: lilinhan * DateTime: 2023/10/31 19:16 */ @Component public class MyListener implements MessageListenerOrderly { @Autowired RedisTemplate redisTemplate; @Autowired OrderService orderService; // 初始化mq @PostConstruct public void init() throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(); consumer.setNamesrvAddr("127.0.0.1:9876"); consumer.subscribe("car2", "*"); consumer.setConsumerGroup("dsf"); consumer.setInstanceName("name"); consumer.registerMessageListener(this); consumer.start(); } // 接收的有序消息队列 @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { if(CollUtil.isNotEmpty(msgs)){ for (MessageExt msg : msgs) { String body = new String(msg.getBody()); System.err.println("接受的消息--------"+body); return ConsumeOrderlyStatus.SUCCESS; } }else { System.err.println("消息为空"); } return ConsumeOrderlyStatus.SUCCESS; } }3.6、模拟抢单配置类，MybatisPlusConfig.javapackage com.llh.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; /** * User: lilinhan * DateTime: 2023/10/13 11:53 */ @Configuration @MapperScan(value = {"com.llh.mapper"}) public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 分页拦截器 mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return mybatisPlusInterceptor; } @Bean public ThreadPoolTaskExecutor threadPoolTaskExecutor(){ ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(5);// 核心线程数量 taskExecutor.setMaxPoolSize(10);// 最大线程数量 taskExecutor.setKeepAliveSeconds(2);// 设置时长（秒） taskExecutor.setQueueCapacity(10);// 设置队列容量 taskExecutor.setThreadNamePrefix("llh-thread:");// 线程名前缀 return taskExecutor; } }ThreadController.javapackage com.llh.controller; import cn.hutool.core.lang.Snowflake; import com.llh.domain.Takeaway; import com.llh.service.TakeawayService; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.util.*; /** * User: lilinhan * DateTime: 2023/11/2 14:04 */ @RestController @RequestMapping("/thread") public class ThreadController { @Autowired TakeawayService takeawayService; @Autowired RedisTemplate redisTemplate; @Autowired ThreadPoolTaskExecutor threadPoolTaskExecutor; @Autowired RedissonClient redissonClient; private int count = 1; // 生成订单 @RequestMapping("/test") public boolean test(){ for (int i = 0; i < 3; i++) { threadPoolTaskExecutor.execute(new Runnable() { @Override public void run() { List<Takeaway> list = new ArrayList<>(); for (int i1 = 0; i1 < 5; i1++) { System.out.println("当前线程-----"+Thread.currentThread().getId()+":"+Thread.currentThread().getName()); // 生成实体 Takeaway takeaway = new Takeaway(); takeaway.setPrice(new BigDecimal(20.22)); takeaway.setFroms(new Snowflake().nextIdStr()); takeaway.setTos(new Snowflake().nextIdStr()); takeaway.setCreated(new Date()); list.add(takeaway); } takeawayService.saveBatch(list); } }); } return true; } // 测试抢单 @RequestMapping("/size") public Map<String,Object> size(String no){ Map<String,Object> map = new HashMap<>(); System.err.println("当前用户："+Thread.currentThread().getId()+"，正在抢购商品："+no); // Boolean b1 = redisTemplate.opsForValue().setIfAbsent(no, no, 2, TimeUnit.SECONDS); RLock lock = redissonClient.getLock(no); if(lock.tryLock()){ try { // 通过id查询订单 Takeaway takeaway = takeawayService.getById(no); takeaway.setStat(1);// 已抢 Thread.sleep(3000); boolean b = takeawayService.updateById(takeaway); if(takeaway.getStat()==1){ System.err.println("此商品已被抢购"); } if(b){ System.err.println("恭喜用户："+Thread.currentThread().getId()+"，@@@@@@成功抢购到商品："+no); } } catch (Exception e) { throw new RuntimeException(e); }finally { if(lock!=null && lock.isHeldByCurrentThread()){ lock.unlock();; } } }else { System.err.println("抢购结束"); } return map; } }4、前端代码用户登录，LoginView.vue <el-tabs v-model="activeName" @tab-click="handleClick"> <el-tab-pane label="普通登录" name="easyLogin"> <el-form :inline="true" :rules="rules" ref="formInline" :model="formInline" class="demo-form-inline"> <el-form-item label="账号" prop="name"> <el-input v-model="formInline.name" placeholder="账号"></el-input> </el-form-item> <el-form-item label="密码" prop="pwd"> <el-input v-model="formInline.pwd" placeholder="密码"></el-input> </el-form-item> <el-form-item label="验证码" prop="code"> <el-input v-model="formInline.code" placeholder="验证码"></el-input> </el-form-item> <el-form-item> <el-image @click="newCode" :src="codeUrl"></el-image> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit('formInline')">登录</el-button> <el-button @click="resetForm('formInline')">重置</el-button> </el-form-item> </el-form> </el-tab-pane> <el-tab-pane label="手机号加验证码登录" name="telAndYzm"> <el-form :inline="true" :rules="rules" ref="formInline2" :model="formInline2" class="demo-form-inline"> <el-form-item label="手机号" prop="phone"> <el-input v-model="formInline2.phone" placeholder="手机号"></el-input> </el-form-item> <el-form-item label="验证码" prop="code"> <el-input v-model="formInline2.code" placeholder="验证码"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="sendSms(formInline2.phone)">发送短信</el-button> <el-button type="primary" @click="onSubmit2('formInline2')">登录</el-button> </el-form-item> </el-form> </el-tab-pane> <el-tab-pane label="邮箱注册" name="register"> <el-form :inline="true" :rules="rules" ref="formInline3" :model="formInline3" class="demo-form-inline"> <el-form-item label="邮箱" prop="email"> <el-input v-model="formInline3.email" placeholder="QQ号"> @qq.com </el-input> </el-form-item> <el-form-item label="手机号" prop="phone"> <el-input v-model="formInline3.phone" placeholder="手机号"></el-input> </el-form-item> <el-form-item label="验证码" prop="code"> <el-input v-model="formInline3.code" placeholder="验证码"></el-input> </el-form-item> <el-form-item> <el-image @click="newCode" :src="codeUrl"></el-image> </el-form-item> <el-form-item> <el-button type="primary" @click="Register('formInline3')">注册</el-button> <el-button @click="resetForm('formInline3')">重置</el-button> </el-form-item> </el-form> </el-tab-pane> </el-tabs> 车辆列表，ListView.vue <el-form :inline="true" :model="formInline" class="demo-form-inline"> <el-form-item label="车辆品牌"> <el-select v-model="formInline.brandId" placeholder="车辆品牌" clearable="clearable"> <el-option v-for="c in brands" :key="c.id" :label="c.distValue" :value="c.id"></el-option> </el-select> </el-form-item> <el-form-item label="车辆型号"> <el-select v-model="formInline.modelId" placeholder="车辆型号" clearable="clearable"> <el-option v-for="c in models" :key="c.id" :label="c.distValue" :value="c.id"></el-option> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">查询</el-button> <el-button type="primary" @click="openAdd">添加</el-button> </el-form-item> </el-form> <el-table :data="tableData" border style="width: 100%"> <el-table-column prop="num" label="车辆编号" width="180"></el-table-column> <el-table-column prop="modelName" label="型号" width="100"></el-table-column> <el-table-column prop="brandName" label="品牌" width="100"></el-table-column> <el-table-column prop="outs" label="排量(T)" width="100"> {{scope.row.outs}}.0 </el-table-column> <el-table-column prop="speed" label="变速箱" width="100"></el-table-column> <el-table-column prop="years" label="年代" width="100"></el-table-column> <el-table-column prop="rent" label="日租金" width="100"></el-table-column> <el-table-column prop="imgUrl" label="图片" width="150"> <el-image :src="scope.row.imgUrl" style="width: 80px;height: 80px"></el-image> </el-table-column> <el-table-column prop="detail" label="介绍" width="180"></el-table-column> <el-table-column prop="stat" label="状态" width="100"> 空闲 出租 </el-table-column> <el-table-column label="操作" width="150"> <el-button type="warning" size="mini" @click="openUpdate(scope.row)">编辑</el-button> <el-button type="danger" size="mini" @click="delCar(scope.row.id)">删除</el-button> <el-button type="primary" size="mini" @click="goCar(scope.row)">租车</el-button> </el-table-column> </el-table> <el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="current" :page-sizes="[3, 6, 9, 12]" :page-size="size" layout="total, sizes, prev, pager, next, jumper" :total="total"> </el-pagination> <el-dialog title="车辆录入" :visible.sync="addDialogFormVisible"> <el-form :model="addForm"> <el-form-item label="车辆信息：" :label-width="formLabelWidth"> <el-select v-model="addForm.brandId" placeholder="请选择车辆品牌" clearable="clearable"> <el-option v-for="c in brands" :key="c.id" :label="c.distValue" :value="c.id"></el-option> </el-select> <el-select v-model="addForm.distId" placeholder="请选择车系" clearable="clearable"> <el-option v-for="c in dists" :key="c.id" :label="c.distValue" :value="c.id"></el-option> </el-select> <el-select v-model="addForm.modelId" placeholder="请选择车辆型号" clearable="clearable"> <el-option v-for="c in models" :key="c.id" :label="c.distValue" :value="c.id"></el-option> </el-select> </el-form-item> <el-form-item label="预售价格：" :label-width="formLabelWidth"> <el-input v-model="addForm.price" autocomplete="off" clearable="clearable"></el-input> </el-form-item> <el-form-item label="行驶里程：" :label-width="formLabelWidth"> <el-input v-model="addForm.mileage" autocomplete="off" clearable="clearable"></el-input> </el-form-item> <el-form-item label="车辆颜色：" :label-width="formLabelWidth"> <el-radio-group v-model="addForm.colorId"> <el-radio v-for="c in colors" :label="c.id" >{{c.distValue}}</el-radio> </el-radio-group> </el-form-item> <el-form-item label="联系人：" :label-width="formLabelWidth"> <el-input v-model="addForm.userName" autocomplete="off" clearable="clearable"></el-input> </el-form-item> <el-form-item label="手机号：" :label-width="formLabelWidth"> <el-input v-model="addForm.userPhone" autocomplete="off" clearable="clearable"></el-input> </el-form-item> <el-form-item label="车辆介绍：" :label-width="formLabelWidth"> <el-input type="textarea" v-model="addForm.detail" clearable="clearable"></el-input> </el-form-item> <el-form-item label="车辆图片：" :label-width="formLabelWidth"> <el-upload class="avatar-uploader" action="http://127.0.0.1:9999/car-api/upload/minio" :show-file-list="false" :on-success="handleAvatarSuccess"> </el-upload> </el-form-item> </el-form> <el-button @click="addDialogFormVisible = false">取 消</el-button> <el-button type="primary" @click="saveCar('addForm')">确 定</el-button> </el-dialog> <el-dialog title="车辆修改" :visible.sync="updateDialogFormVisible"> <el-form :model="updateForm"> <el-form-item label="车辆信息：" :label-width="formLabelWidth"> <el-select v-model="updateForm.brandId" placeholder="请选择车辆品牌" clearable="clearable"> <el-option v-for="c in brands" :key="c.id" :label="c.distValue" :value="c.id"></el-option> </el-select> <el-select v-model="updateForm.distId" placeholder="请选择车系" clearable="clearable"> <el-option v-for="c in dists" :key="c.id" :label="c.distValue" :value="c.id"></el-option> </el-select> <el-select v-model="updateForm.modelId" placeholder="请选择车辆型号" clearable="clearable"> <el-option v-for="c in models" :key="c.id" :label="c.distValue" :value="c.id"></el-option> </el-select> </el-form-item> <el-form-item label="预售价格：" :label-width="formLabelWidth"> <el-input v-model="updateForm.price" autocomplete="off" clearable="clearable"></el-input> </el-form-item> <el-form-item label="行驶里程：" :label-width="formLabelWidth"> <el-input v-model="updateForm.mileage" autocomplete="off" clearable="clearable"></el-input> </el-form-item> <el-form-item label="车辆颜色：" :label-width="formLabelWidth"> <el-radio-group v-model="updateForm.colorId"> <el-radio v-for="c in colors" :label="c.id" >{{c.distValue}}</el-radio> </el-radio-group> </el-form-item> <el-form-item label="联系人：" :label-width="formLabelWidth"> <el-input v-model="updateForm.userName" autocomplete="off" clearable="clearable"></el-input> </el-form-item> <el-form-item label="手机号：" :label-width="formLabelWidth"> <el-input v-model="updateForm.userPhone" autocomplete="off" clearable="clearable"></el-input> </el-form-item> <el-form-item label="车辆介绍：" :label-width="formLabelWidth"> <el-input type="textarea" v-model="updateForm.detail" clearable="clearable"></el-input> </el-form-item> <el-form-item label="车辆图片：" :label-width="formLabelWidth"> <el-upload class="avatar-uploader" action="http://127.0.0.1:9999/car-api/upload/minio" :show-file-list="false" :on-success="handleAvatarSuccess"> </el-upload> </el-form-item> </el-form> <el-button @click="updateDialogFormVisible = false">取 消</el-button> <el-button type="primary" @click="updateCar('updateForm')">确 定</el-button> </el-dialog> <el-dialog title="租车详情" :visible.sync="orderDialogFormVisible"> <el-form :model="orderForm"> <el-form-item label="车辆编号" :label-width="formLabelWidth"> <el-input v-model="orderForm.carNo" autocomplete="off"></el-input> </el-form-item> <el-form-item label="租金/天" :label-width="formLabelWidth"> <el-input v-model="orderForm.carRent" autocomplete="off"></el-input> </el-form-item> <el-form-item label="租用天数" :label-width="formLabelWidth"> <el-date-picker v-model="value" type="daterange" value-format="yyyy-MM-dd" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"> </el-date-picker> </el-form-item> </el-form> <el-button @click="orderDialogFormVisible = false">取 消</el-button> <el-button type="primary" @click="saveOrder">确 定</el-button> </el-dialog> 订单列表，OrderView.vue <el-button type="primary" @click="check">校验订单</el-button> <el-image src="https://llh-images.oss-cn-beijing.aliyuncs.com/test/6454bb7c-55e7-4ae7-808c-5c6c5204f0e2.jpg" style="width: 100px;height: 100px"></el-image> <el-image src="https://llh-images.oss-cn-beijing.aliyuncs.com/bdaffc4d-cdac-46dd-85a4-ea36cecb122b.jpg" style="width: 100px;height: 100px"></el-image> <el-table :data="tableData" border style="width: 100%"> <el-table-column prop="num" label="订单编号" width="180"></el-table-column> <el-table-column prop="carNo" label="车辆编号" width="180"></el-table-column> <el-table-column prop="carRent" label="租金/天" width="150"></el-table-column> <el-table-column prop="carPrice" label="费用" width="150"></el-table-column> <el-table-column prop="userPhone" label="联系人电话" width="150"></el-table-column> <el-table-column prop="remark" label="说明" width="180"> {{scope.row.remark}} <el-button type="warning" size="mini" @click="goUpdate(scope.row.num)">去修改</el-button> {{scope.row.remark}} <el-button type="warning" size="mini" @click="goPay(scope.row.num)">去支付</el-button> {{scope.row.remark}} {{scope.row.remark}} </el-table-column> <el-table-column prop="createTime" label="提交时间" width="180"></el-table-column> <el-table-column label="操作" width="150"> <el-button type="primary" size="mini" @click="goPay(scope.row.num)">待支付</el-button> <el-button type="danger" size="mini" @click="refund(scope.row.num)">去退款</el-button> 已退款 </el-table-column> </el-table>