Bladeren bron

1、新增WebClient配置、配置连接池
2、优化数字人流请求tts服务

liyanbo 1 maand geleden
bovenliggende
commit
e1a5af5849

+ 11 - 1
byzs-framework/byzs-spring-boot-starter-web/pom.xml

@@ -26,6 +26,16 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
+        <!-- WebFlux 相关,用于 WebClient -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+        <!-- Reactor Netty 相关,WebClient 的底层实现 -->
+        <dependency>
+            <groupId>io.projectreactor.netty</groupId>
+            <artifactId>reactor-netty</artifactId>
+        </dependency>
         <!-- spring boot 配置所需依赖 -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -72,4 +82,4 @@
         </dependency>
     </dependencies>
 
-</project>
+</project>

+ 78 - 0
byzs-framework/byzs-spring-boot-starter-web/src/main/java/cn/iocoder/byzs/framework/web/config/WebClientAutoConfiguration.java

@@ -0,0 +1,78 @@
+package cn.iocoder.byzs.framework.web.config;
+
+import io.netty.channel.ChannelOption;
+import io.netty.handler.timeout.ReadTimeoutHandler;
+import io.netty.handler.timeout.WriteTimeoutHandler;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.resources.ConnectionProvider;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * WebClient 自动配置
+ *
+ * @author fansili
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(WebClientProperties.class)
+public class WebClientAutoConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean
+    public WebClient webClient(WebClient.Builder builder) {
+        return builder.build();
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public WebClient.Builder webClientBuilder(WebClientProperties properties) {
+        // 配置连接池
+        ConnectionProvider connectionProvider = ConnectionProvider.builder("web-client-pool")
+                .maxConnections(properties.getPool().getMaxConnections())
+                .maxIdleTime(properties.getPool().getMaxIdleTime())
+                .maxLifeTime(properties.getPool().getMaxLifeTime())
+                .build();
+
+        // 配置 HttpClient
+        HttpClient httpClient = HttpClient.create(connectionProvider)
+                // 连接超时设置
+                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) properties.getConnectTimeout().toMillis())
+                // 读取超时设置
+                .doOnConnected(conn -> conn
+                        .addHandlerLast(new ReadTimeoutHandler((int) properties.getReadTimeout().getSeconds(), TimeUnit.SECONDS))
+                        .addHandlerLast(new WriteTimeoutHandler((int) properties.getWriteTimeout().getSeconds(), TimeUnit.SECONDS)))
+                // 增加连接复用
+                .keepAlive(true);
+
+        // 构建 WebClient.Builder
+        return WebClient.builder()
+                .clientConnector(new ReactorClientHttpConnector(httpClient));
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public HttpClient httpClient(WebClientProperties properties) {
+        // 配置连接池
+        ConnectionProvider connectionProvider = ConnectionProvider.builder("web-client-pool")
+                .maxConnections(properties.getPool().getMaxConnections())
+                .maxIdleTime(properties.getPool().getMaxIdleTime())
+                .maxLifeTime(properties.getPool().getMaxLifeTime())
+                .build();
+
+        // 配置 HttpClient
+        return HttpClient.create(connectionProvider)
+                // 连接超时设置
+                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) properties.getConnectTimeout().toMillis())
+                // 读取超时设置
+                .doOnConnected(conn -> conn
+                        .addHandlerLast(new ReadTimeoutHandler((int) properties.getReadTimeout().getSeconds(), TimeUnit.SECONDS))
+                        .addHandlerLast(new WriteTimeoutHandler((int) properties.getWriteTimeout().getSeconds(), TimeUnit.SECONDS)));
+    }
+
+}

+ 170 - 0
byzs-framework/byzs-spring-boot-starter-web/src/main/java/cn/iocoder/byzs/framework/web/config/WebClientProperties.java

