diff --git a/logs/system-server.log.2025-04-22.0.gz b/logs/system-server.log.2025-04-22.0.gz
deleted file mode 100644
index e4bce88..0000000
Binary files a/logs/system-server.log.2025-04-22.0.gz and /dev/null differ
diff --git a/tashow-framework/tashow-framework-mq/pom.xml b/tashow-framework/tashow-framework-mq/pom.xml
index 3128466..835535e 100644
--- a/tashow-framework/tashow-framework-mq/pom.xml
+++ b/tashow-framework/tashow-framework-mq/pom.xml
@@ -2,32 +2,42 @@
- 4.0.0
- com.tashow.cloud
tashow-framework
+ com.tashow.cloud
${revision}
+ 4.0.0
+
tashow-framework-mq
jar
${project.artifactId}
- 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种
+ 消息队列模块,基于RabbitMQ等中间件
+ https://github.com/tashow/tashow-platform
-
-
- com.tashow.cloud
- tashow-data-redis
-
+
org.springframework.boot
spring-boot-starter-amqp
+ true
+
- org.springframework.amqp
- spring-rabbit
+ org.springframework
+ spring-web
+ true
+
+
+ org.springframework
+ spring-webmvc
+ true
+
+
+ jakarta.servlet
+ jakarta.servlet-api
true
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/BaseMqMessage.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/BaseMqMessage.java
new file mode 100644
index 0000000..2415c34
--- /dev/null
+++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/BaseMqMessage.java
@@ -0,0 +1,115 @@
+package com.tashow.cloud.mq.core;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * MQ消息基类,所有消息类型都应该继承此类
+ *
+ * @author tashow
+ */
+public class BaseMqMessage implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 消息ID,默认为UUID
+ */
+ private Integer id = UUID.randomUUID().hashCode();
+
+ /**
+ * 消息状态码
+ */
+ private Integer statusCode;
+
+ /**
+ * 消息重试次数
+ */
+ private Integer retryCount = 0;
+
+ /**
+ * 消息错误信息
+ */
+ private String errorMessage;
+
+ /**
+ * 消息创建时间
+ */
+ private Date createTime = new Date();
+
+ /**
+ * 扩展数据
+ */
+ private Map extraData = new HashMap<>();
+
+ /**
+ * 增加重试次数
+ */
+ public void incrementRetryCount() {
+ if (retryCount == null) {
+ retryCount = 0;
+ }
+ retryCount++;
+ }
+
+ /**
+ * 添加额外数据
+ */
+ public void addExtraData(String key, Object value) {
+ if (extraData == null) {
+ extraData = new HashMap<>();
+ }
+ extraData.put(key, value);
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public Integer getStatusCode() {
+ return statusCode;
+ }
+
+ public void setStatusCode(Integer statusCode) {
+ this.statusCode = statusCode;
+ }
+
+ public Integer getRetryCount() {
+ return retryCount;
+ }
+
+ public void setRetryCount(Integer retryCount) {
+ this.retryCount = retryCount;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public Date getCreateTime() {
+ return createTime;
+ }
+
+ public void setCreateTime(Date createTime) {
+ this.createTime = createTime;
+ }
+
+ public Map getExtraData() {
+ return extraData;
+ }
+
+ public void setExtraData(Map extraData) {
+ this.extraData = extraData;
+ }
+}
\ No newline at end of file
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/CustomCorrelationData.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/CustomCorrelationData.java
new file mode 100644
index 0000000..518d0fd
--- /dev/null
+++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/CustomCorrelationData.java
@@ -0,0 +1,44 @@
+package com.tashow.cloud.mq.core;
+
+import org.springframework.amqp.rabbit.connection.CorrelationData;
+
+/**
+ * 自定义关联数据,用于存储额外的消息数据
+ *
+ * @author tashow
+ */
+public class CustomCorrelationData extends CorrelationData {
+
+ /**
+ * 消息内容
+ */
+ private final String messageContent;
+
+ /**
+ * 构造函数
+ *
+ * @param id 关联ID
+ * @param messageContent 消息内容
+ */
+ public CustomCorrelationData(String id, String messageContent) {
+ super(id);
+ this.messageContent = messageContent;
+ }
+
+ /**
+ * 获取消息内容
+ *
+ * @return 消息内容
+ */
+ public String getMessageContent() {
+ return messageContent;
+ }
+
+ @Override
+ public String toString() {
+ return "CustomCorrelationData{" +
+ "id='" + getId() + '\'' +
+ ", messageContent='" + messageContent + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/handler/FailRecordHandler.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/handler/FailRecordHandler.java
new file mode 100644
index 0000000..ad5266f
--- /dev/null
+++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/handler/FailRecordHandler.java
@@ -0,0 +1,39 @@
+package com.tashow.cloud.mq.handler;
+
+/**
+ * 消息发送失败记录处理接口
+ *
+ * @author tashow
+ */
+public interface FailRecordHandler {
+
+ /**
+ * 保存消息发送失败记录
+ *
+ * @param correlationId 关联ID
+ * @param exchange 交换机
+ * @param routingKey 路由键
+ * @param cause 失败原因
+ * @param messageContent 消息内容
+ */
+ void saveFailRecord(String correlationId, String exchange, String routingKey, String cause, String messageContent);
+
+ /**
+ * 检查是否达到告警阈值
+ *
+ * @return 是否需要告警
+ */
+ default boolean checkAlertThreshold() {
+ return checkAlertThreshold(null);
+ }
+
+ /**
+ * 检查是否达到告警阈值,带错误信息
+ *
+ * @param cause 错误原因
+ * @return 是否需要告警
+ */
+ default boolean checkAlertThreshold(String cause) {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/package-info.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/package-info.java
deleted file mode 100644
index 356754a..0000000
--- a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/package-info.java
+++ /dev/null
@@ -1 +0,0 @@
-package com.tashow.cloud.mq;
\ No newline at end of file
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQAutoConfiguration.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQAutoConfiguration.java
index f161527..843e28d 100644
--- a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQAutoConfiguration.java
+++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQAutoConfiguration.java
@@ -1,28 +1,19 @@
package com.tashow.cloud.mq.rabbitmq.config;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
-import org.springframework.amqp.support.converter.MessageConverter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
-import org.springframework.context.annotation.Bean;
/**
- * RabbitMQ 消息队列配置类
+ * RabbitMQ 消息队列自动配置类
*
- * @author 芋道源码
+ * @author tashow
*/
@AutoConfiguration
-@Slf4j
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
-public class RabbitMQAutoConfiguration {
+public class RabbitMQAutoConfiguration extends RabbitMQConfiguration {
- /**
- * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息
- */
- @Bean
- public MessageConverter createMessageConverter() {
- return new Jackson2JsonMessageConverter();
- }
+ private static final Logger log = LoggerFactory.getLogger(RabbitMQAutoConfiguration.class);
-}
+}
\ No newline at end of file
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQConfiguration.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQConfiguration.java
new file mode 100644
index 0000000..028f91b
--- /dev/null
+++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQConfiguration.java
@@ -0,0 +1,49 @@
+package com.tashow.cloud.mq.rabbitmq.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.MessageConverter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * RabbitMQ 配置类
+ *
+ * @author tashow
+ */
+@Configuration
+@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
+public class RabbitMQConfiguration {
+
+ private static final Logger log = LoggerFactory.getLogger(RabbitMQConfiguration.class);
+
+ /**
+ * 初始化RabbitTemplate
+ *
+ * @param rabbitTemplate RabbitTemplate
+ */
+ protected void initRabbitTemplate(RabbitTemplate rabbitTemplate) {
+ log.info("[MQ配置] 初始化RabbitTemplate: {}", rabbitTemplate);
+ // 启用消息发送到交换机确认机制
+ rabbitTemplate.setMandatory(true);
+
+ if (rabbitTemplate.isConfirmListener()) {
+ log.info("[MQ配置] 确认回调已正确配置");
+ } else {
+ log.error("[MQ配置] 确认回调配置失败");
+ }
+ }
+
+ /**
+ * 创建消息转换器
+ *
+ * @return MessageConverter
+ */
+ @Bean
+ public MessageConverter messageConverter() {
+ return new Jackson2JsonMessageConverter();
+ }
+}
\ No newline at end of file
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/consumer/AbstractRabbitMQConsumer.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/consumer/AbstractRabbitMQConsumer.java
new file mode 100644
index 0000000..ebf65bf
--- /dev/null
+++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/consumer/AbstractRabbitMQConsumer.java
@@ -0,0 +1,174 @@
+package com.tashow.cloud.mq.rabbitmq.consumer;
+
+import com.rabbitmq.client.Channel;
+import com.tashow.cloud.mq.core.BaseMqMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.amqp.support.AmqpHeaders;
+import org.springframework.messaging.handler.annotation.Header;
+
+/**
+ * RabbitMQ消息消费者抽象类
+ *
+ * @param 消息类型
+ * @author tashow
+ */
+public abstract class AbstractRabbitMQConsumer {
+
+ private static final Logger log = LoggerFactory.getLogger(AbstractRabbitMQConsumer.class);
+
+ /**
+ * 消息状态:处理中
+ */
+ public static final int STATUS_PROCESSING = 10;
+
+ /**
+ * 消息状态:成功
+ */
+ public static final int STATUS_SUCCESS = 20;
+
+ /**
+ * 消息状态:失败
+ */
+ public static final int STATUS_ERROR = 30;
+
+ /**
+ * 处理消息
+ *
+ * @param message 消息对象
+ * @return 处理结果,true表示处理成功,false表示处理失败
+ */
+ public abstract boolean processMessage(T message);
+
+ /**
+ * 获取消息重试次数
+ *
+ * @param message 消息对象
+ * @return 重试次数
+ */
+ public Integer getRetryCount(T message) {
+ return message.getRetryCount() != null ? message.getRetryCount() : 0;
+ }
+
+ /**
+ * 更新消息状态
+ *
+ * @param message 消息对象
+ */
+ public abstract void updateMessageStatus(T message);
+
+ /**
+ * 更新消息重试次数
+ *
+ * @param message 消息对象
+ */
+ public abstract void updateRetryCount(T message);
+
+ /**
+ * 保存消息到数据库
+ *
+ * @param message 消息对象
+ * @return 保存结果
+ */
+ public abstract boolean saveToDatabase(T message);
+
+ /**
+ * 保存消息到失败记录
+ *
+ * @param message 消息对象
+ * @param cause 失败原因
+ */
+ public abstract void saveToFailRecord(T message, String cause);
+
+ /**
+ * 获取最大允许重试次数
+ *
+ * @return 最大重试次数
+ */
+ public int getMaxRetryAllowed() {
+ return 3;
+ }
+
+ /**
+ * 消息处理入口
+ *
+ * @param message 消息对象
+ * @param channel 通道
+ * @param deliveryTag 投递标签
+ */
+ public void onMessage(T message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
+ Integer dbRetryCount = getRetryCount(message);
+ message.setRetryCount(dbRetryCount);
+
+ if (message.getRetryCount() != null && message.getRetryCount() >= getMaxRetryAllowed()) {
+ message.setStatusCode(STATUS_ERROR);
+ message.addExtraData("errorMessage", "已达到最大重试次数");
+ saveToFailRecord(message, "已达到最大重试次数");
+ safeChannelAck(channel, deliveryTag);
+ return;
+ }
+
+ log.info("[MQ消费者] 收到消息: {}, 当前重试次数: {}/{}", message, message.getRetryCount(), getMaxRetryAllowed());
+ message.setStatusCode(STATUS_PROCESSING);
+
+ try {
+ boolean result = processMessage(message);
+ if (result) {
+ message.setStatusCode(STATUS_SUCCESS);
+ updateMessageStatus(message);
+ log.info("[MQ消费者] 消息处理成功,状态已更新为成功: {}", message.getId());
+ } else {
+ throw new RuntimeException("消息处理失败");
+ }
+ safeChannelAck(channel, deliveryTag);
+ } catch (Exception e) {
+ message.setStatusCode(STATUS_ERROR);
+ message.addExtraData("errorMessage", e.getMessage());
+ message.setErrorMessage(e.getMessage());
+ log.error("[MQ消费者] 消息处理失败: {}, 错误: {}", message.getId(), e.getMessage());
+
+ message.incrementRetryCount();
+ updateRetryCount(message);
+
+ if (message.getRetryCount() >= getMaxRetryAllowed()) {
+ saveToDatabase(message);
+ log.warn("[MQ消费者] 消息已达到最大重试次数: {}, 确认消息并保存到失败记录表", message.getRetryCount());
+ saveToFailRecord(message, e.getMessage());
+ safeChannelAck(channel, deliveryTag);
+ } else {
+ log.info("[MQ消费者] 消息将重新入队重试: {}, 当前重试次数: {}", message.getId(), message.getRetryCount());
+ safeChannelNack(channel, deliveryTag, false, true);
+ }
+ }
+ }
+
+ /**
+ * 安全确认消息
+ *
+ * @param channel 通道
+ * @param deliveryTag 投递标签
+ */
+ protected void safeChannelAck(Channel channel, long deliveryTag) {
+ try {
+ channel.basicAck(deliveryTag, false);
+ } catch (Exception e) {
+ log.error("[MQ消费者] 确认消息失败: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 安全拒绝消息
+ *
+ * @param channel 通道
+ * @param deliveryTag 投递标签
+ * @param multiple 是否批量
+ * @param requeue 是否重新入队
+ */
+ protected void safeChannelNack(Channel channel, long deliveryTag, boolean multiple, boolean requeue) {
+ try {
+ channel.basicNack(deliveryTag, multiple, requeue);
+ } catch (Exception e) {
+ log.error("[MQ消费者] 拒绝消息失败: {}", e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/producer/AbstractRabbitMQProducer.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/producer/AbstractRabbitMQProducer.java
new file mode 100644
index 0000000..970704e
--- /dev/null
+++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/producer/AbstractRabbitMQProducer.java
@@ -0,0 +1,145 @@
+package com.tashow.cloud.mq.rabbitmq.producer;
+
+import com.tashow.cloud.mq.core.BaseMqMessage;
+import com.tashow.cloud.mq.core.CustomCorrelationData;
+import com.tashow.cloud.mq.handler.FailRecordHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.amqp.core.ReturnedMessage;
+import org.springframework.amqp.rabbit.connection.CorrelationData;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import jakarta.annotation.PostConstruct;
+import java.util.UUID;
+
+/**
+ * RabbitMQ消息生产者抽象类
+ *
+ * @param 消息类型
+ * @author tashow
+ */
+public abstract class AbstractRabbitMQProducer
+ implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
+
+ private static final Logger log = LoggerFactory.getLogger(AbstractRabbitMQProducer.class);
+
+ @Autowired
+ protected RabbitTemplate rabbitTemplate;
+
+ @Autowired(required = false)
+ protected FailRecordHandler failRecordHandler;
+
+ /**
+ * 初始化RabbitTemplate
+ */
+ @PostConstruct
+ public void initRabbitTemplate() {
+ log.info("[MQ生产者] 初始化RabbitTemplate: {}", rabbitTemplate);
+ rabbitTemplate.setMandatory(true);
+ rabbitTemplate.setReturnsCallback(this);
+ rabbitTemplate.setConfirmCallback(this);
+
+ if (rabbitTemplate.isConfirmListener()) {
+ log.info("[MQ生产者] 确认回调已正确配置");
+ } else {
+ log.error("[MQ生产者] 确认回调配置失败");
+ }
+ }
+
+ /**
+ * 将消息转换为字符串
+ *
+ * @param message 消息对象
+ * @return 消息字符串
+ */
+ protected abstract String convertMessageToString(T message);
+
+ /**
+ * 异步发送消息,自动生成correlationId
+ *
+ * @param message 消息对象
+ */
+ public void asyncSendMessage(T message) {
+ String correlationId = UUID.randomUUID().toString();
+ asyncSendMessage(message, correlationId);
+ }
+
+ /**
+ * 异步发送消息,使用指定的correlationId
+ *
+ * @param message 消息对象
+ * @param correlationId 关联ID
+ */
+ public void asyncSendMessage(T message, String correlationId) {
+ log.info("[MQ生产者] 准备发送消息: {}, correlationId: {}", message, correlationId);
+ try {
+ String messageJson = convertMessageToString(message);
+ CustomCorrelationData correlationData = new CustomCorrelationData(correlationId, messageJson);
+ rabbitTemplate.convertAndSend(getExchange(), getRoutingKey(), message, correlationData);
+ log.info("[MQ生产者] 消息发送完成: {}, 状态: {}, 重试次数: {}, correlationId: {}",
+ message.getId(), message.getStatusCode(), message.getRetryCount(), correlationId);
+ } catch (Exception e) {
+ log.error("[MQ生产者] 消息发送异常: {}, correlationId: {}", e.getMessage(), correlationId, e);
+ throw e;
+ }
+ }
+
+ /**
+ * 获取交换机名称
+ *
+ * @return 交换机名称
+ */
+ public abstract String getExchange();
+
+ /**
+ * 获取路由键
+ *
+ * @return 路由键
+ */
+ public abstract String getRoutingKey();
+
+ @Override
+ public void confirm(CorrelationData correlationData, boolean ack, String cause) {
+ if (ack) {
+ log.info("[MQ生产者] 消息发送确认成功: {}", correlationData.getId());
+ } else {
+ log.error("[MQ生产者] 消息发送确认失败: {}, 原因: {}, correlationData={}",
+ correlationData.getId(), cause, correlationData);
+ if (failRecordHandler != null && correlationData instanceof CustomCorrelationData) {
+ CustomCorrelationData customData = (CustomCorrelationData) correlationData;
+ String messageContent = customData.getMessageContent();
+ failRecordHandler.saveFailRecord(
+ correlationData.getId(),
+ getExchange(),
+ getRoutingKey(),
+ cause,
+ messageContent
+ );
+ } else {
+ log.warn("[MQ生产者] 未配置FailRecordHandler或非CustomCorrelationData类型, 无法保存失败记录");
+ }
+ }
+ }
+
+ @Override
+ public void returnedMessage(ReturnedMessage returned) {
+ log.error("[MQ生产者] 消息路由失败: exchange={}, routingKey={}, replyCode={}, replyText={}, message={}",
+ returned.getExchange(),
+ returned.getRoutingKey(),
+ returned.getReplyCode(),
+ returned.getReplyText(),
+ new String(returned.getMessage().getBody()));
+ if (failRecordHandler != null) {
+ failRecordHandler.saveFailRecord(
+ returned.getMessage().getMessageProperties().getCorrelationId(),
+ returned.getExchange(),
+ returned.getRoutingKey(),
+ "路由失败: " + returned.getReplyText(),
+ new String(returned.getMessage().getBody())
+ );
+ } else {
+ log.warn("[MQ生产者] 未配置FailRecordHandler, 无法保存失败记录");
+ }
+ }
+}
\ No newline at end of file
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/AbstractMessageRetryTask.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/AbstractMessageRetryTask.java
new file mode 100644
index 0000000..b8cf3a0
--- /dev/null
+++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/AbstractMessageRetryTask.java
@@ -0,0 +1,91 @@
+package com.tashow.cloud.mq.retry;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * 消息重试任务抽象实现
+ *
+ * @param 失败记录类型
+ * @author tashow
+ */
+public abstract class AbstractMessageRetryTask {
+
+ private static final Logger log = LoggerFactory.getLogger(AbstractMessageRetryTask.class);
+
+ /**
+ * 处理中状态码
+ */
+ public static final int STATUS_PROCESSING = 3;
+
+ /**
+ * 失败状态码
+ */
+ public static final int STATUS_FAILED = 4;
+
+ /**
+ * 获取消息重试服务
+ *
+ * @return 消息重试服务
+ */
+ protected abstract MessageRetryService getMessageRetryService();
+
+ /**
+ * 获取记录ID
+ *
+ * @param record 记录对象
+ * @return 记录ID
+ */
+ protected abstract String getRecordId(T record);
+
+ /**
+ * 获取关联ID
+ *
+ * @param record 记录对象
+ * @return 关联ID
+ */
+ protected abstract String getCorrelationId(T record);
+
+ /**
+ * 执行重试
+ */
+ public void retryFailedMessages() {
+ try {
+ List unprocessedRecords = getMessageRetryService().getUnprocessedRecords();
+ if (unprocessedRecords.isEmpty()) {
+ log.info("[MQ重试] 没有需要重试的消息");
+ return;
+ }
+
+ log.info("[MQ重试] 本次需要重试的消息数量: {}", unprocessedRecords.size());
+ for (T record : unprocessedRecords) {
+ try {
+ // 先将状态更新为处理中,避免其他实例重复处理
+ if (!getMessageRetryService().updateStatus(record, STATUS_PROCESSING)) {
+ continue; // 如果更新失败,跳过当前记录
+ }
+
+ // 执行重试
+ String recordId = getRecordId(record);
+ boolean success = getMessageRetryService().retryFailedMessage(recordId);
+
+ if (success) {
+ log.info("[MQ重试] 消息重试成功: {}", getCorrelationId(record));
+ } else {
+ log.warn("[MQ重试] 消息重试失败: {}", getCorrelationId(record));
+ getMessageRetryService().updateStatus(record, STATUS_FAILED);
+ }
+ } catch (Exception e) {
+ // 发生异常时,更新状态为失败
+ getMessageRetryService().updateStatus(record, STATUS_FAILED);
+ log.error("[MQ重试] 重试消息异常: {}", getCorrelationId(record), e);
+ }
+ }
+ log.info("[MQ重试] 消息重试任务完成");
+ } catch (Exception e) {
+ log.error("[MQ重试] 执行消息重试任务异常", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/MessageRetryService.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/MessageRetryService.java
new file mode 100644
index 0000000..0dc29b0
--- /dev/null
+++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/MessageRetryService.java
@@ -0,0 +1,36 @@
+package com.tashow.cloud.mq.retry;
+
+import java.util.List;
+
+/**
+ * 消息重试服务接口
+ *
+ * @param 失败记录类型
+ * @author tashow
+ */
+public interface MessageRetryService {
+
+ /**
+ * 获取未处理的失败记录
+ *
+ * @return 失败记录列表
+ */
+ List getUnprocessedRecords();
+
+ /**
+ * 重试失败消息
+ *
+ * @param recordId 记录ID
+ * @return 重试结果
+ */
+ boolean retryFailedMessage(String recordId);
+
+ /**
+ * 更新记录状态
+ *
+ * @param record 记录对象
+ * @param status 记录状态
+ * @return 更新结果
+ */
+ boolean updateStatus(T record, int status);
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/pom.xml b/tashow-module/tashow-module-app/pom.xml
index b4afe93..4de4e69 100644
--- a/tashow-module/tashow-module-app/pom.xml
+++ b/tashow-module/tashow-module-app/pom.xml
@@ -28,11 +28,27 @@
com.tashow.cloud
tashow-framework-rpc
-
+
+ com.tashow.cloud
+ tashow-data-mybatis
+
com.tashow.cloud
tashow-framework-web
+
+ com.tashow.cloud
+ tashow-framework-env
+
+
+ com.tashow.cloud
+ tashow-infra-api
+
+
+
+ com.tashow.cloud
+ tashow-framework-websocket
+
com.tashow.cloud
tashow-data-redis
@@ -41,5 +57,23 @@
com.tashow.cloud
tashow-framework-security
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ com.tashow.cloud
+ tashow-feishu-sdk
+ 1.0.0
+ compile
+
+
+ org.springframework.amqp
+ spring-rabbit
+
+
+ com.tashow.cloud
+ tashow-data-redis
+
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/AppServerApplication.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/AppServerApplication.java
index a74dc76..5099474 100644
--- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/AppServerApplication.java
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/AppServerApplication.java
@@ -2,14 +2,15 @@ package com.tashow.cloud.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
- * Hello world!
- *
+ * 应用服务启动类
*/
@SpringBootApplication
@EnableScheduling
+@ComponentScan(basePackages = {"com.tashow.cloud.app", "com.tashow.cloud.sdk.feishu"})
public class AppServerApplication {
public static void main(String[] args) {
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/AppConfig.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/AppConfig.java
new file mode 100644
index 0000000..6a1f2d3
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/AppConfig.java
@@ -0,0 +1,20 @@
+package com.tashow.cloud.app.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 应用配置类
+ */
+@Configuration
+public class AppConfig {
+
+ /**
+ * 提供ObjectMapper bean用于JSON处理
+ */
+ @Bean
+ public ObjectMapper objectMapper() {
+ return new ObjectMapper();
+ }
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeiShuClientConfig.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeiShuClientConfig.java
new file mode 100644
index 0000000..fa88d22
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeiShuClientConfig.java
@@ -0,0 +1,24 @@
+package com.tashow.cloud.app.config;
+
+import com.tashow.cloud.sdk.feishu.client.FeiShuAlertClient;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * 飞书客户端配置
+ * 用于初始化FeiShuAlertClient的相关依赖
+ */
+@Configuration
+public class FeiShuClientConfig {
+
+ @Autowired
+ private StringRedisTemplate stringRedisTemplate;
+
+
+ /* @PostConstruct
+ public void initFeiShuClient() {
+ FeiShuAlertClient.setRedisTemplate(stringRedisTemplate);
+ }*/
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeishuConfig.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeishuConfig.java
new file mode 100644
index 0000000..cf7478b
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeishuConfig.java
@@ -0,0 +1,26 @@
+package com.tashow.cloud.app.config;
+
+import com.tashow.cloud.app.service.feishu.FeiShuCardDataService;
+import com.tashow.cloud.sdk.feishu.client.FeiShuAlertClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import jakarta.annotation.PostConstruct;
+
+/**
+ * 飞书配置类
+ * 用于初始化飞书SDK与应用层的集成
+ */
+@Configuration
+public class FeishuConfig {
+
+ private final FeiShuAlertClient feiShuAlertClient;
+ private final FeiShuCardDataService feiShuCardDataService;
+
+ @Autowired
+ public FeishuConfig(FeiShuAlertClient feiShuAlertClient, FeiShuCardDataService feiShuCardDataService) {
+ this.feiShuAlertClient = feiShuAlertClient;
+ this.feiShuCardDataService = feiShuCardDataService;
+ }
+
+
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/FeishuController.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/FeishuController.java
new file mode 100644
index 0000000..8fc964f
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/FeishuController.java
@@ -0,0 +1,61 @@
+package com.tashow.cloud.app.controller;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.lark.oapi.core.utils.Decryptor;
+import com.tashow.cloud.app.service.feishu.FeiShuCardDataService;
+import com.tashow.cloud.sdk.feishu.client.FeiShuAlertClient;
+import com.tashow.cloud.sdk.feishu.config.LarkConfig;
+import jakarta.annotation.security.PermitAll;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import java.util.Map;
+
+@RestController
+public class FeishuController {
+ private final Logger log = LoggerFactory.getLogger(FeishuController.class);
+
+ private final FeiShuAlertClient feiShuAlertClient;
+ private final FeiShuCardDataService feiShuCardDataService;
+ private final LarkConfig larkConfig;
+
+ @Autowired
+ public FeishuController(FeiShuAlertClient feiShuAlertClient,
+ FeiShuCardDataService feiShuCardDataService,
+ LarkConfig larkConfig) {
+ this.feiShuAlertClient = feiShuAlertClient;
+ this.feiShuCardDataService = feiShuCardDataService;
+ this.larkConfig = larkConfig;
+
+ }
+
+ @RequestMapping("/card1")
+ @PermitAll
+ public String card(@RequestBody JSONObject data) {
+ try {
+ if (data.containsKey("app_id") && data.containsKey("action")) {
+ JSONObject action = data.getJSONObject("action");
+ JSONObject value = action.getJSONObject("value");
+ if (value != null && "complete_alarm".equals(value.getStr("action"))) {
+ String messageId = data.getStr("open_message_id");
+
+ Map templateData = feiShuCardDataService.getCardData(messageId);
+ log.info("从Redis获取的模板数据: {}", templateData);
+ return feiShuAlertClient.buildCardWithData("AAqdp4Mrvf2V9", templateData);
+ }
+ }
+ if (data.containsKey("encrypt")) {
+ Decryptor decryptor = new Decryptor(larkConfig.getEncryptKey());
+ String encrypt = decryptor.decrypt(data.getStr("encrypt"));
+ return encrypt;
+ }
+ return "{}";
+ } catch (Exception e) {
+ log.error("卡片处理异常", e);
+ return "{\"code\":1,\"msg\":\"处理异常: " + e.getMessage() + "\"}";
+ }
+ }
+}
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/TestController.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/TestController.java
index 35ce5c5..6770341 100644
--- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/TestController.java
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/TestController.java
@@ -4,7 +4,6 @@ import com.tashow.cloud.app.mq.producer.buriedPoint.BuriedPointProducer;
import jakarta.annotation.security.PermitAll;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
-
import java.util.HashMap;
import java.util.Map;
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/HttpTranslator.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/HttpTranslator.java
new file mode 100644
index 0000000..095b40e
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/HttpTranslator.java
@@ -0,0 +1,47 @@
+package com.tashow.cloud.app.ext;
+
+import com.lark.oapi.core.request.EventReq;
+import com.lark.oapi.core.response.EventResp;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class HttpTranslator {
+
+ private Map> toHeaderMap(HttpServletRequest req) {
+ Map> headers = new HashMap<>();
+ Enumeration names = req.getHeaderNames();
+ while (names.hasMoreElements()) {
+ String name = names.nextElement();
+ List values = Collections.list(req.getHeaders(name));
+ headers.put(name, values);
+ }
+ return headers;
+ }
+
+ public EventReq translate(HttpServletRequest request) throws IOException {
+ String bodyStr = request.getReader().lines()
+ .collect(Collectors.joining(System.lineSeparator()));
+ EventReq req = new EventReq();
+ req.setHeaders(toHeaderMap(request));
+ req.setBody(bodyStr.getBytes(StandardCharsets.UTF_8));
+ req.setHttpPath(request.getRequestURI());
+ return req;
+ }
+
+ public void write(HttpServletResponse response, EventResp eventResp) throws IOException {
+ response.setStatus(eventResp.getStatusCode());
+ eventResp.getHeaders().entrySet().stream().forEach(keyValues -> {
+ String key = keyValues.getKey();
+ List values = keyValues.getValue();
+ values.stream().forEach(v -> response.addHeader(key, v));
+ });
+ if (eventResp.getBody() != null) {
+ response.getWriter().write(new String(eventResp.getBody()));
+ }
+ }
+}
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/ServletAdapter.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/ServletAdapter.java
new file mode 100644
index 0000000..1fcfbd5
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/ServletAdapter.java
@@ -0,0 +1,57 @@
+package com.tashow.cloud.app.ext;
+
+import com.lark.oapi.card.CardActionHandler;
+import com.lark.oapi.core.request.EventReq;
+import com.lark.oapi.core.response.EventResp;
+import com.lark.oapi.event.EventDispatcher;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+
+/**
+ * Servlet的适配器,用于适配基于Servlet技术栈实现的Web服务
+ */
+public class ServletAdapter {
+
+ private static final HttpTranslator HTTP_TRANSLATOR = new HttpTranslator();
+
+ /**
+ * 处理消息事件
+ *
+ * @param req
+ * @param response
+ * @param eventDispatcher
+ * @throws Throwable
+ */
+ public void handleEvent(HttpServletRequest req, HttpServletResponse response,
+ EventDispatcher eventDispatcher) throws Throwable {
+ // 转换请求对象
+ EventReq eventReq = HTTP_TRANSLATOR.translate(req);
+
+ // 处理请求
+ EventResp resp = eventDispatcher.handle(eventReq);
+
+ // 回写结果
+ HTTP_TRANSLATOR.write(response, resp);
+ }
+
+ /**
+ * 处理卡片消息
+ *
+ * @param req
+ * @param response
+ * @param handler
+ * @throws Throwable
+ */
+ public void handleCardAction(HttpServletRequest req, HttpServletResponse response,
+ CardActionHandler handler) throws Throwable {
+ // 转换请求对象
+ EventReq eventReq = HTTP_TRANSLATOR.translate(req);
+
+ // 处理请求
+ EventResp resp = handler.handle(eventReq);
+
+ // 回写结果
+ HTTP_TRANSLATOR.write(response, resp);
+ }
+}
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/interceptor/BuriedPointInterceptor.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/interceptor/BuriedPointInterceptor.java
index 5be550d..5616d25 100644
--- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/interceptor/BuriedPointInterceptor.java
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/interceptor/BuriedPointInterceptor.java
@@ -47,11 +47,9 @@ public class BuriedPointInterceptor implements HandlerInterceptor {
String method = request.getMethod() + " " + request.getRequestURI()+ JsonUtils.toJsonString(request.getParameterMap());
String controllerName = handlerMethod.getBeanType().getSimpleName();
String actionName = handlerMethod.getMethod().getName();
-
-
BuriedMessages message = new BuriedMessages();
message.setId(requestId);
- message.setEventTime(System.currentTimeMillis());
+ message.setEventTime(new java.util.Date());
message.setService(SpringUtils.getApplicationName());
message.setMethod(method);
message.setUserId(getUserId(request));
@@ -60,7 +58,6 @@ public class BuriedPointInterceptor implements HandlerInterceptor {
message.setServerIp(getServerIp());
message.setEventType("API_REQUEST_START");
message.setPagePath(controllerName + "#" + actionName);
- message.setUserAgent(request.getHeader("User-Agent"));
message.setStatusCode(BuriedMessages.STATUS_PROCESSING);
buriedPointProducer.asyncSendMessage(message);
if (log.isDebugEnabled()) {
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mapper/BuriedPointFailRecordMapper.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mapper/BuriedPointFailRecordMapper.java
new file mode 100644
index 0000000..87236ee
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mapper/BuriedPointFailRecordMapper.java
@@ -0,0 +1,12 @@
+package com.tashow.cloud.app.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tashow.cloud.app.model.BuriedPointFailRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 埋点消息发送失败记录Mapper接口
+ */
+@Mapper
+public interface BuriedPointFailRecordMapper extends BaseMapper {
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPoint.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPoint.java
index ad5b745..435621e 100644
--- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPoint.java
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPoint.java
@@ -33,7 +33,7 @@ public class BuriedPoint {
*/
@TableField(value = "event_time")
private Long eventTime;
-
+
/**
* 服务名称
*/
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPointFailRecord.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPointFailRecord.java
new file mode 100644
index 0000000..7ef0be3
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPointFailRecord.java
@@ -0,0 +1,72 @@
+package com.tashow.cloud.app.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 埋点消息发送失败记录实体类
+ */
+@Data
+@TableName("buried_point_fail_record")
+public class BuriedPointFailRecord {
+
+ /**
+ * 状态常量定义
+ */
+ public static final int STATUS_UNPROCESSED = 0; // 未处理
+ public static final int STATUS_PROCESSING = 1; // 处理中
+ public static final int STATUS_SUCCESS = 2; // 处理成功
+ public static final int STATUS_FAILED = 3; // 处理失败
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ /**
+ * 消息关联ID
+ */
+ private String correlationId;
+
+ /**
+ * 交换机名称
+ */
+ private String exchange;
+
+ /**
+ * 路由键
+ */
+ private String routingKey;
+
+ /**
+ * 失败原因
+ */
+ private String cause;
+
+ /**
+ * 消息内容
+ */
+ private String messageContent;
+
+ /**
+ * 重试次数
+ */
+ private Integer retryCount;
+
+ /**
+ * 状态:0-未处理,1-处理中,2-处理成功,3-处理失败
+ */
+ private Integer status;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+ /**
+ * 更新时间
+ */
+ private Date updateTime;
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/config/BuriedPointConfiguration.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/config/BuriedPointConfiguration.java
index 6cd9ca7..35a8eca 100644
--- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/config/BuriedPointConfiguration.java
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/config/BuriedPointConfiguration.java
@@ -1,21 +1,15 @@
package com.tashow.cloud.app.mq.config;
+
import com.tashow.cloud.app.interceptor.BuriedPointInterceptor;
-import com.tashow.cloud.app.mapper.BuriedPointFailRecordMapper;
import com.tashow.cloud.app.mq.message.BuriedMessages;
-import com.tashow.cloud.app.model.BuriedPointFailRecord;
import com.tashow.cloud.app.mq.producer.buriedPoint.BuriedPointProducer;
-import com.tashow.cloud.app.mq.producer.buriedPoint.CustomCorrelationData;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
-import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import lombok.extern.slf4j.Slf4j;
-import jakarta.annotation.PostConstruct;
-import java.util.Date;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
/**
* 埋点功能配置类
@@ -26,100 +20,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
public class BuriedPointConfiguration implements WebMvcConfigurer {
private final BuriedPointProducer buriedPointProducer;
- private final RabbitTemplate rabbitTemplate;
- private final BuriedPointFailRecordMapper buriedPointFailRecordMapper;
-
- /**
- * RabbitTemplate初始化配置
- */
- @PostConstruct
- public RabbitTemplate initRabbitTemplate() {
- log.info("[埋点配置] 初始化RabbitTemplate: {}", rabbitTemplate);
- rabbitTemplate.setMandatory(true);
- rabbitTemplate.setReturnsCallback(returned -> {
- log.error("[埋点配置] 消息路由失败: exchange={}, routingKey={}, replyCode={}, replyText={}, ={}",
- returned.getExchange(),
- returned.getRoutingKey(),
- returned.getReplyCode(),
- returned.getReplyText(),
- new String(returned.getMessage().getBody()));
-
- saveFailRecord(
- returned.getMessage().getMessageProperties().getCorrelationId(),
- returned.getExchange(),
- returned.getRoutingKey(),
- "路由失败: " + returned.getReplyText(),
- new String(returned.getMessage().getBody())
- );
- });
- rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
- if (ack) {
- log.info("[埋点配置] 消息成功发送到交换机: {}", correlationData.getId());
- } else {
- log.error("[埋点配置] 消息发送到交换机失败: cause={}, correlationData={}", cause, correlationData);
- CustomCorrelationData customData = (CustomCorrelationData) correlationData;
- String messageContent = customData.getMessageContent();
- saveFailRecord(
- correlationData.getId(),
- BuriedMessages.EXCHANGE,
- BuriedMessages.ROUTING_KEY,
- cause,
- messageContent
- );
- }
- });
- if (rabbitTemplate.isConfirmListener()) {
- log.info("[埋点配置] 确认回调已正确配置");
- } else {
- log.error("[埋点配置] 确认回调配置失败");
- }
- return rabbitTemplate;
- }
/**
- * 保存消息发送失败记录
+ * 创建埋点队列
*/
- private void saveFailRecord(String correlationId, String exchange, String routingKey, String cause, String messageContent) {
- try {
- log.info("[埋点配置] 保存发送失败记录: correlationId={}", correlationId);
-
- // 先查询是否已存在记录
- LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
- queryWrapper.eq(BuriedPointFailRecord::getCorrelationId, correlationId);
- BuriedPointFailRecord existingRecord = buriedPointFailRecordMapper.selectOne(queryWrapper);
-
- if (existingRecord != null) {
- // 已存在记录,执行更新
- log.info("[埋点配置] 发现已有失败记录,将更新: {}", correlationId);
- existingRecord.setExchange(exchange);
- existingRecord.setRoutingKey(routingKey);
- existingRecord.setCause(cause);
- existingRecord.setMessageContent(messageContent);
- existingRecord.setStatus(BuriedPointFailRecord.STATUS_UNPROCESSED);
- existingRecord.setUpdateTime(new Date());
- buriedPointFailRecordMapper.updateById(existingRecord);
- log.info("[埋点配置] 发送失败记录已更新: correlationId={}", correlationId);
- } else {
- // 不存在记录,执行插入
- BuriedPointFailRecord failRecord = new BuriedPointFailRecord();
- failRecord.setCorrelationId(correlationId);
- failRecord.setExchange(exchange);
- failRecord.setRoutingKey(routingKey);
- failRecord.setCause(cause);
- failRecord.setMessageContent(messageContent);
- failRecord.setRetryCount(0);
- failRecord.setStatus(BuriedPointFailRecord.STATUS_UNPROCESSED);
- failRecord.setCreateTime(new Date());
- failRecord.setUpdateTime(new Date());
-
- buriedPointFailRecordMapper.insert(failRecord);
- log.info("[埋点配置] 发送失败记录已保存: correlationId={}", correlationId);
- }
- } catch (Exception e) {
- log.error("[埋点配置] 保存发送失败记录异常", e);
- }
- }
-
@Bean
public Queue buriedPointQueue() {
return new Queue(BuriedMessages.QUEUE, true, false, false);
@@ -170,4 +74,4 @@ public class BuriedPointConfiguration implements WebMvcConfigurer {
"/error"
);
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/consumer/buriedPoint/BuriedPointConsumer.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/consumer/buriedPoint/BuriedPointConsumer.java
index 9c7e035..03a8734 100644
--- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/consumer/buriedPoint/BuriedPointConsumer.java
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/consumer/buriedPoint/BuriedPointConsumer.java
@@ -1,90 +1,70 @@
package com.tashow.cloud.app.mq.consumer.buriedPoint;
+
+import com.tashow.cloud.sdk.feishu.client.FeiShuAlertClient;
import com.tashow.cloud.app.mapper.BuriedPointMapper;
import com.tashow.cloud.app.mapper.BuriedPointFailRecordMapper;
import com.tashow.cloud.app.mq.message.BuriedMessages;
import com.tashow.cloud.app.model.BuriedPoint;
import com.tashow.cloud.app.model.BuriedPointFailRecord;
+import com.tashow.cloud.sdk.feishu.config.LarkConfig;
+import com.tashow.cloud.sdk.feishu.util.ChartImageGenerator;
+import com.rabbitmq.client.Channel;
+import com.tashow.cloud.mq.rabbitmq.consumer.AbstractRabbitMQConsumer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
-import java.util.Date;
-import com.rabbitmq.client.Channel;
+import java.text.SimpleDateFormat;
+import java.util.*;
import org.springframework.dao.DuplicateKeyException;
import com.tashow.cloud.common.util.json.JsonUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+/**
+ * 埋点消息消费者
+ */
@Component
@RabbitListener(queues = BuriedMessages.QUEUE)
@Slf4j
@RequiredArgsConstructor
-public class BuriedPointConsumer {
+public class BuriedPointConsumer extends AbstractRabbitMQConsumer {
private final BuriedPointMapper buriedPointMapper;
private final BuriedPointFailRecordMapper buriedPointFailRecordMapper;
-
+ private final FeiShuAlertClient feiShuAlertClient;
+ private final LarkConfig larkConfig;
+
@Value("${spring.application.name:tashow-app}")
private String applicationName;
- private static final int MAX_RETRY_ALLOWED = 1;
-
- @RabbitHandler
- public void onMessage(BuriedMessages message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
- Integer dbRetryCount = getActualRetryCount(message);
- message.setRetryCount(dbRetryCount);
- if (message.getRetryCount() != null && message.getRetryCount() >= MAX_RETRY_ALLOWED) {
- message.setStatusCode(BuriedMessages.STATUS_ERROR);
- message.addExtraData("errorMessage", "已达到最大重试次数");
- saveToFailRecord(message, "已达到最大重试次数");
- safeChannelAck(channel, deliveryTag);
- return;
- }
-
- log.info("[埋点消费者] 收到埋点消息: {}, 当前重试次数: {}/{}", message, message.getRetryCount(), MAX_RETRY_ALLOWED);
-
- message.setStatusCode(BuriedMessages.STATUS_PROCESSING);
- log.info("[埋点消费者] 消息状态更新为处理中(STATUS_PROCESSING): {}", message.getId());
- try {
- /* if(true){
- throw new RuntimeException("测试异常");
- }*/
- saveToDatabase(message);
- message.setStatusCode(BuriedMessages.STATUS_SUCCESS);
- updateMessageStatus(message);
- log.info("[埋点消费者] 消息处理成功,状态已更新为成功(STATUS_SUCCESS): {}", message.getId());
- safeChannelAck(channel, deliveryTag);
- } catch (DuplicateKeyException e) {
- log.warn("[埋点消费者] 消息已被处理过,直接确认: {}, 错误: {}", message.getId(), e.getMessage());
- safeChannelAck(channel, deliveryTag);
- } catch (Exception e) {
- message.setStatusCode(BuriedMessages.STATUS_ERROR);
- message.addExtraData("errorMessage", e.getMessage());
- log.error("[埋点消费者] 消息处理失败: {}, 错误: {}", message.getId(), e.getMessage());
- message.incrementRetryCount();
- updateRetryCount(message);
- if (message.getRetryCount() >= MAX_RETRY_ALLOWED) {
- saveToDatabase(message);
- log.warn("[埋点消费者] 消息已达到最大重试次数: {}, 确认消息并保存到失败记录表", message.getRetryCount());
- saveToFailRecord(message, e.getMessage());
- safeChannelAck(channel, deliveryTag);
- } else {
- log.info("[埋点消费者] 消息将重新入队重试: {}, 当前重试次数: {}", message.getId(), message.getRetryCount());
- safeChannelNack(channel, deliveryTag, false, true);
- }
- }
+ @Override
+ public int getMaxRetryAllowed() {
+ return 1;
}
- private Integer getActualRetryCount(BuriedMessages message) {
+ @RabbitHandler
+ public void handleMessage(BuriedMessages message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
+ onMessage(message, channel, deliveryTag);
+ }
+
+ @Override
+ public boolean processMessage(BuriedMessages message) {
+ // 消息处理
+ return saveToDatabase(message);
+ }
+
+ @Override
+ public Integer getRetryCount(BuriedMessages message) {
try {
BuriedPoint buriedPoint = buriedPointMapper.selectByEventId(message.getId());
if (buriedPoint != null && buriedPoint.getRetryCount() != null) {
- if ((buriedPoint.getStatus() == BuriedMessages.STATUS_ERROR ||
+ if ((buriedPoint.getStatus() == BuriedMessages.STATUS_ERROR ||
buriedPoint.getStatus() == BuriedMessages.STATUS_PROCESSING)) {
- log.info("[埋点消费者] 检测到消息可能因服{}", message.getId());
return buriedPoint.getRetryCount() - 1;
}
return buriedPoint.getRetryCount();
@@ -93,32 +73,33 @@ public class BuriedPointConsumer {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(BuriedPointFailRecord::getCorrelationId, correlationId);
BuriedPointFailRecord failRecord = buriedPointFailRecordMapper.selectOne(queryWrapper);
- return failRecord!=null? failRecord.getRetryCount():0;
-
+ return failRecord != null ? failRecord.getRetryCount() : 0;
}
} catch (Exception e) {
log.warn("[埋点消费者] 获取消息重试次数失败: {}", e.getMessage());
- throw new RuntimeException("获取消息重试次数失败", e);
+ return 0;
}
}
- private void safeChannelAck(Channel channel, long deliveryTag) {
+ @Override
+ public void updateMessageStatus(BuriedMessages message) {
try {
- channel.basicAck(deliveryTag, false);
+ BuriedPoint buriedPoint = buriedPointMapper.selectByEventId(message.getId());
+ if (buriedPoint != null) {
+ buriedPoint.setStatus(message.getStatusCode());
+ buriedPoint.setUpdateTime(new Date());
+ buriedPoint.setRetryCount(message.getRetryCount());
+ buriedPointMapper.updateById(buriedPoint);
+ log.debug("[埋点消费者] 已更新埋点状态, 事件ID: {}, 新状态: {}, 重试次数: {}",
+ message.getId(), message.getStatusCode(), message.getRetryCount());
+ }
} catch (Exception e) {
- log.error("[埋点消费者] 确认消息失败: {}", e.getMessage());
+ log.error("[埋点消费者] 更新埋点状态失败: {}, 错误: {}", message.getId(), e.getMessage(), e);
}
}
- private void safeChannelNack(Channel channel, long deliveryTag, boolean multiple, boolean requeue) {
- try {
- channel.basicNack(deliveryTag, multiple, requeue);
- } catch (Exception e) {
- log.error("[埋点消费者] 拒绝消息失败: {}", e.getMessage());
- }
- }
-
- private void updateRetryCount(BuriedMessages message) {
+ @Override
+ public void updateRetryCount(BuriedMessages message) {
try {
BuriedPoint buriedPoint = buriedPointMapper.selectByEventId(message.getId());
if (buriedPoint != null) {
@@ -136,9 +117,7 @@ public class BuriedPointConsumer {
failRecord.setMessageContent(JsonUtils.toJsonString(message));
buriedPointFailRecordMapper.updateById(failRecord);
} else {
- // 记录或创建新的失败记录
- log.warn("[埋点消费者] 未找到埋点记录和失败记录, 事件ID: {}, 准备创建失败记录", message.getId());
- saveToFailRecord(message, "未找到原始埋点记录");
+ saveToFailRecord(message, "");
}
}
} catch (Exception e) {
@@ -146,20 +125,8 @@ public class BuriedPointConsumer {
}
}
- private void updateMessageStatus(BuriedMessages message) {
- try {
- BuriedPoint buriedPoint = buriedPointMapper.selectByEventId(message.getId());
- buriedPoint.setStatus(message.getStatusCode());
- buriedPoint.setUpdateTime(new Date());
- buriedPoint.setRetryCount(message.getRetryCount());
- buriedPointMapper.updateById(buriedPoint);
- log.debug("[埋点消费者] 已更新埋点状态, 事件ID: {}, 新状态: {}, 重试次数: {}", message.getId(), message.getStatusCode(), message.getRetryCount());
- } catch (Exception e) {
- log.error("[埋点消费者] 更新埋点状态失败: {}, 错误: {}", message.getId(), e.getMessage(), e);
- }
- }
-
- private boolean saveToDatabase(BuriedMessages message) {
+ @Override
+ public boolean saveToDatabase(BuriedMessages message) {
try {
log.debug("[埋点消费者] 准备保存埋点数据,事件ID: {}", message.getId());
BuriedPoint existingPoint = buriedPointMapper.selectByEventId(message.getId());
@@ -174,10 +141,11 @@ public class BuriedPointConsumer {
int result = buriedPointMapper.updateById(existingPoint);
return result > 0;
}
-
+
BuriedPoint buriedPoint = new BuriedPoint();
buriedPoint.setEventId(message.getId());
- buriedPoint.setEventTime(message.getEventTime());
+ buriedPoint.setEventTime(System.currentTimeMillis());
+
buriedPoint.setUserId(message.getUserId());
buriedPoint.setEventType(message.getEventType());
buriedPoint.setService(applicationName);
@@ -192,31 +160,37 @@ public class BuriedPointConsumer {
buriedPoint.setDuration(message.getDuration());
buriedPoint.setCreateTime(new Date());
buriedPoint.setUpdateTime(new Date());
- log.debug("[埋点消费者] 埋点实体数据: eventId={}, eventType={}, userId={}, service={}, method={}, status={}, retryCount={}", buriedPoint.getEventId(), buriedPoint.getEventType(), buriedPoint.getUserId(), buriedPoint.getService(), buriedPoint.getMethod(), buriedPoint.getStatus(), buriedPoint.getRetryCount());
+
+ log.debug("[埋点消费者] 埋点实体数据: eventId={}, eventType={}, userId={}, service={}, method={}, status={}, retryCount={}",
+ buriedPoint.getEventId(), buriedPoint.getEventType(), buriedPoint.getUserId(),
+ buriedPoint.getService(), buriedPoint.getMethod(), buriedPoint.getStatus(),
+ buriedPoint.getRetryCount());
+
buriedPointMapper.insert(buriedPoint);
log.info("[埋点消费者] 埋点数据已保存到数据库, 事件ID: {}, 状态: {}", message.getId(), message.getStatusCode());
return true;
+ } catch (DuplicateKeyException e) {
+ log.warn("[埋点消费者] 埋点数据已存在, 事件ID: {}", message.getId());
+ return true; // 数据已存在也视为成功
} catch (Exception e) {
log.error("[埋点消费者] 保存埋点数据到数据库失败, 事件ID: {}, 错误: {}", message.getId(), e.getMessage(), e);
throw e;
}
}
- /**
- * 保存失败记录到BuriedPointFailRecord表
- */
- private void saveToFailRecord(BuriedMessages message, String cause) {
+ @Override
+ public void saveToFailRecord(BuriedMessages message, String cause) {
try {
String correlationId = String.valueOf(message.getId());
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(BuriedPointFailRecord::getCorrelationId, correlationId);
BuriedPointFailRecord existingRecord = buriedPointFailRecordMapper.selectOne(queryWrapper);
-
+
if (existingRecord != null) {
log.info("[埋点消费者] 发现已有失败记录,将更新: {}", correlationId);
existingRecord.setExchange(BuriedMessages.EXCHANGE);
existingRecord.setRoutingKey(BuriedMessages.ROUTING_KEY);
- existingRecord.setCause(cause);
+ existingRecord.setCause(message.getErrorMessage()+cause);
existingRecord.setMessageContent(JsonUtils.toJsonString(message));
existingRecord.setRetryCount(message.getRetryCount());
existingRecord.setStatus(BuriedPointFailRecord.STATUS_UNPROCESSED);
@@ -228,7 +202,7 @@ public class BuriedPointConsumer {
failRecord.setCorrelationId(correlationId);
failRecord.setExchange(BuriedMessages.EXCHANGE);
failRecord.setRoutingKey(BuriedMessages.ROUTING_KEY);
- failRecord.setCause(cause);
+ failRecord.setCause(message.getErrorMessage()+cause);
failRecord.setMessageContent(JsonUtils.toJsonString(message));
failRecord.setRetryCount(message.getRetryCount());
failRecord.setStatus(BuriedPointFailRecord.STATUS_UNPROCESSED);
@@ -236,9 +210,99 @@ public class BuriedPointConsumer {
failRecord.setUpdateTime(new Date());
buriedPointFailRecordMapper.insert(failRecord);
log.info("[埋点消费者] 已将失败消息保存到失败记录表, 事件ID: {}", message.getId());
+
+ // 查询最近12小时的失败记录数量
+ checkFailRecordsAndAlert();
}
} catch (Exception e) {
log.error("[埋点消费者] 保存失败记录失败: {}, 错误: {}", message.getId(), e.getMessage(), e);
}
}
+
+ /**
+ * 检查失败记录数量并发送告警
+ */
+ private void checkFailRecordsAndAlert() {
+ try {
+ Date now = new Date();
+ Date twelveHoursAgo = new Date(now.getTime() - 12 * 60 * 60 * 1000L);
+ LambdaQueryWrapper failRecordQuery = new LambdaQueryWrapper<>();
+ failRecordQuery.ge(BuriedPointFailRecord::getCreateTime, twelveHoursAgo)
+ .le(BuriedPointFailRecord::getCreateTime, now)
+ .eq(BuriedPointFailRecord::getStatus, BuriedPointFailRecord.STATUS_UNPROCESSED);
+
+ Long failCountLast12Hours = buriedPointFailRecordMapper.selectCount(failRecordQuery);
+ log.warn("[埋点配置] 最近12小时埋点失败数量: {}", failCountLast12Hours);
+
+ // 如果失败数量过多,记录警告日志
+ if (failCountLast12Hours > 3) {
+ // 查询最近12小时的埋点失败数据,按小时统计
+ List monitoringData = queryHourlyFailRecordData(twelveHoursAgo, now);
+
+ try {
+ // 发送飞书告警消息
+ feiShuAlertClient.sendBuriedPointAlertMessage(larkConfig.getChatId(),
+ monitoringData,
+ failCountLast12Hours.intValue(),
+ "埋点处理异常,请检查系统");
+ } catch (Exception e) {
+ log.error("[埋点配置] 发送飞书告警失败", e);
+ }
+ log.error("[埋点配置] 警告:最近12小时埋点失败数量过多,请检查系统!失败数量: {}", failCountLast12Hours);
+ }
+ } catch (Exception e) {
+ log.error("[埋点配置] 检查失败记录数量异常", e);
+ }
+ }
+
+ /**
+ * 查询失败记录数据,按小时统计
+ */
+ private List queryHourlyFailRecordData(Date startDate, Date endDate) {
+ List result = new ArrayList<>();
+
+ try {
+ // 只取最近12个小时的数据
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(endDate);
+ calendar.add(Calendar.HOUR_OF_DAY, -12);
+ Date twelveHoursAgo = calendar.getTime();
+
+ SimpleDateFormat sdf = new SimpleDateFormat("HH:00");
+
+ // 从12小时前开始,每小时一个数据点
+ for (int i = 0; i < 12; i++) {
+ calendar.setTime(twelveHoursAgo);
+ calendar.add(Calendar.HOUR_OF_DAY, i);
+ Date currentHourStart = calendar.getTime();
+ calendar.add(Calendar.HOUR_OF_DAY, 1);
+ Date nextHourStart = calendar.getTime();
+
+ // 查询处理成功的记录数量
+ LambdaQueryWrapper successQuery = new LambdaQueryWrapper<>();
+ successQuery.ge(BuriedPointFailRecord::getCreateTime, currentHourStart)
+ .lt(BuriedPointFailRecord::getCreateTime, nextHourStart)
+ .eq(BuriedPointFailRecord::getStatus, BuriedPointFailRecord.STATUS_SUCCESS); // 处理成功
+ Long successCount = buriedPointFailRecordMapper.selectCount(successQuery);
+
+ // 查询处理失败或未处理的记录数量
+ LambdaQueryWrapper failQuery = new LambdaQueryWrapper<>();
+ failQuery.ge(BuriedPointFailRecord::getCreateTime, currentHourStart)
+ .lt(BuriedPointFailRecord::getCreateTime, nextHourStart)
+ .in(BuriedPointFailRecord::getStatus,
+ Arrays.asList(BuriedPointFailRecord.STATUS_UNPROCESSED, BuriedPointFailRecord.STATUS_FAILED)); // 未处理或处理失败
+ Long failCount = buriedPointFailRecordMapper.selectCount(failQuery);
+
+ // 添加到结果列表,无论是否有数据
+ String hourLabel = sdf.format(currentHourStart);
+ result.add(new ChartImageGenerator.MonitoringDataPoint(hourLabel, successCount.intValue(), failCount.intValue()));
+ }
+
+ return result;
+ } catch (Exception e) {
+ log.error("[埋点配置] 查询每小时失败记录数据失败", e);
+ // 返回空列表
+ return Collections.emptyList();
+ }
+ }
}
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/handler/BuriedPointFailRecordHandler.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/handler/BuriedPointFailRecordHandler.java
new file mode 100644
index 0000000..524ddf0
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/handler/BuriedPointFailRecordHandler.java
@@ -0,0 +1,156 @@
+package com.tashow.cloud.app.mq.handler;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.tashow.cloud.app.mapper.BuriedPointFailRecordMapper;
+import com.tashow.cloud.app.model.BuriedPointFailRecord;
+import com.tashow.cloud.mq.handler.FailRecordHandler;
+import com.tashow.cloud.sdk.feishu.client.FeiShuAlertClient;
+import com.tashow.cloud.sdk.feishu.config.LarkConfig;
+import com.tashow.cloud.sdk.feishu.util.ChartImageGenerator;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 埋点失败记录处理器
+ *
+ * @author tashow
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class BuriedPointFailRecordHandler implements FailRecordHandler {
+
+
+ @Autowired
+ private BuriedPointFailRecordMapper buriedPointFailRecordMapper;
+ @Autowired
+ FeiShuAlertClient feiShuAlertClient;
+ @Autowired
+ LarkConfig larkConfig;
+ /**
+ * 保存消息发送失败记录
+ */
+ @Override
+ public void saveFailRecord(String correlationId, String exchange, String routingKey, String cause, String messageContent) {
+ try {
+ log.info("[埋点处理器] 保存发送失败记录: correlationId={}", correlationId);
+ // 先查询是否已存在记录
+ LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
+ queryWrapper.eq(BuriedPointFailRecord::getCorrelationId, correlationId);
+ BuriedPointFailRecord existingRecord = buriedPointFailRecordMapper.selectOne(queryWrapper);
+ if (existingRecord != null) {
+ log.info("[埋点处理器] 发现已有失败记录,将更新: {}", correlationId);
+ existingRecord.setExchange(exchange);
+ existingRecord.setRoutingKey(routingKey);
+ existingRecord.setCause(cause);
+ existingRecord.setMessageContent(messageContent);
+ existingRecord.setStatus(BuriedPointFailRecord.STATUS_UNPROCESSED);
+ existingRecord.setUpdateTime(new Date());
+ buriedPointFailRecordMapper.updateById(existingRecord);
+ log.info("[埋点处理器] 发送失败记录已更新: correlationId={}", correlationId);
+ } else {
+ BuriedPointFailRecord failRecord = new BuriedPointFailRecord();
+ failRecord.setCorrelationId(correlationId);
+ failRecord.setExchange(exchange);
+ failRecord.setRoutingKey(routingKey);
+ failRecord.setCause(cause);
+ failRecord.setMessageContent(messageContent);
+ failRecord.setRetryCount(0);
+ failRecord.setStatus(BuriedPointFailRecord.STATUS_UNPROCESSED);
+ failRecord.setCreateTime(new Date());
+ failRecord.setUpdateTime(new Date());
+ buriedPointFailRecordMapper.insert(failRecord);
+ log.info("[埋点处理器] 发送失败记录已保存: correlationId={}", correlationId);
+ checkAlertThreshold(cause);
+ }
+ } catch (Exception e) {
+ log.error("[埋点处理器] 保存发送失败记录异常", e);
+ }
+ }
+
+ /**
+ * 检查是否达到告警阈值
+ */
+ @Override
+ public boolean checkAlertThreshold(String cause) {
+ try {
+ Date now = new Date();
+ Date twelveHoursAgo = new Date(now.getTime() - 12 * 60 * 60 * 1000L);
+ LambdaQueryWrapper failRecordQuery = new LambdaQueryWrapper<>();
+ failRecordQuery.ge(BuriedPointFailRecord::getCreateTime, twelveHoursAgo).le(BuriedPointFailRecord::getCreateTime, now).eq(BuriedPointFailRecord::getStatus, BuriedPointFailRecord.STATUS_UNPROCESSED);
+ Long failCountLast12Hours = buriedPointFailRecordMapper.selectCount(failRecordQuery);
+ // 如果失败数量过多,记录警告日志
+ if (failCountLast12Hours > 3) {
+ List monitoringData = queryHourlyFailRecordData(twelveHoursAgo, now);
+ try {
+ // 发送飞书告警消息
+ feiShuAlertClient.sendBuriedPointAlertMessage(larkConfig.getChatId(), monitoringData, failCountLast12Hours.intValue(), cause);
+ } catch (Exception e) {
+ log.error("[埋点处理器] 发送飞书告警失败", e);
+ }
+ return true;
+ }
+ return false;
+ } catch (Exception e) {
+ log.error("[埋点处理器] 检查告警阈值异常", e);
+ return false;
+ }
+ }
+
+ /**
+ * 查询失败记录数据,按小时统计
+ * 仅查询最近12个小时的数据
+ */
+ private List queryHourlyFailRecordData(Date startDate, Date endDate) {
+ List result = new ArrayList<>();
+
+ try {
+ // 只取最近12个小时的数据
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(endDate);
+ calendar.add(Calendar.HOUR_OF_DAY, -12);
+ Date twelveHoursAgo = calendar.getTime();
+
+ SimpleDateFormat sdf = new SimpleDateFormat("HH:00");
+
+ // 从12小时前开始,每小时一个数据点
+ for (int i = 0; i < 12; i++) {
+ calendar.setTime(twelveHoursAgo);
+ calendar.add(Calendar.HOUR_OF_DAY, i);
+ Date currentHourStart = calendar.getTime();
+ calendar.add(Calendar.HOUR_OF_DAY, 1);
+ Date nextHourStart = calendar.getTime();
+
+ // 查询处理成功的记录数量
+ LambdaQueryWrapper successQuery = new LambdaQueryWrapper<>();
+ successQuery.ge(BuriedPointFailRecord::getCreateTime, currentHourStart).lt(BuriedPointFailRecord::getCreateTime, nextHourStart).eq(BuriedPointFailRecord::getStatus, BuriedPointFailRecord.STATUS_SUCCESS); // 处理成功
+ Long successCount = buriedPointFailRecordMapper.selectCount(successQuery);
+
+ // 查询处理失败或未处理的记录数量
+ LambdaQueryWrapper failQuery = new LambdaQueryWrapper<>();
+ failQuery.ge(BuriedPointFailRecord::getCreateTime, currentHourStart).lt(BuriedPointFailRecord::getCreateTime, nextHourStart).in(BuriedPointFailRecord::getStatus, Arrays.asList(BuriedPointFailRecord.STATUS_UNPROCESSED, BuriedPointFailRecord.STATUS_FAILED)); // 未处理或处理失败
+ Long failCount = buriedPointFailRecordMapper.selectCount(failQuery);
+
+ // 添加到结果列表,无论是否有数据
+ String hourLabel = sdf.format(currentHourStart);
+ result.add(new ChartImageGenerator.MonitoringDataPoint(hourLabel, successCount.intValue(), failCount.intValue()));
+ }
+
+ return result;
+ } catch (Exception e) {
+ log.error("[埋点处理器] 查询每小时失败记录数据失败", e);
+ // 返回空列表
+ return Collections.emptyList();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/message/BuriedMessages.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/message/BuriedMessages.java
index 7a072e1..fa54a12 100644
--- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/message/BuriedMessages.java
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/message/BuriedMessages.java
@@ -1,67 +1,99 @@
package com.tashow.cloud.app.mq.message;
+
+import com.tashow.cloud.mq.core.BaseMqMessage;
import lombok.Data;
-import java.io.Serializable;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.Date;
+
+/**
+ * 埋点消息
+ */
@Data
-public class BuriedMessages implements Serializable {
+public class BuriedMessages extends BaseMqMessage {
- private static final long serialVersionUID = 1L; // 添加序列化ID
-
- // 消息队列配置
- public static final String QUEUE = "BURIED_POINT_QUEUE";
- public static final String EXCHANGE = "BURIED_POINT_EXCHANGE";
- public static final String ROUTING_KEY = "BURIED_POINT_ROUTING_KEY";
-
- // 状态码定义
- public static final Integer STATUS_INIT = 10; // 初始状态
- public static final Integer STATUS_PROCESSING = 20; // 处理中
- public static final Integer STATUS_SUCCESS = 30; // 处理成功
- public static final Integer STATUS_WARNING = 40; // 处理警告
- public static final Integer STATUS_ERROR = 50; // 处理错误
-
-
- private Integer id; // 事件唯一ID
- private Long eventTime; // 事件时间戳
- private String service; // 服务名称
- private String method; // 方法/接口
- private String userId; // 用户标识
- private String sessionId; // 会话标识
- private String clientIp; // 客户端IP
- private String serverIp; // 服务器IP
-
- private String eventType; // 事件类型: PAGE_VIEW, API_CALL, BUTTON_CLICK 等
- private String pagePath; // 页面路径/功能模块
- private String elementId; // 元素标识
- private Long duration; // 操作时长(毫秒)
- private String deviceInfo; // 设备信息
- private String userAgent; // 用户代理信息
- private Integer statusCode; // 响应状态码
- private String errorMessage; // 错误信息
- private Integer retryCount = 0; // 重试次数计数器,默认0
-
- private Map extraData = new HashMap<>();
-
-
- public BuriedMessages addExtraData(String key, Object value) {
- if (this.extraData == null) {
- this.extraData = new HashMap<>();
- }
- this.extraData.put(key, value);
- return this;
- }
+ /**
+ * 交换机名称
+ */
+ public static final String EXCHANGE = "tashow.buried.point.exchange";
/**
- * 增加重试计数
+ * 队列名称
*/
- public void incrementRetryCount() {
- if (this.retryCount == null) {
- this.retryCount = 0;
- }
- this.retryCount++;
- }
+ public static final String QUEUE = "tashow.buried.point.queue";
+ /**
+ * 路由键
+ */
+ public static final String ROUTING_KEY = "tashow.buried.point.routing.key";
+
+ /**
+ * 消息状态:处理中
+ */
+ public static final int STATUS_PROCESSING = 10;
+
+ /**
+ * 消息状态:成功
+ */
+ public static final int STATUS_SUCCESS = 20;
+
+ /**
+ * 消息状态:失败
+ */
+ public static final int STATUS_ERROR = 30;
+
+ /**
+ * 事件时间
+ */
+ private Date eventTime;
+
+ /**
+ * 用户ID
+ */
+ private String userId;
+
+ /**
+ * 事件类型
+ */
+ private String eventType;
+
+ /**
+ * 方法名称
+ */
+ private String method;
+
+ /**
+ * 会话ID
+ */
+ private String sessionId;
+
+ /**
+ * 客户端IP
+ */
+ private String clientIp;
+
+ /**
+ * 服务端IP
+ */
+ private String serverIp;
+
+ /**
+ * 页面路径
+ */
+ private String pagePath;
+
+ /**
+ * 元素ID
+ */
+ private String elementId;
+
+ /**
+ * 持续时间
+ */
+ private Long duration;
+ /**
+ * 服务名称
+ */
+ private String service;
}
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/producer/buriedPoint/BuriedPointProducer.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/producer/buriedPoint/BuriedPointProducer.java
index 648691c..c1db0be 100644
--- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/producer/buriedPoint/BuriedPointProducer.java
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/producer/buriedPoint/BuriedPointProducer.java
@@ -1,63 +1,29 @@
package com.tashow.cloud.app.mq.producer.buriedPoint;
+
import com.tashow.cloud.app.mq.message.BuriedMessages;
-import com.tashow.cloud.app.mapper.BuriedPointMapper;
import com.tashow.cloud.common.util.json.JsonUtils;
-import lombok.SneakyThrows;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.amqp.rabbit.connection.CorrelationData;
-import org.springframework.amqp.rabbit.core.RabbitTemplate;
-import org.springframework.beans.factory.annotation.Autowired;
+import com.tashow.cloud.mq.rabbitmq.producer.AbstractRabbitMQProducer;
import org.springframework.stereotype.Component;
-import java.util.UUID;
/**
* 埋点消息生产者
*/
-@Slf4j
@Component
-public class BuriedPointProducer implements RabbitTemplate.ConfirmCallback {
-
- @Autowired
- private RabbitTemplate rabbitTemplate;
-
- @Autowired
- private BuriedPointMapper buriedPointMapper;
-
- /**
- * 异步发送完整的埋点消息(生成新的correlationId)
- */
- @SneakyThrows
- public void asyncSendMessage(BuriedMessages message) {
- String correlationId = UUID.randomUUID().toString();
- asyncSendMessage(message, correlationId);
- }
-
- /**
- * 异步发送完整的埋点消息(使用指定的correlationId)
- * 用于重试场景,保持原有的correlationId
- */
- @SneakyThrows
- public void asyncSendMessage(BuriedMessages message, String correlationId) {
- log.info("[埋点] 异步准备发送消息: {}, correlationId: {}", message, correlationId);
- String messageJson = JsonUtils.toJsonString(message);
-
- CustomCorrelationData correlationData = new CustomCorrelationData(correlationId, messageJson);
-
- rabbitTemplate.convertAndSend(BuriedMessages.EXCHANGE, BuriedMessages.ROUTING_KEY, message, correlationData);
- log.info("[埋点] 异步消息发送完成: {}, 状态: {}, 重试次数: {}, correlationId: {}",
- message.getId(), message.getStatusCode(), message.getRetryCount(), correlationId);
- }
+public class BuriedPointProducer extends AbstractRabbitMQProducer {
- /**
- * 确认消息是否成功发送到Broker的回调方法
- */
@Override
- public void confirm(CorrelationData correlationData, boolean ack, String cause) {
- if (ack) {
- log.info("[埋点] 消息发送确认成功: {}", correlationData.getId());
- } else {
- log.error("[埋点] 消息发送确认失败: {}, 原因: {}", correlationData.getId(), cause);
- }
+ public String getExchange() {
+ return "BuriedMessages.EXCHANGE";
+ }
+
+ @Override
+ public String getRoutingKey() {
+ return BuriedMessages.ROUTING_KEY;
+ }
+
+ @Override
+ protected String convertMessageToString(BuriedMessages message) {
+ return JsonUtils.toJsonString(message);
}
}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/feishu/FeiShuCardDataService.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/feishu/FeiShuCardDataService.java
new file mode 100644
index 0000000..1cb7ea4
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/feishu/FeiShuCardDataService.java
@@ -0,0 +1,65 @@
+package com.tashow.cloud.app.service.feishu;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.tashow.cloud.sdk.feishu.client.FeiShuAlertClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 飞书卡片数据处理服务
+ * 负责卡片数据的存储和获取
+ */
+@Service
+public class FeiShuCardDataService implements FeiShuAlertClient.CardDataHandler {
+
+ private final Logger log = LoggerFactory.getLogger(FeiShuCardDataService.class);
+ private final StringRedisTemplate stringRedisTemplate;
+ private final ObjectMapper objectMapper;
+
+ @Autowired
+ public FeiShuCardDataService(StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {
+ this.stringRedisTemplate = stringRedisTemplate;
+ this.objectMapper = objectMapper;
+ }
+
+ /**
+ * 保存卡片数据到Redis
+ * @param messageId 消息ID
+ * @param data 卡片数据
+ */
+ @Override
+ public void saveCardData(String messageId, Map data) {
+ try {
+ String jsonData = objectMapper.writeValueAsString(data);
+ stringRedisTemplate.opsForValue().set(messageId, jsonData, 30, TimeUnit.DAYS);
+ log.debug("卡片数据已保存到Redis, messageId: {}", messageId);
+ } catch (JsonProcessingException e) {
+ log.error("保存卡片数据到Redis失败", e);
+ }
+ }
+
+ /**
+ * 从Redis获取卡片数据
+ * @param messageId 消息ID
+ * @return 卡片数据
+ */
+ @Override
+ public Map getCardData(String messageId) {
+ try {
+ String jsonData = stringRedisTemplate.opsForValue().get(messageId);
+ return objectMapper.readValue(jsonData, Map.class);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/impl/BuriedPointFailRecordService.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/impl/BuriedPointFailRecordService.java
new file mode 100644
index 0000000..f458aaa
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/impl/BuriedPointFailRecordService.java
@@ -0,0 +1,85 @@
+package com.tashow.cloud.app.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.tashow.cloud.app.mapper.BuriedPointFailRecordMapper;
+import com.tashow.cloud.app.model.BuriedPointFailRecord;
+import com.tashow.cloud.app.mq.message.BuriedMessages;
+import com.tashow.cloud.app.mq.producer.buriedPoint.BuriedPointProducer;
+import com.tashow.cloud.common.util.json.JsonUtils;
+import com.tashow.cloud.mq.retry.MessageRetryService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 埋点失败记录服务
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class BuriedPointFailRecordService implements MessageRetryService {
+
+ private final BuriedPointFailRecordMapper buriedPointFailRecordMapper;
+ private final BuriedPointProducer buriedPointProducer;
+
+ /**
+ * 获取未处理的失败记录
+ */
+ @Override
+ public List getUnprocessedRecords() {
+ LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
+ queryWrapper.eq(BuriedPointFailRecord::getStatus, BuriedPointFailRecord.STATUS_UNPROCESSED)
+ .orderByAsc(BuriedPointFailRecord::getCreateTime);
+ return buriedPointFailRecordMapper.selectList(queryWrapper);
+ }
+
+ /**
+ * 重试失败消息
+ */
+ @Override
+ public boolean retryFailedMessage(String recordId) {
+ try {
+ Long id = Long.valueOf(recordId);
+ BuriedPointFailRecord record = buriedPointFailRecordMapper.selectById(id);
+ if (record == null) {
+ log.warn("[埋点重试] 未找到失败记录: {}", id);
+ return false;
+ }
+ BuriedMessages message = JsonUtils.parseObject(record.getMessageContent(), BuriedMessages.class);
+ if (message == null) {
+ log.error("[埋点重试] 消息内容解析失败: {}", record.getCorrelationId());
+ updateStatus(record, BuriedPointFailRecord.STATUS_FAILED);
+ return false;
+ }
+ log.info("[埋点重试] 准备重新发送消息: {}", record.getCorrelationId());
+ buriedPointProducer.asyncSendMessage(message, record.getCorrelationId());
+ record.setStatus(BuriedPointFailRecord.STATUS_SUCCESS);
+ record.setUpdateTime(new Date());
+ buriedPointFailRecordMapper.updateById(record);
+ log.info("[埋点重试] 重试成功,状态已更新为成功: {}", record.getCorrelationId());
+ return true;
+ } catch (Exception e) {
+ log.error("[埋点重试] 重试失败消息异常: {}", recordId, e);
+ return false;
+ }
+ }
+
+ /**
+ * 更新记录状态
+ */
+ @Override
+ public boolean updateStatus(BuriedPointFailRecord record, int status) {
+ try {
+ record.setStatus(status);
+ record.setUpdateTime(new Date());
+ int result = buriedPointFailRecordMapper.updateById(record);
+ return result > 0;
+ } catch (Exception e) {
+ log.error("[埋点重试] 更新状态失败: {}", record.getCorrelationId(), e);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/task/BuriedPointRetryTask.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/task/BuriedPointRetryTask.java
new file mode 100644
index 0000000..686c022
--- /dev/null
+++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/task/BuriedPointRetryTask.java
@@ -0,0 +1,45 @@
+package com.tashow.cloud.app.task;
+
+import com.tashow.cloud.app.model.BuriedPointFailRecord;
+import com.tashow.cloud.app.service.impl.BuriedPointFailRecordService;
+import com.tashow.cloud.mq.retry.AbstractMessageRetryTask;
+import com.tashow.cloud.mq.retry.MessageRetryService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 埋点消息重试定时任务
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class BuriedPointRetryTask extends AbstractMessageRetryTask {
+
+ private final BuriedPointFailRecordService buriedPointFailRecordService;
+
+ /**
+ * 定时重试失败消息
+ * 每天凌晨执行一次
+ */
+ @Scheduled(cron = "0 0 0 * * ?")
+ public void execute() {
+ retryFailedMessages();
+ }
+
+ @Override
+ protected MessageRetryService getMessageRetryService() {
+ return buriedPointFailRecordService;
+ }
+
+ @Override
+ protected String getRecordId(BuriedPointFailRecord record) {
+ return String.valueOf(record.getId());
+ }
+
+ @Override
+ protected String getCorrelationId(BuriedPointFailRecord record) {
+ return record.getCorrelationId();
+ }
+}
\ No newline at end of file
diff --git a/tashow-sdk/pom.xml b/tashow-sdk/pom.xml
index f3b4015..58ee94c 100644
--- a/tashow-sdk/pom.xml
+++ b/tashow-sdk/pom.xml
@@ -12,6 +12,7 @@
tashow-sdk-payment
+ tashow-feishu-sdk
diff --git a/tashow-sdk/tashow-feishu-sdk/pom.xml b/tashow-sdk/tashow-feishu-sdk/pom.xml
new file mode 100644
index 0000000..097a044
--- /dev/null
+++ b/tashow-sdk/tashow-feishu-sdk/pom.xml
@@ -0,0 +1,44 @@
+
+ 4.0.0
+
+ com.tashow.cloud
+ tashow-sdk
+ ${revision}
+
+
+ tashow-feishu-sdk
+ jar
+
+
+
+
+
+ com.tashow.cloud
+ tashow-data-redis
+
+
+
+ com.larksuite.oapi
+ oapi-sdk
+ 2.4.18
+
+
+ httpclient
+ org.apache.httpcomponents
+ 4.5.13
+
+
+
+
+ junit
+ junit
+ test
+ 4.13.2
+
+
+ com.tashow.cloud
+ tashow-common
+
+
+
diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuAlertClient.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuAlertClient.java
new file mode 100644
index 0000000..f1e378c
--- /dev/null
+++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuAlertClient.java
@@ -0,0 +1,274 @@
+package com.tashow.cloud.sdk.feishu.client;
+import com.lark.oapi.Client;
+import com.lark.oapi.service.im.v1.model.*;
+import com.tashow.cloud.sdk.feishu.util.ChartImageGenerator;
+import com.lark.oapi.service.im.v1.model.ext.MessageTemplate;
+import com.lark.oapi.service.im.v1.model.ext.MessageTemplateData;
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import com.tashow.cloud.sdk.feishu.config.LarkConfig;
+import com.tashow.cloud.sdk.feishu.util.LarkClientUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 飞书告警客户端
+ * 用于处理系统告警消息的发送
+ */
+@Service
+public class FeiShuAlertClient {
+ private final Logger log = LoggerFactory.getLogger(FeiShuAlertClient.class);
+ private final Client client;
+ private final LarkConfig larkConfig;
+ private final ChartImageGenerator chartImageGenerator;
+ public interface CardDataHandler {
+ /**
+ * 保存卡片数据
+ * @param messageId 消息ID
+ * @param data 卡片数据
+ */
+ void saveCardData(String messageId, Map data);
+ /**
+ * 获取卡片数据
+ * @param messageId 消息ID
+ * @return 卡片数据
+ */
+ Map getCardData(String messageId);
+ }
+ private CardDataHandler cardDataHandler;
+
+ @Autowired
+ public FeiShuAlertClient(LarkClientUtil larkClientUtil, LarkConfig larkConfig,
+ ChartImageGenerator chartImageGenerator, ObjectMapper objectMapper) {
+ this.client = larkClientUtil.getLarkClient();
+ this.larkConfig = larkConfig;
+ this.chartImageGenerator = chartImageGenerator;
+ }
+
+
+
+ /**
+ * 创建报警群并拉人入群
+ *
+ * @return 创建的群聊ID
+ * @throws Exception 异常信息
+ */
+ public String createAlertChat() throws Exception {
+ CreateChatReq req = CreateChatReq.newBuilder()
+ .userIdType("open_id")
+ .createChatReqBody(CreateChatReqBody.newBuilder()
+ .name("[待处理] 线上事故处理")
+ .description("线上紧急事故处理")
+ .userIdList(larkConfig.getAlertUserOpenIds())
+ .build())
+ .build();
+ CreateChatResp resp = client.im().chat().create(req);
+ if (!resp.success()) {
+ throw new Exception(String.format("client.im.chat.create failed, code: %d, msg: %s, logId: %s",
+ resp.getCode(), resp.getMsg(), resp.getRequestId()));
+ }
+ return resp.getData().getChatId();
+ }
+
+ /**
+ * 发送埋点报警消息
+ *
+ * @param chatId 会话ID
+ * @param buriedPointData 埋点数据
+ * @param failCount 失败数量
+ * @throws Exception 异常信息
+ */
+ public void sendBuriedPointAlertMessage(String chatId, List buriedPointData, int failCount) throws Exception {
+ sendBuriedPointAlertMessage(chatId, buriedPointData, failCount, null);
+ }
+
+ /**
+ * 发送带错误信息的埋点报警消息
+ *
+ * @param chatId 会话ID
+ * @param buriedPointData 埋点数据
+ * @param failCount 失败数量
+ * @param errorMessage 错误信息
+ * @throws Exception 异常信息
+ */
+ public void sendBuriedPointAlertMessage(String chatId, List buriedPointData, int failCount, String errorMessage) throws Exception {
+ HashMap templateData = new HashMap<>();
+ String imageKey = uploadImage(buriedPointData, errorMessage);
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String currentTime = sdf.format(new Date());
+ templateData.put("alert_title", "埋点数据异常告警");
+ templateData.put("image_key", imageKey);
+ templateData.put("current_time", currentTime);
+ templateData.put("fail_count", failCount);
+ sendCardMessage(chatId, "AAqdpjayeOVp2", templateData);
+ }
+
+ /**
+ * 发送带错误信息的埋点报警消息(创建群)
+ *
+ * @param buriedPointData 埋点数据
+ * @param failCount 失败数量
+ * @param errorMessage 错误信息
+ * @throws Exception 异常信息
+ */
+ public void sendBuriedPointAlertMessage(List buriedPointData, int failCount, String errorMessage) throws Exception {
+ String chatId = createAlertChat();
+ sendBuriedPointAlertMessage(chatId, buriedPointData, failCount, errorMessage);
+ }
+
+ /**
+ * 发送报警消息
+ *
+ * @param chatId 会话ID
+ * @param msgType 消息类型
+ * @param content 消息内容
+ * @return 消息ID
+ * @throws Exception 异常信息
+ */
+ public String sendMessage(String chatId, String msgType, String content) throws Exception {
+ CreateMessageReq req = CreateMessageReq.newBuilder()
+ .receiveIdType("chat_id")
+ .createMessageReqBody(CreateMessageReqBody.newBuilder()
+ .receiveId(chatId)
+ .msgType(msgType)
+ .content(content)
+ .build())
+ .build();
+
+ CreateMessageResp resp = client.im().message().create(req);
+ if (!resp.success()) {
+ throw new Exception(String.format("client.im.message.create failed, code: %d, msg: %s, logId: %s",
+ resp.getCode(), resp.getMsg(), resp.getRequestId()));
+ }
+
+ return resp.getData().getMessageId();
+ }
+
+ /**
+ * 更新卡片消息
+ *
+ * @param messageId 消息ID
+ * @param content 新的卡片内容
+ * @throws Exception 异常信息
+ */
+ public void updateCardMessage(String messageId, String content) throws Exception {
+ PatchMessageReq req = PatchMessageReq.newBuilder()
+ .messageId(messageId)
+ .patchMessageReqBody(PatchMessageReqBody.newBuilder()
+ .content(content)
+ .build())
+ .build();
+ PatchMessageResp resp = client.im().message().patch(req);
+
+ if (!resp.success()) {
+ throw new Exception(String.format("client.im.message.patch failed, code: %d, msg: %s, logId: %s",
+ resp.getCode(), resp.getMsg(), resp.getRequestId()));
+ }
+ }
+
+ /**
+ * 上传指定数据的监控图表(带错误信息)
+ *
+ * @param monitoringData 监控数据
+ * @param errorMessage 错误信息
+ * @return 上传后的图片KEY
+ * @throws Exception 异常信息
+ */
+ public String uploadImage(List monitoringData, String errorMessage) throws Exception {
+ // 动态生成监控图表
+ File tempFile = File.createTempFile("alert", ".png");
+ // 使用提供的数据生成图表
+ chartImageGenerator.generateDashboardImage(tempFile, monitoringData, errorMessage);
+ CreateImageReq req = CreateImageReq.newBuilder()
+ .createImageReqBody(CreateImageReqBody.newBuilder()
+ .imageType("message")
+ .image(tempFile)
+ .build())
+ .build();
+
+ CreateImageResp resp = client.im().image().create(req);
+ if (!resp.success()) {
+ throw new Exception(String.format("client.im.image.create failed, code: %d, msg: %s, logId: %s",
+ resp.getCode(), resp.getMsg(), resp.getRequestId()));
+ }
+ tempFile.delete();
+ return resp.getData().getImageKey();
+ }
+
+ /**
+ * 使用模板数据构建卡片内容
+ *
+ * @param templateId 卡片模板ID
+ * @param templateData 模板数据
+ * @return 卡片JSON内容
+ */
+ public String buildCardWithData(String templateId, Map templateData) {
+ return new MessageTemplate.Builder()
+ .data(new MessageTemplateData.Builder().templateId(templateId)
+ .templateVariable(templateData)
+ .build())
+ .build();
+ }
+
+ /**
+ * 构建埋点异常卡片
+ *
+ * @param buttonName 按钮名称
+ * @param buriedPointData 埋点数据
+ * @param failCount 失败数量
+ * @return 卡片JSON
+ * @throws Exception 异常信息
+ */
+ private String buildBuriedPointCard(String buttonName, List buriedPointData, int failCount) throws Exception {
+ return buildBuriedPointCard(buttonName, buriedPointData, failCount, null);
+ }
+
+ /**
+ * 构建埋点异常卡片(带错误信息)
+ *
+ * @param buttonName 按钮名称
+ * @param buriedPointData 埋点数据
+ * @param failCount 失败数量
+ * @param errorMessage 错误信息
+ * @return 卡片JSON
+ * @throws Exception 异常信息
+ */
+ private String buildBuriedPointCard(String buttonName, List buriedPointData, int failCount, String errorMessage) throws Exception {
+ String imageKey = uploadImage(buriedPointData, errorMessage);
+ // 获取当前时间
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String currentTime = sdf.format(new Date());
+
+ HashMap templateData = new HashMap<>();
+ templateData.put("alert_title", "埋点数据异常告警");
+ templateData.put("image_key", imageKey);
+ templateData.put("current_time", currentTime);
+ templateData.put("fail_count", failCount);
+ templateData.put("button_name", buttonName);
+
+ return buildCardWithData("AAqdpjayeOVp2", templateData);
+ }
+
+ /**
+ * 发送卡片消息并保存数据
+ *
+ * @param chatId 会话ID
+ * @param templateId 卡片模板ID
+ * @param templateData 模板数据
+ * @return 消息ID
+ * @throws Exception 异常信息
+ */
+ public String sendCardMessage(String chatId, String templateId, Map templateData) throws Exception {
+ String cardContent = buildCardWithData(templateId, templateData);
+ String messageId = sendMessage(chatId, "interactive", cardContent);
+ if (cardDataHandler != null && messageId != null) {
+ cardDataHandler.saveCardData(messageId, templateData);
+ }
+
+ return messageId;
+ }
+}
diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuMessageClient.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuMessageClient.java
new file mode 100644
index 0000000..e2dd2a1
--- /dev/null
+++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuMessageClient.java
@@ -0,0 +1,109 @@
+package com.tashow.cloud.sdk.feishu.client;
+import com.lark.oapi.Client;
+import com.lark.oapi.core.utils.Jsons;
+import com.lark.oapi.service.im.v1.model.*;
+import com.tashow.cloud.sdk.feishu.config.LarkConfig;
+import com.tashow.cloud.sdk.feishu.util.LarkClientUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.*;
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * 飞书普通消息客户端
+ * 用于处理与警报无关的消息发送
+ */
+@Service
+public class FeiShuMessageClient {
+
+ private final Client client;
+ private final LarkConfig larkConfig;
+
+ @Autowired
+ public FeiShuMessageClient(LarkClientUtil larkClientUtil, LarkConfig larkConfig) {
+ this.client = larkClientUtil.getLarkClient();
+ this.larkConfig = larkConfig;
+ }
+
+ /**
+ * 发送文本消息
+ * @param chatId 会话ID
+ * @param text 消息文本
+ * @return 发送结果
+ * @throws Exception 异常信息
+ */
+ public boolean sendTextMessage(String chatId, String text) throws Exception {
+ Map content = new HashMap<>();
+ content.put("text", text);
+ CreateMessageReq req = CreateMessageReq.newBuilder()
+ .receiveIdType("chat_id")
+ .createMessageReqBody(CreateMessageReqBody.newBuilder()
+ .receiveId(chatId)
+ .msgType("text")
+ .content(Jsons.DEFAULT.toJson(content))
+ .build())
+ .build();
+ CreateMessageResp resp = client.im().message().create(req);
+ if (!resp.success()) {
+ System.out.println("发送失败原因: " + resp.getMsg() + ", 错误码: " + resp.getCode());
+ }
+ return resp.success();
+ }
+
+ /**
+ * 发送富文本消息
+ * @param chatId 会话ID()
+ * @param title 标题
+ * @param content 内容
+ * @return 发送结果
+ * @throws Exception 异常信息
+ */
+ public boolean sendPostMessage(String chatId, String title, String content) throws Exception {
+ // 正确的富文本消息格式
+ String postJson = String.format("{\"zh_cn\":{\"title\":\"%s\",\"content\":[[{\"tag\":\"text\",\"text\":\"%s\"}]]}}",
+ title, content);
+ CreateMessageReq req = CreateMessageReq.newBuilder()
+ .receiveIdType("chat_id")
+ .createMessageReqBody(CreateMessageReqBody.newBuilder()
+ .receiveId(chatId)
+ .msgType("post")
+ .content(postJson)
+ .build())
+ .build();
+
+ CreateMessageResp resp = client.im().message().create(req);
+ if (!resp.success()) {
+ System.out.println("发送失败原因: " + resp.getMsg() + ", 错误码: " + resp.getCode());
+ }
+ return resp.success();
+ }
+
+ /**
+ * 获取会话历史消息
+ * @param chatId 会话ID
+ * @throws Exception 异常信息
+ */
+ public void listChatHistory(String chatId) throws Exception {
+ ListMessageReq req = ListMessageReq.newBuilder().containerIdType("chat").containerId(chatId).build();
+
+ ListMessageResp resp = client.im().message().list(req);
+
+ if (!resp.success()) {
+ throw new Exception(String.format("client.im.message.list failed, code: %d, msg: %s, logId: %s", resp.getCode(), resp.getMsg(), resp.getRequestId()));
+ }
+ File file = new File("./src/main/java/com/larksuite/oapi/quick_start/robot/chat_history.txt");
+ FileWriter writer = new FileWriter(file);
+ for (Message item : resp.getData().getItems()) {
+ String senderId = item.getSender().getId();
+ String content = item.getBody().getContent();
+ String createTime = item.getCreateTime();
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ createTime = sdf.format(new Date(Long.parseLong(createTime)));
+ writer.write(String.format("chatter(%s) at (%s) send: %s\n", senderId, createTime, content));
+ }
+ writer.close();
+ }
+}
\ No newline at end of file
diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/config/LarkConfig.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/config/LarkConfig.java
new file mode 100644
index 0000000..f283442
--- /dev/null
+++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/config/LarkConfig.java
@@ -0,0 +1,35 @@
+package com.tashow.cloud.sdk.feishu.config;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+
+/**
+ * 飞书配置类
+ * 用于管理飞书应用的配置信息
+ */
+@Component
+@Data
+public class LarkConfig {
+
+ @Value("${lark.app.id}")
+ private String appId;
+
+ @Value("${lark.app.secret}")
+ private String appSecret;
+
+ @Value("${lark.app.encrypt-key}")
+ private String encryptKey;
+
+ @Value("${lark.app.verification-token}")
+ private String verificationToken;
+
+ @Value("${lark.alert.chat-id}")
+ private String chatId;
+
+ @Value("${lark.alert.user-open-ids}")
+ private String[] alertUserOpenIds;
+
+
+}
\ No newline at end of file
diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/ChartImageGenerator.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/ChartImageGenerator.java
new file mode 100644
index 0000000..6183f49
--- /dev/null
+++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/ChartImageGenerator.java
@@ -0,0 +1,324 @@
+package com.tashow.cloud.sdk.feishu.util;
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import javax.imageio.ImageIO;
+import org.springframework.stereotype.Component;
+
+/**
+ * 图表生成工具类
+ * 用于生成监控数据图表
+ */
+@Component
+public class ChartImageGenerator {
+
+ /**
+ * 监控数据点类
+ */
+ public static class MonitoringDataPoint {
+ private String timestamp; // 时间戳,格式如 "13:54"
+ private int successCount; // 成功数量
+ private int failureCount; // 失败数量
+
+ public MonitoringDataPoint(String timestamp, int successCount, int failureCount) {
+ this.timestamp = timestamp;
+ this.successCount = successCount;
+ this.failureCount = failureCount;
+ }
+
+ public String getTimestamp() {
+ return timestamp;
+ }
+
+ public int getSuccessCount() {
+ return successCount;
+ }
+
+ public int getFailureCount() {
+ return failureCount;
+ }
+ }
+
+ /**
+ * 生成监控仪表盘图像
+ * @param outputFile 输出文件
+ * @param monitoringData 监控数据
+ * @throws IOException 如果图像创建失败
+ */
+ public void generateDashboardImage(File outputFile, List monitoringData) throws IOException {
+ generateDashboardImage(outputFile, monitoringData, null);
+ }
+
+ /**
+ * 生成监控仪表盘图像(带错误信息)
+ * @param outputFile 输出文件
+ * @param monitoringData 监控数据
+ * @param errorMessage 错误信息,如为null则不显示
+ * @throws IOException 如果图像创建失败
+ */
+ public void generateDashboardImage(File outputFile, List monitoringData, String errorMessage) throws IOException {
+ int width = 850;
+ int height = 350; // 减小高度,原来是550
+ int padding = 70;
+ int topPadding = 30; // 减少顶部空白,使用单独的顶部padding值
+
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ Graphics2D g2d = image.createGraphics();
+
+ // 启用抗锯齿
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+
+ // 设置背景为白色
+ g2d.setColor(Color.WHITE);
+ g2d.fillRect(0, 0, width, height);
+
+ // 添加科技感背景网格
+ drawTechBackground(g2d, width, height);
+
+ // 计算图表区域
+ int chartWidth = width - padding * 2;
+ int chartHeight = height - padding - topPadding; // 调整图表高度计算
+
+ // 绘制水平网格线
+ Font labelFont = new Font("Microsoft YaHei", Font.PLAIN, 12);
+ g2d.setFont(labelFont);
+ FontMetrics metrics = g2d.getFontMetrics(labelFont);
+
+ // 找出最大值以确定y轴的刻度
+ int maxValue = 0;
+ for (MonitoringDataPoint point : monitoringData) {
+ maxValue = Math.max(maxValue, Math.max(point.getSuccessCount(), point.getFailureCount()));
+ }
+
+ // 向上调整10%,确保有足够空间显示数据
+ maxValue = (int)(maxValue * 1.1);
+
+ // 如果最大值太小,设置一个最小值确保图表可读性
+ if (maxValue < 10) {
+ maxValue = 10;
+ }
+
+ // 向上取整到合适的刻度
+ if (maxValue <= 100) {
+ // 小于100时,取整到10的倍数
+ maxValue = ((maxValue + 9) / 10) * 10;
+ } else if (maxValue <= 1000) {
+ // 100-1000时,取整到50的倍数
+ maxValue = ((maxValue + 49) / 50) * 50;
+ } else {
+ // 大于1000时,取整到100的倍数
+ maxValue = ((maxValue + 99) / 100) * 100;
+ }
+
+ // 动态计算y轴刻度
+ int yDivisions = 5; // Y轴分段数
+ int yStep = maxValue / yDivisions;
+
+ // 绘制水平网格线
+ for (int i = 0; i <= yDivisions; i++) {
+ int y = height - padding - (i * chartHeight / yDivisions);
+
+ // 科技感网格线
+ g2d.setColor(new Color(220, 220, 240, 100));
+ g2d.setStroke(new BasicStroke(0.8f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 0, new float[]{3}, 0));
+ g2d.drawLine(padding, y, width - padding, y);
+
+ // 添加y轴标签
+ String yLabel = String.format("%d", i * yStep);
+ int labelWidth = metrics.stringWidth(yLabel);
+ g2d.setColor(new Color(80, 80, 120));
+ g2d.drawString(yLabel, padding - labelWidth - 10, y + metrics.getHeight() / 2 - 2);
+ }
+
+ // 绘制垂直网格线和X轴标签
+ int totalPoints = monitoringData.size();
+ for (int i = 0; i < totalPoints; i++) {
+ int x = padding + (i * chartWidth / (totalPoints - 1));
+
+ // 科技感垂直网格线
+ g2d.setColor(new Color(220, 220, 240, 100));
+ g2d.setStroke(new BasicStroke(0.8f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 0, new float[]{3}, 0));
+ g2d.drawLine(x, topPadding, x, height - padding); // 调整网格线顶部起点
+
+ // 添加每个点对应的时间标签
+ if (i % 2 == 0 || i == totalPoints - 1) { // 每隔1个点显示标签,减少拥挤感
+ String timeLabel = monitoringData.get(i).getTimestamp();
+ int labelWidth = metrics.stringWidth(timeLabel);
+ g2d.setColor(new Color(80, 80, 120));
+ g2d.drawString(timeLabel, x - labelWidth / 2, height - padding + 20);
+ }
+ }
+
+ // 绘制成功线(荧光蓝色)- 使用带标签的方法替代原方法
+ drawGlowingLineWithLabels(g2d,
+ calculateXPoints(totalPoints, padding, chartWidth),
+ calculateSuccessYPoints(monitoringData, totalPoints, height, padding, chartHeight, yDivisions, yStep),
+ new Color(0, 191, 255), new Color(0, 120, 215), 4.0f,
+ monitoringData, true);
+
+ // 绘制失败线(荧光红色)- 使用带标签的方法替代原方法
+ drawGlowingLineWithLabels(g2d,
+ calculateXPoints(totalPoints, padding, chartWidth),
+ calculateFailureYPoints(monitoringData, totalPoints, height, padding, chartHeight, yDivisions, yStep),
+ new Color(255, 50, 100), new Color(200, 30, 80), 4.0f,
+ monitoringData, false);
+
+ // 绘制图表边框
+ g2d.setColor(new Color(210, 210, 230));
+ g2d.setStroke(new BasicStroke(1.5f));
+ g2d.drawRect(padding, topPadding, chartWidth, chartHeight); // 调整边框位置
+
+ // 释放资源
+ g2d.dispose();
+
+ // 在底部添加错误信息
+ // 重新获取图像的Graphics2D对象
+ g2d = image.createGraphics();
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ g2d.setColor(new Color(100, 100, 130));
+ g2d.setFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
+
+ // 使用动态传入的错误信息,而非硬编码
+ if (errorMessage != null && !errorMessage.trim().isEmpty()) {
+ g2d.drawString(errorMessage, 70, height - 10);
+ }
+
+ g2d.dispose();
+
+ // 保存图像
+ ImageIO.write(image, "png", outputFile);
+ }
+
+ /**
+ * 添加科技感背景
+ */
+ private void drawTechBackground(Graphics2D g2d, int width, int height) {
+ g2d.setColor(new Color(240, 240, 250, 120));
+ g2d.setStroke(new BasicStroke(0.5f));
+
+ // 小网格
+ int smallGridSize = 15;
+ for (int x = 0; x < width; x += smallGridSize) {
+ g2d.drawLine(x, 0, x, height);
+ }
+ for (int y = 0; y < height; y += smallGridSize) {
+ g2d.drawLine(0, y, width, y);
+ }
+ }
+
+ /**
+ * 绘制发光线条
+ */
+ private void drawGlowingLine(Graphics2D g2d, int[] xPoints, int[] yPoints, Color mainColor, Color glowColor, float thickness) {
+ int totalPoints = xPoints.length;
+
+ // 绘制发光效果(外层)
+ g2d.setColor(new Color(glowColor.getRed(), glowColor.getGreen(), glowColor.getBlue(), 80));
+ g2d.setStroke(new BasicStroke(thickness + 4.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+ for (int i = 0; i < totalPoints - 1; i++) {
+ g2d.drawLine(xPoints[i], yPoints[i], xPoints[i + 1], yPoints[i + 1]);
+ }
+
+ // 绘制发光效果(中层)
+ g2d.setColor(new Color(glowColor.getRed(), glowColor.getGreen(), glowColor.getBlue(), 120));
+ g2d.setStroke(new BasicStroke(thickness + 2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+ for (int i = 0; i < totalPoints - 1; i++) {
+ g2d.drawLine(xPoints[i], yPoints[i], xPoints[i + 1], yPoints[i + 1]);
+ }
+
+ // 绘制主线
+ g2d.setColor(mainColor);
+ g2d.setStroke(new BasicStroke(thickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+ for (int i = 0; i < totalPoints - 1; i++) {
+ g2d.drawLine(xPoints[i], yPoints[i], xPoints[i + 1], yPoints[i + 1]);
+ }
+
+ // 绘制高亮数据点
+ for (int i = 0; i < totalPoints; i++) {
+ // 外发光
+ g2d.setColor(new Color(glowColor.getRed(), glowColor.getGreen(), glowColor.getBlue(), 80));
+ g2d.fillOval(xPoints[i] - 6, yPoints[i] - 6, 12, 12);
+
+ // 中发光
+ g2d.setColor(new Color(mainColor.getRed(), mainColor.getGreen(), mainColor.getBlue(), 150));
+ g2d.fillOval(xPoints[i] - 4, yPoints[i] - 4, 8, 8);
+
+ // 内部点
+ g2d.setColor(Color.WHITE);
+ g2d.fillOval(xPoints[i] - 2, yPoints[i] - 2, 4, 4);
+ }
+ }
+
+ /**
+ * 绘制带有数值标签的发光线条
+ */
+ private void drawGlowingLineWithLabels(Graphics2D g2d, int[] xPoints, int[] yPoints,
+ Color mainColor, Color glowColor, float thickness,
+ List data, boolean isSuccess) {
+ // 先绘制基本的发光线条
+ drawGlowingLine(g2d, xPoints, yPoints, mainColor, glowColor, thickness);
+ // 添加数值标签 - 使用普通字体而非粗体
+ g2d.setFont(new Font("Microsoft YaHei", Font.PLAIN, 11));
+ FontMetrics metrics = g2d.getFontMetrics();
+
+ for (int i = 0; i < xPoints.length; i++) {
+ // 获取当前值
+ int currentValue = isSuccess ? data.get(i).getSuccessCount() : data.get(i).getFailureCount();
+
+ // 始终显示所有数据点的数值标签
+ String label = String.valueOf(currentValue);
+ int labelWidth = metrics.stringWidth(label);
+
+ // 设置标签文本
+ g2d.setColor(mainColor.darker());
+ g2d.drawString(label, xPoints[i] - labelWidth/2, yPoints[i] - 5);
+ }
+ }
+
+ /**
+ * 计算X坐标点
+ */
+ private int[] calculateXPoints(int totalPoints, int padding, int chartWidth) {
+ int[] points = new int[totalPoints];
+ for (int i = 0; i < totalPoints; i++) {
+ points[i] = padding + (i * chartWidth / (totalPoints - 1));
+ }
+ return points;
+ }
+
+ /**
+ * 计算成功线的Y坐标点
+ */
+ private int[] calculateSuccessYPoints(List data, int totalPoints, int height,
+ int padding, int chartHeight, int yDivisions, int yStep) {
+ int[] points = new int[totalPoints];
+ for (int i = 0; i < totalPoints; i++) {
+ int successScaled = (int)((double)data.get(i).getSuccessCount() * chartHeight / (yDivisions * yStep));
+ points[i] = height - padding - successScaled;
+ }
+ return points;
+ }
+
+ /**
+ * 计算失败线的Y坐标点
+ */
+ private int[] calculateFailureYPoints(List data, int totalPoints, int height,
+ int padding, int chartHeight, int yDivisions, int yStep) {
+ int[] points = new int[totalPoints];
+ for (int i = 0; i < totalPoints; i++) {
+ int failureScaled = (int)((double)data.get(i).getFailureCount() * chartHeight / (yDivisions * yStep));
+ points[i] = height - padding - failureScaled;
+ }
+ return points;
+ }
+}
\ No newline at end of file
diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/LarkClientUtil.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/LarkClientUtil.java
new file mode 100644
index 0000000..5375137
--- /dev/null
+++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/LarkClientUtil.java
@@ -0,0 +1,28 @@
+package com.tashow.cloud.sdk.feishu.util;
+import com.lark.oapi.Client;
+import com.tashow.cloud.sdk.feishu.config.LarkConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 飞书客户端工具类
+ * 用于创建和获取飞书客户端实例
+ */
+@Component
+public class LarkClientUtil {
+
+ private final LarkConfig larkConfig;
+
+ @Autowired
+ public LarkClientUtil(LarkConfig larkConfig) {
+ this.larkConfig = larkConfig;
+ }
+
+ /**
+ * 获取飞书客户端实例
+ * @return 飞书客户端
+ */
+ public Client getLarkClient() {
+ return Client.newBuilder(larkConfig.getAppId(), larkConfig.getAppSecret()).build();
+ }
+}
\ No newline at end of file
diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/resources/card.json b/tashow-sdk/tashow-feishu-sdk/src/main/resources/card.json
new file mode 100644
index 0000000..327b1b8
--- /dev/null
+++ b/tashow-sdk/tashow-feishu-sdk/src/main/resources/card.json
@@ -0,0 +1,266 @@
+{
+ "name": "12",
+ "dsl": {
+ "schema": "2.0",
+ "config": {
+ "update_multi": true,
+ "locales": [
+ "en_us"
+ ],
+ "style": {
+ "text_size": {
+ "normal_v2": {
+ "default": "normal",
+ "pc": "normal",
+ "mobile": "heading"
+ }
+ }
+ }
+ },
+ "body": {
+ "direction": "vertical",
+ "padding": "12px 12px 12px 12px",
+ "elements": [
+ {
+ "tag": "column_set",
+ "horizontal_spacing": "8px",
+ "horizontal_align": "left",
+ "columns": [
+ {
+ "tag": "column",
+ "width": "weighted",
+ "elements": [
+ {
+ "tag": "markdown",
+ "content": "负责人\n",
+ "i18n_content": {
+ "en_us": "Alert details\nMobile client crash rate at 5%"
+ },
+ "text_align": "left",
+ "text_size": "normal_v2",
+ "margin": "0px 0px 0px 0px",
+ "icon": {
+ "tag": "standard_icon",
+ "token": "contacts_outlined",
+ "color": "grey"
+ }
+ }
+ ],
+ "vertical_spacing": "8px",
+ "horizontal_align": "left",
+ "vertical_align": "top",
+ "weight": 1
+ },
+ {
+ "tag": "column",
+ "width": "weighted",
+ "elements": [
+ {
+ "tag": "markdown",
+ "content": "失败数量\n${fail_count}",
+ "i18n_content": {
+ "en_us": "Diagnostic info\nService request volume exceeds rate limit"
+ },
+ "text_align": "left",
+ "text_size": "normal_v2",
+ "margin": "0px 0px 0px 0px",
+ "icon": {
+ "tag": "standard_icon",
+ "token": "meego_colorful",
+ "color": "grey"
+ }
+ }
+ ],
+ "vertical_spacing": "8px",
+ "horizontal_align": "left",
+ "vertical_align": "top",
+ "weight": 1
+ }
+ ],
+ "margin": "0px 0px 0px 0px"
+ },
+ {
+ "tag": "column_set",
+ "horizontal_spacing": "8px",
+ "horizontal_align": "left",
+ "columns": [
+ {
+ "tag": "column",
+ "width": "weighted",
+ "elements": [
+ {
+ "tag": "markdown",
+ "content": "项目\nTashow平台",
+ "i18n_content": {
+ "en_us": "Priority level\nP0"
+ },
+ "text_align": "left",
+ "text_size": "normal_v2",
+ "margin": "0px 0px 0px 0px",
+ "icon": {
+ "tag": "standard_icon",
+ "token": "file-form_colorful",
+ "color": "grey"
+ }
+ }
+ ],
+ "direction": "vertical",
+ "horizontal_spacing": "8px",
+ "vertical_spacing": "8px",
+ "horizontal_align": "left",
+ "vertical_align": "top",
+ "weight": 1
+ },
+ {
+ "tag": "column",
+ "width": "weighted",
+ "elements": [
+ {
+ "tag": "markdown",
+ "content": "告警时间\n${current_time}",
+ "i18n_content": {
+ "en_us": "Incident time\n${alarm_time}"
+ },
+ "text_align": "left",
+ "text_size": "normal_v2",
+ "margin": "0px 0px 0px 0px",
+ "icon": {
+ "tag": "standard_icon",
+ "token": "calendar_colorful",
+ "color": "grey"
+ }
+ }
+ ],
+ "direction": "vertical",
+ "horizontal_spacing": "8px",
+ "vertical_spacing": "8px",
+ "horizontal_align": "left",
+ "vertical_align": "top",
+ "weight": 1
+ }
+ ],
+ "margin": "0px 0px 0px 0px"
+ },
+ {
+ "tag": "form",
+ "elements": [
+ {
+ "tag": "img",
+ "img_key": "img_v3_02nc_085db227-0547-40eb-90a1-dd80434b229g",
+ "preview": true,
+ "transparent": false,
+ "scale_type": "fit_horizontal",
+ "margin": "0px 0px 0px 0px"
+ },
+ {
+ "tag": "input",
+ "placeholder": {
+ "tag": "plain_text",
+ "content": "处理情况说明,选填",
+ "i18n_content": {
+ "en_us": "Action taken (if any)"
+ }
+ },
+ "default_value": "",
+ "width": "fill",
+ "name": "notes_input",
+ "margin": "0px 0px 0px 0px"
+ },
+ {
+ "tag": "column_set",
+ "horizontal_align": "left",
+ "columns": [
+ {
+ "tag": "column",
+ "width": "auto",
+ "elements": [
+ {
+ "tag": "button",
+ "text": {
+ "tag": "plain_text",
+ "content": "处理完成",
+ "i18n_content": {
+ "en_us": "Mark as Resolved"
+ }
+ },
+ "type": "primary",
+ "width": "default",
+ "behaviors": [
+ {
+ "type": "callback",
+ "value": {
+ "action": "complete_alarm",
+ "time": "${alarm_time}"
+ }
+ }
+ ],
+ "form_action_type": "submit",
+ "name": "Button_m6vy7xom"
+ }
+ ],
+ "vertical_spacing": "8px",
+ "horizontal_align": "left",
+ "vertical_align": "top"
+ }
+ ],
+ "margin": "0px 0px 0px 0px"
+ }
+ ],
+ "direction": "vertical",
+ "padding": "4px 0px 4px 0px",
+ "margin": "0px 0px 0px 0px",
+ "name": "Form_m6vy7xol"
+ }
+ ]
+ },
+ "header": {
+ "title": {
+ "tag": "plain_text",
+ "content": "${alert_title}",
+ "i18n_content": {
+ "en_us": "[Action Needed] Alert: Process Error - Please Address Promptly"
+ }
+ },
+ "subtitle": {
+ "tag": "plain_text",
+ "content": ""
+ },
+ "template": "red",
+ "icon": {
+ "tag": "standard_icon",
+ "token": "warning-hollow_filled"
+ },
+ "padding": "12px 12px 12px 12px"
+ }
+ },
+ "variables": [
+ {
+ "type": "text",
+ "apiName": "var_m6vy7ngf",
+ "name": "alarm_time",
+ "desc": "告警时间",
+ "mockData": "2025-01-01 10:10:08"
+ },
+ {
+ "type": "text",
+ "apiName": "var_mc1d8e1w",
+ "name": "fail_count",
+ "desc": "",
+ "mockData": "0"
+ },
+ {
+ "type": "text",
+ "apiName": "var_mc1d8e1z",
+ "name": "current_time",
+ "desc": "",
+ "mockData": "2025-06-17 17:32:13"
+ },
+ {
+ "type": "text",
+ "apiName": "var_mc1d8e6b",
+ "name": "alert_title",
+ "desc": "",
+ "mockData": "埋点数据异常告警"
+ }
+ ]
+}
\ No newline at end of file