diff --git a/pigx-common/pigx-common-bom/pom.xml b/pigx-common/pigx-common-bom/pom.xml
index 2d8d2be11..a1a3622be 100644
--- a/pigx-common/pigx-common-bom/pom.xml
+++ b/pigx-common/pigx-common-bom/pom.xml
@@ -139,6 +139,11 @@
pigx-common-xss
${pigx.version}
+
+ com.pig4cloud
+ pigx-common-sse
+ ${pigx.version}
+
com.pig4cloud
pigx-common-websocket
diff --git a/pigx-common/pigx-common-sse/pom.xml b/pigx-common/pigx-common-sse/pom.xml
new file mode 100755
index 000000000..3cb211e24
--- /dev/null
+++ b/pigx-common/pigx-common-sse/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+
+ com.pig4cloud
+ pigx-common
+ 5.3.0
+
+
+ pigx-common-sse
+ jar
+
+ pigx sse 服务端推送
+
+
+
+ org.springframework.data
+ spring-data-redis
+
+
+
+ cn.hutool
+ hutool-json
+
+
+
+ com.pig4cloud
+ pigx-common-security
+
+
+
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/LocalMessageDistributorConfiguration.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/LocalMessageDistributorConfiguration.java
new file mode 100644
index 000000000..0d30ad835
--- /dev/null
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/LocalMessageDistributorConfiguration.java
@@ -0,0 +1,30 @@
+package com.pig4cloud.pigx.common.sse.config;
+
+import com.pig4cloud.pigx.common.sse.distribute.LocalMessageDistributor;
+import com.pig4cloud.pigx.common.sse.distribute.MessageDistributor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 本地的消息分发器配置
+ *
+ * @author lengleng
+ */
+@ConditionalOnProperty(prefix = SseEmitterProperties.PREFIX, name = "message-distributor",
+ havingValue = MessageDistributorTypeConstants.LOCAL)
+@Configuration(proxyBeanMethods = false)
+public class LocalMessageDistributorConfiguration {
+
+ /**
+ * 本地基于内存的消息分发,不支持集群
+ * @return LocalMessageDistributor
+ */
+ @Bean
+ @ConditionalOnMissingBean(MessageDistributor.class)
+ public LocalMessageDistributor messageDistributor() {
+ return new LocalMessageDistributor();
+ }
+
+}
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/MessageDistributorTypeConstants.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/MessageDistributorTypeConstants.java
new file mode 100644
index 000000000..6ae8cee0b
--- /dev/null
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/MessageDistributorTypeConstants.java
@@ -0,0 +1,26 @@
+package com.pig4cloud.pigx.common.sse.config;
+
+/**
+ * @author lengleng
+ */
+public final class MessageDistributorTypeConstants {
+
+ private MessageDistributorTypeConstants() {
+ }
+
+ /**
+ * 本地
+ */
+ public static final String LOCAL = "local";
+
+ /**
+ * 基于 Redis PUB/SUB
+ */
+ public static final String REDIS = "redis";
+
+ /**
+ * 自定义
+ */
+ public static final String CUSTOM = "custom";
+
+}
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/RedisMessageDistributorConfiguration.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/RedisMessageDistributorConfiguration.java
new file mode 100644
index 000000000..72010638d
--- /dev/null
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/RedisMessageDistributorConfiguration.java
@@ -0,0 +1,69 @@
+package com.pig4cloud.pigx.common.sse.config;
+
+import com.pig4cloud.pigx.common.sse.distribute.MessageDistributor;
+import com.pig4cloud.pigx.common.sse.distribute.RedisMessageDistributor;
+import com.pig4cloud.pigx.common.sse.distribute.RedisSseEmitterMessageListener;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.listener.PatternTopic;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+
+/**
+ * 基于 Redis Pub/Sub 的消息分发器
+ *
+ * @author lengleng
+ */
+@ConditionalOnClass(StringRedisTemplate.class)
+@ConditionalOnProperty(prefix = SseEmitterProperties.PREFIX, name = "message-distributor",
+ havingValue = MessageDistributorTypeConstants.REDIS, matchIfMissing = true)
+@Configuration(proxyBeanMethods = false)
+public class RedisMessageDistributorConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(MessageDistributor.class)
+ public RedisMessageDistributor messageDistributor(StringRedisTemplate stringRedisTemplate) {
+ return new RedisMessageDistributor(stringRedisTemplate);
+ }
+
+ @Bean
+ @ConditionalOnBean(RedisMessageDistributor.class)
+ @ConditionalOnMissingBean
+ public RedisSseEmitterMessageListener redisWebsocketMessageListener(StringRedisTemplate stringRedisTemplate) {
+ return new RedisSseEmitterMessageListener(stringRedisTemplate);
+ }
+
+ @Bean
+ @ConditionalOnBean(RedisMessageDistributor.class)
+ @ConditionalOnMissingBean
+ public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
+ RedisMessageListenerContainer container = new RedisMessageListenerContainer();
+ container.setConnectionFactory(connectionFactory);
+ return container;
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnMissingBean(MessageDistributor.class)
+ @RequiredArgsConstructor
+ static class RedisMessageListenerRegisterConfiguration {
+
+ private final RedisMessageListenerContainer redisMessageListenerContainer;
+
+ private final RedisSseEmitterMessageListener redisWebsocketMessageListener;
+
+ @PostConstruct
+ public void addMessageListener() {
+ redisMessageListenerContainer.addMessageListener(redisWebsocketMessageListener,
+ new PatternTopic(RedisSseEmitterMessageListener.CHANNEL));
+ }
+
+ }
+
+}
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterAutoConfiguration.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterAutoConfiguration.java
new file mode 100644
index 000000000..c2fa9cdec
--- /dev/null
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterAutoConfiguration.java
@@ -0,0 +1,21 @@
+package com.pig4cloud.pigx.common.sse.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * websocket自动配置
+ *
+ * @author Yakir
+ */
+@EnableConfigurationProperties(SseEmitterProperties.class)
+public class SseEmitterAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SseEmitterEndpoint sseEmitterEndpoint() {
+ return new SseEmitterEndpoint();
+ }
+
+}
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterEndpoint.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterEndpoint.java
new file mode 100644
index 000000000..b39693c6c
--- /dev/null
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterEndpoint.java
@@ -0,0 +1,45 @@
+package com.pig4cloud.pigx.common.sse.config;
+
+import com.pig4cloud.pigx.common.security.util.SecurityUtils;
+import com.pig4cloud.pigx.common.sse.holder.SseEmitterHolder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.util.Objects;
+
+/**
+ * @author lengleng
+ * @date 2023/12/11
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/sse")
+public class SseEmitterEndpoint {
+
+ @RequestMapping("/info")
+ public SseEmitter info() {
+ SseEmitter sseEmitter = new SseEmitter(0L);
+
+ if (Objects.isNull(SecurityUtils.getUser())) {
+ log.debug("MSG: SseConnectError");
+ return null;
+ }
+ log.info("MSG: SseConnect | EmitterHash: {} | ID: {}", sseEmitter.hashCode(), SecurityUtils.getUser().getId());
+ sseEmitter.onCompletion(() -> {
+ log.info("MSG: SseConnectCompletion | EmitterHash: {} |ID: {}", sseEmitter.hashCode(), SecurityUtils.getUser().getId());
+ });
+ sseEmitter.onTimeout(() -> {
+ log.error("MSG: SseConnectTimeout | EmitterHash: {} |ID: {} ", sseEmitter.hashCode(), SecurityUtils.getUser().getId());
+ });
+ sseEmitter.onError(t -> {
+ log.error("MSG: SseConnectError | EmitterHash: {} |ID: {}", sseEmitter.hashCode(), SecurityUtils.getUser().getId());
+ });
+ SseEmitterHolder.addSseEmitter(SecurityUtils.getUser().getId(), sseEmitter);
+ return sseEmitter;
+ }
+
+}
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterMessageSender.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterMessageSender.java
new file mode 100644
index 000000000..c3a4b5922
--- /dev/null
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterMessageSender.java
@@ -0,0 +1,59 @@
+package com.pig4cloud.pigx.common.sse.config;
+
+import cn.hutool.core.util.ReflectUtil;
+import com.pig4cloud.pigx.common.sse.holder.SseEmitterHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * @author lengleng 2021/1/4
+ * @version 1.0
+ */
+@Slf4j
+public class SseEmitterMessageSender {
+
+ public static void broadcast(String message) {
+ Collection sseEmitters = SseEmitterHolder.getSseEmitters();
+ for (SseEmitter sseEmitter : sseEmitters) {
+ send(sseEmitter, message);
+ }
+ }
+
+ public static boolean send(Object sseEmitterKey, String message) {
+ SseEmitter sseEmitter = SseEmitterHolder.getSseEmitter(sseEmitterKey);
+ if (sseEmitter == null) {
+ log.info("[send] 当前 sseEmitterKey:{} 对应 sseEmitter 不在本服务中", sseEmitterKey);
+ return false;
+ }
+ else {
+ return send(sseEmitter, message);
+ }
+ }
+
+ public static boolean send(SseEmitter sseEmitter, String message) {
+ if (sseEmitter == null) {
+ log.error("[send] sseEmitter 为 null");
+ return false;
+ }
+
+ Object sendFailed = ReflectUtil.getFieldValue(sseEmitter, "sendFailed");
+ if ((boolean) sendFailed) {
+ log.error("[send] sseEmitter 已经关闭");
+ SseEmitterHolder.removeSseEmitter(sseEmitter);
+ return false;
+ }
+ try {
+ sseEmitter.send(message);
+ }
+ catch (IOException e) {
+ log.error("[send] sseEmitter({}) 发送消息({}) 异常", sseEmitter, message, e);
+ SseEmitterHolder.removeSseEmitter(sseEmitter);
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterProperties.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterProperties.java
new file mode 100644
index 000000000..238969416
--- /dev/null
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/config/SseEmitterProperties.java
@@ -0,0 +1,28 @@
+package com.pig4cloud.pigx.common.sse.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * websocket props
+ *
+ * @author Yakir
+ */
+@Data
+@ConfigurationProperties(SseEmitterProperties.PREFIX)
+public class SseEmitterProperties {
+
+ public static final String PREFIX = "pigx.sse";
+
+ /**
+ * 路径: 无参: /ws 有参: PathVariable: 单参: /ws/{test} 多参: /ws/{test1}/{test2} query:
+ * /ws?uid=1&name=test
+ */
+ private String path = "/sse/info";
+
+ /**
+ * 消息分发器:local | redis,默认 local, 如果自定义的话,可以配置为其他任意值
+ */
+ private String messageDistributor = MessageDistributorTypeConstants.LOCAL;
+
+}
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/distribute/LocalMessageDistributor.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/distribute/LocalMessageDistributor.java
new file mode 100644
index 000000000..9a7d92867
--- /dev/null
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/distribute/LocalMessageDistributor.java
@@ -0,0 +1,20 @@
+package com.pig4cloud.pigx.common.sse.distribute;
+
+/**
+ * 本地消息分发,直接进行发送
+ *
+ * @author lengleng 2021/1/12
+ * @version 1.0
+ */
+public class LocalMessageDistributor implements MessageDistributor, MessageSender {
+
+ /**
+ * 消息分发
+ * @param messageDO 发送的消息
+ */
+ @Override
+ public void distribute(MessageDO messageDO) {
+ doSend(messageDO);
+ }
+
+}
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/distribute/MessageDO.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/distribute/MessageDO.java
new file mode 100644
index 000000000..e1fd87688
--- /dev/null
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/distribute/MessageDO.java
@@ -0,0 +1,39 @@
+package com.pig4cloud.pigx.common.sse.distribute;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+/**
+ * @author lengleng 2021/1/12
+ * @version 1.0
+ */
+@Data
+@Accessors(chain = true)
+public class MessageDO {
+
+ /**
+ * 是否广播
+ */
+ private Boolean needBroadcast;
+
+ /**
+ * sessionKeys
+ */
+ private List