@@ -0,0 +1,170 @@
+package cn.iocoder.byzs.framework.web.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.time.Duration;
+
+/**
+ * WebClient 配置属性
+ *
+ * @author fansili
+ */
+@ConfigurationProperties(prefix = "byzs.web.web-client")
+public class WebClientProperties {
+
+    /**
+     * 连接超时时间
+     */
+    private Duration connectTimeout = Duration.ofSeconds(15);
+
+    /**
+     * 读取超时时间
+     */
+    private Duration readTimeout = Duration.ofSeconds(30);
+
+    /**
+     * 写入超时时间
+     */
+    private Duration writeTimeout = Duration.ofSeconds(30);
+
+    /**
+     * 连接池配置
+     */
+    private Pool pool = new Pool();
+
+    /**
+     * 重试配置
+     */
+    private Retry retry = new Retry();
+
+    public Duration getConnectTimeout() {
+        return connectTimeout;
+    }
+
+    public void setConnectTimeout(Duration connectTimeout) {
+        this.connectTimeout = connectTimeout;
+    }
+
+    public Duration getReadTimeout() {
+        return readTimeout;
+    }
+
+    public void setReadTimeout(Duration readTimeout) {
+        this.readTimeout = readTimeout;
+    }
+
+    public Duration getWriteTimeout() {
+        return writeTimeout;
+    }
+
+    public void setWriteTimeout(Duration writeTimeout) {
+        this.writeTimeout = writeTimeout;
+    }
+
+    public Pool getPool() {
+        return pool;
+    }
+
+    public void setPool(Pool pool) {
+        this.pool = pool;
+    }
+
+    public Retry getRetry() {
+        return retry;
+    }
+
+    public void setRetry(Retry retry) {
+        this.retry = retry;
+    }
+
+    /**
+     * 连接池配置
+     */
+    public static class Pool {
+
+        /**
+         * 最大连接数
+         */
+        private int maxConnections = 50;
+
+        /**
+         * 最大空闲时间
+         */
+        private Duration maxIdleTime = Duration.ofMinutes(1);
+
+        /**
+         * 最大生命周期
+         */
+        private Duration maxLifeTime = Duration.ofMinutes(5);
+
+        public int getMaxConnections() {
+            return maxConnections;
+        }
+
+        public void setMaxConnections(int maxConnections) {
+            this.maxConnections = maxConnections;
+        }
+
+        public Duration getMaxIdleTime() {
+            return maxIdleTime;
+        }
+
+        public void setMaxIdleTime(Duration maxIdleTime) {
+            this.maxIdleTime = maxIdleTime;
+        }
+
+        public Duration getMaxLifeTime() {
+            return maxLifeTime;
+        }
+
+        public void setMaxLifeTime(Duration maxLifeTime) {
+            this.maxLifeTime = maxLifeTime;
+        }
+    }
+
+    /**
+     * 重试配置
+     */
+    public static class Retry {
+
+        /**
+         * 是否启用重试
+         */
+        private boolean enabled = true;
+
+        /**
+         * 最大重试次数
+         */
+        private int maxAttempts = 3;
+
+        /**
+         * 重试间隔时间
+         */
+        private Duration backoff = Duration.ofMillis(100);
+
+        public boolean isEnabled() {
+            return enabled;
+        }
+
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+
+        public int getMaxAttempts() {
+            return maxAttempts;
+        }
+
+        public void setMaxAttempts(int maxAttempts) {
+            this.maxAttempts = maxAttempts;
+        }
+
+        public Duration getBackoff() {
+            return backoff;
+        }
+
+        public void setBackoff(Duration backoff) {
+            this.backoff = backoff;
+        }
+    }
+
+}

+ 1 - 0
byzs-framework/byzs-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -2,5 +2,6 @@ cn.iocoder.byzs.framework.apilog.config.YudaoApiLogAutoConfiguration
 cn.iocoder.byzs.framework.jackson.config.YudaoJacksonAutoConfiguration
 cn.iocoder.byzs.framework.swagger.config.YudaoSwaggerAutoConfiguration
 cn.iocoder.byzs.framework.web.config.YudaoWebAutoConfiguration
+cn.iocoder.byzs.framework.web.config.WebClientAutoConfiguration
 cn.iocoder.byzs.framework.xss.config.YudaoXssAutoConfiguration
 cn.iocoder.byzs.framework.banner.config.YudaoBannerAutoConfiguration

+ 50 - 36
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/service/chat/AiChatMessageServiceImpl.java

@@ -212,16 +212,45 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         AtomicReference<ScheduledFuture<?>> ttsTask;
         StringBuffer contentTTSBuffer;
         Pattern sentencePattern;
+        Flux<CommonResult<AiChatMessageSendRespVO>> audioStream = Flux.empty();
 
         if (useTts) {
             // 只有当需要使用TTS服务时才创建实例
             streamTtsService = streamTtsServiceProvider.getObject();
-            streamTtsService.startTts(aiTtsDO);
-
             contentTTSBuffer = new StringBuffer();
             sentencePattern = Pattern.compile("[。!?;\n\r]"); // 增加换行符支持
             scheduler = Executors.newSingleThreadScheduledExecutor();
             ttsTask = new AtomicReference<>();
+
+            // 先创建音频流并设置回调,再启动TTS服务
+            audioStream = Flux.create(sink2 -> {
+                AtomicBoolean isFirstChunk = new AtomicBoolean(true); // 首包标志位
+                streamTtsService.setAudioDataCallback(audioBytes -> {
+                    try {
+                        byte[] processedAudio;
+                        if (isFirstChunk.getAndSet(false)) {
+                            // 仅首包添加WAV头
+                            processedAudio = WavHeader.addWavHeader(audioBytes, SampleRateEnum.SAMPLE_RATE_16K.value, 16, 1);
+                            log.info("首包音频带WAV头,长度={} bytes", processedAudio.length);
+                        } else {
+                            // 后续包直接使用原始PCM数据
+                            processedAudio = audioBytes;
+                        }
+                        String base64Audio = Base64.getEncoder().encodeToString(processedAudio);
+                        AiChatMessageSendRespVO audioResp = new AiChatMessageSendRespVO();
+                        audioResp.setEventType("AUDIO");
+                        audioResp.setAudioData(base64Audio);
+                        sink2.next(success(audioResp));
+                    } catch (Exception e) {
+                        log.error("[TTS处理异常] 音频编码失败", e);
+                        sink2.error(new RuntimeException("TTS音频处理失败: " + e.getMessage(), e));
+                    }
+                });
+                streamTtsService.setOnCompleteCallback(sink2::complete);
+            });
+
+            // 启动TTS服务
+            streamTtsService.startTts(aiTtsDO);
         } else {
             streamTtsService = null;
             sentencePattern = null;
@@ -290,7 +319,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
                 scheduler.shutdown(); // 关闭调度器
 
                 // 通知TTS服务文本发送完成
-                streamTtsService.stopTts();
+                try {
+                    streamTtsService.stopTts();
+                } catch (Exception e) {
+                    log.error("停止TTS服务失败", e);
+                }
             }
 
             // 忽略租户,因为 Flux 异步无法透传租户
@@ -303,48 +336,29 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
                     new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())));
             // 发生错误时停止TTS服务
             if (useTts && streamTtsService != null) {
-                streamTtsService.stopTts();
+                try {
+                    streamTtsService.stopTts();
+                } catch (Exception e) {
+                    log.error("停止TTS服务失败", e);
+                }
             }
         })
         // ==== 添加finally块清理 ====
         .doFinally(signalType -> {
             // 通知TTS服务文本发送完成
             if (useTts && streamTtsService != null) {
-                streamTtsService.stopTts();
+                try {
+                    streamTtsService.stopTts();
+                } catch (Exception e) {
+                    log.error("停止TTS服务失败", e);
+                }
+            }
+            // 确保调度器被关闭
+            if (useTts && scheduler != null && !scheduler.isShutdown()) {
+                scheduler.shutdownNow();
             }
         }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
 
-        // 创建音频流 - 只有当需要使用TTS服务时才创建音频流
-        Flux<CommonResult<AiChatMessageSendRespVO>> audioStream = Flux.empty();
-
-        if (useTts) {
-            audioStream = Flux.create(sink2 -> {
-                AtomicBoolean isFirstChunk = new AtomicBoolean(true); // 首包标志位
-                streamTtsService.setAudioDataCallback(audioBytes -> {
-                    try {
-                        byte[] processedAudio;
-                        if (isFirstChunk.getAndSet(false)) {
-                            // 仅首包添加WAV头
-                            processedAudio = WavHeader.addWavHeader(audioBytes, SampleRateEnum.SAMPLE_RATE_16K.value, 16, 1);
-                            log.info("首包音频带WAV头,长度={} bytes", processedAudio.length);
-                        } else {
-                            // 后续包直接使用原始PCM数据
-                            processedAudio = audioBytes;
-                        }
-                        String base64Audio = Base64.getEncoder().encodeToString(processedAudio);
-                        AiChatMessageSendRespVO audioResp = new AiChatMessageSendRespVO();
-                        audioResp.setEventType("AUDIO");
-                        audioResp.setAudioData(base64Audio);
-                        sink2.next(success(audioResp));
-                    } catch (Exception e) {
-                        log.error("[TTS处理异常] 音频编码失败", e);
-                        sink2.error(new RuntimeException("TTS音频处理失败: " + e.getMessage(), e));
-                    }
-                });
-                streamTtsService.setOnCompleteCallback(sink2::complete);
-            });
-        }
-
         // 使用merge而非mergeSequential,确保任一流完成不阻塞其他流
         return Flux.merge(textStream, audioStream)
                 .doFinally(signalType -> {

+ 27 - 4
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/util/tts/StreamTtsService.java

@@ -51,7 +51,9 @@ public class StreamTtsService {
     public void init() {
         getToken();
         // 初始化NlsClient
-        client = new NlsClient(url, token);
+        if (client == null) {
+            client = new NlsClient(url, token);
+        }
     }
 
     public String getToken(){
@@ -126,18 +128,39 @@ public class StreamTtsService {
     public void stopTts() {
         try {
             if (synthesizer != null) {
-                synthesizer.stopStreamInputTts();
-                synthesizer.close();
+                try {
+                    synthesizer.stopStreamInputTts();
+                } catch (Exception e) {
+                    log.error("停止TTS流输入失败", e);
+                }
+                try {
+                    synthesizer.close();
+                } catch (Exception e) {
+                    log.error("关闭TTS合成器失败", e);
+                }
             }
             if (playbackThread != null) {
-                playbackThread.join();
+                try {
+                    playbackThread.join();
+                } catch (Exception e) {
+                    log.error("等待播放线程结束失败", e);
+                }
+            }
+            if (client != null) {
+                try {
+                    client.shutdown();
+                } catch (Exception e) {
+                    log.error("关闭NlsClient失败", e);
+                }
             }
         } catch (Exception e) {
             log.error("停止TTS服务失败", e);
         } finally {
             synthesizer = null;
             playbackThread = null;
+            client = null;
             setAudioDataCallback(null);
+            onCompleteCallback = null;
         }
     }
 

+ 0 - 15
byzs-server/src/main/resources/application-dev.yaml

@@ -92,22 +92,7 @@ spring:
     jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置
       initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。
 
---- #################### 消息队列相关 ####################
 
-# rocketmq 配置项,对应 RocketMQProperties 配置类
-rocketmq:
-  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
-
-spring:
-  # RabbitMQ 配置项,对应 RabbitProperties 配置类
-  rabbitmq:
-    host: 127.0.0.1 # RabbitMQ 服务的地址
-    port: 5672 # RabbitMQ 服务的端口
-    username: guest # RabbitMQ 服务的账号
-    password: guest # RabbitMQ 服务的密码
-  # Kafka 配置项,对应 KafkaProperties 配置类
-  kafka:
-    bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
 
 --- #################### 服务保障相关配置 ####################
 

+ 1 - 14
byzs-server/src/main/resources/application-local.yaml

@@ -116,22 +116,9 @@ spring:
     jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置
       initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。
 
---- #################### 消息队列相关 ####################
 
-# rocketmq 配置项,对应 RocketMQProperties 配置类
-rocketmq:
-  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
 
-spring:
-  # RabbitMQ 配置项,对应 RabbitProperties 配置类
-  rabbitmq:
-    host: 127.0.0.1 # RabbitMQ 服务的地址
-    port: 5672 # RabbitMQ 服务的端口
-    username: rabbit # RabbitMQ 服务的账号
-    password: rabbit # RabbitMQ 服务的密码
-  # Kafka 配置项,对应 KafkaProperties 配置类
-  kafka:
-    bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
+
 
 --- #################### 服务保障相关配置 ####################
 

+ 1 - 16
byzs-server/src/main/resources/application-localProd.yaml

@@ -152,22 +152,7 @@ wx:
       key-prefix: wa # Redis Key 的前缀
       http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台
 
---- #################### 消息队列相关 ####################
 
-# rocketmq 配置项,对应 RocketMQProperties 配置类
-rocketmq:
-  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
-
-spring:
-  # RabbitMQ 配置项,对应 RabbitProperties 配置类
-  rabbitmq:
-    host: 127.0.0.1 # RabbitMQ 服务的地址
-    port: 5672 # RabbitMQ 服务的端口
-    username: rabbit # RabbitMQ 服务的账号
-    password: rabbit # RabbitMQ 服务的密码
-  # Kafka 配置项,对应 KafkaProperties 配置类
-  kafka:
-    bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
 
 --- #################### 服务保障相关配置 ####################
 
@@ -262,4 +247,4 @@ justauth:
   cache:
     type: REDIS
     prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
-    timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟
+    timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟

+ 0 - 15
byzs-server/src/main/resources/application-prod.yaml

@@ -92,22 +92,7 @@ spring:
     jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置
       initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。
 
---- #################### 消息队列相关 ####################
 
-# rocketmq 配置项,对应 RocketMQProperties 配置类
-rocketmq:
-  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
-
-spring:
-  # RabbitMQ 配置项,对应 RabbitProperties 配置类
-  rabbitmq:
-    host: 127.0.0.1 # RabbitMQ 服务的地址
-    port: 5672 # RabbitMQ 服务的端口
-    username: rabbit # RabbitMQ 服务的账号
-    password: rabbit # RabbitMQ 服务的密码
-  # Kafka 配置项,对应 KafkaProperties 配置类
-  kafka:
-    bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
 
 --- #################### 服务保障相关配置 ####################
 

+ 1 - 14
byzs-server/src/main/resources/application-prodDev.yaml

@@ -91,22 +91,9 @@ spring:
     jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置
       initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。
 
---- #################### 消息队列相关 ####################
 
-# rocketmq 配置项,对应 RocketMQProperties 配置类
-rocketmq:
-  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
 
-spring:
-  # RabbitMQ 配置项,对应 RabbitProperties 配置类
-  rabbitmq:
-    host: 127.0.0.1 # RabbitMQ 服务的地址
-    port: 5672 # RabbitMQ 服务的端口
-    username: rabbit # RabbitMQ 服务的账号
-    password: rabbit # RabbitMQ 服务的密码
-  # Kafka 配置项,对应 KafkaProperties 配置类
-  kafka:
-    bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
+
 
 --- #################### 服务保障相关配置 ####################
 

+ 30 - 21
byzs-server/src/main/resources/application.yaml

@@ -121,28 +121,37 @@ aj:
 --- #################### 消息队列相关 ####################
 
 # rocketmq 配置项,对应 RocketMQProperties 配置类
-rocketmq:
-  # Producer 配置项
-  producer:
-    group: ${spring.application.name}_PRODUCER # 生产者分组
+#rocketmq:
+#  # Producer 配置项
+#  producer:
+#    group: ${spring.application.name}_PRODUCER # 生产者分组
+#  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
 
-spring:
-  # Kafka 配置项,对应 KafkaProperties 配置类
-  kafka:
-    # Kafka Producer 配置项
-    producer:
-      acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。
-      retries: 3 # 发送失败时,重试发送的次数
-      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化
-    # Kafka Consumer 配置项
-    consumer:
-      auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解
-      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
-      properties:
-        spring.json.trusted.packages: '*'
-    # Kafka Consumer Listener 监听器配置
-    listener:
-      missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错
+#spring:
+#  # Kafka 配置项,对应 KafkaProperties 配置类
+#  kafka:
+#    # Kafka Producer 配置项
+#    producer:
+#      acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。
+#      retries: 3 # 发送失败时,重试发送的次数
+#      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化
+#    # Kafka Consumer 配置项
+#    consumer:
+#      auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解
+#      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
+#      properties:
+#        spring.json.trusted.packages: '*'
+#    # Kafka Consumer Listener 监听器配置
+#    listener:
+#      missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错
+#    bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
+
+#  # RabbitMQ 配置项,对应 RabbitProperties 配置类
+#  rabbitmq:
+#    host: 127.0.0.1 # RabbitMQ 服务的地址
+#    port: 5672 # RabbitMQ 服务的端口
+#    username: rabbit # RabbitMQ 服务的账号
+#    password: rabbit # RabbitMQ 服务的密码
 
 --- #################### AI 相关配置 ####################