This commit is contained in:
2025-06-18 17:14:27 +08:00
parent e384dc1163
commit 98bb3529ea
41 changed files with 2776 additions and 335 deletions

View File

@@ -2,32 +2,42 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework</artifactId>
<groupId>com.tashow.cloud</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tashow-framework-mq</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种</description>
<description>消息队列模块基于RabbitMQ等中间件</description>
<url>https://github.com/tashow/tashow-platform</url>
<dependencies>
<!-- DB 相关 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-data-redis</artifactId>
</dependency>
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<optional>true</optional>
</dependency>
<!-- Web Services -->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

View File

@@ -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<String, Object> 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<String, Object> getExtraData() {
return extraData;
}
public void setExtraData(Map<String, Object> extraData) {
this.extraData = extraData;
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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 <T> 消息类型
* @author tashow
*/
public abstract class AbstractRabbitMQConsumer<T extends BaseMqMessage> {
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());
}
}
}

View File

@@ -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 <T> 消息类型
* @author tashow
*/
public abstract class AbstractRabbitMQProducer<T extends BaseMqMessage>
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, 无法保存失败记录");
}
}
}

View File

@@ -0,0 +1,91 @@
package com.tashow.cloud.mq.retry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* 消息重试任务抽象实现
*
* @param <T> 失败记录类型
* @author tashow
*/
public abstract class AbstractMessageRetryTask<T> {
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<T> 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<T> 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);
}
}
}

View File

@@ -0,0 +1,36 @@
package com.tashow.cloud.mq.retry;
import java.util.List;
/**
* 消息重试服务接口
*
* @param <T> 失败记录类型
* @author tashow
*/
public interface MessageRetryService<T> {
/**
* 获取未处理的失败记录
*
* @return 失败记录列表
*/
List<T> getUnprocessedRecords();
/**
* 重试失败消息
*
* @param recordId 记录ID
* @return 重试结果
*/
boolean retryFailedMessage(String recordId);
/**
* 更新记录状态
*
* @param record 记录对象
* @param status 记录状态
* @return 更新结果
*/
boolean updateStatus(T record, int status);
}

View File

@@ -28,11 +28,27 @@
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-rpc</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-data-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-web</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-env</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-infra-api</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-data-redis</artifactId>
@@ -41,5 +57,23 @@
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-feishu-sdk</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-data-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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) {

View File

@@ -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();
}
}

View File

@@ -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);
}*/
}

View File

@@ -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;
}
}

View File

@@ -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<String, Object> 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() + "\"}";
}
}
}

View File

@@ -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;

View File

@@ -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<String, List<String>> toHeaderMap(HttpServletRequest req) {
Map<String, List<String>> headers = new HashMap<>();
Enumeration<String> names = req.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
List<String> 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<String> values = keyValues.getValue();
values.stream().forEach(v -> response.addHeader(key, v));
});
if (eventResp.getBody() != null) {
response.getWriter().write(new String(eventResp.getBody()));
}
}
}

View File

@@ -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);
}
}

View File

@@ -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()) {

View File

@@ -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<BuriedPointFailRecord> {
}

View File

@@ -33,7 +33,7 @@ public class BuriedPoint {
*/
@TableField(value = "event_time")
private Long eventTime;
/**
* 服务名称
*/

View File

@@ -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;
}

View File

@@ -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<BuriedPointFailRecord> 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"
);
}
}
}

View File

@@ -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<BuriedMessages> {
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<BuriedPointFailRecord> 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<BuriedPointFailRecord> 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<BuriedPointFailRecord> 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<ChartImageGenerator.MonitoringDataPoint> 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<ChartImageGenerator.MonitoringDataPoint> queryHourlyFailRecordData(Date startDate, Date endDate) {
List<ChartImageGenerator.MonitoringDataPoint> 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<BuriedPointFailRecord> successQuery = new LambdaQueryWrapper<>();
successQuery.ge(BuriedPointFailRecord::getCreateTime, currentHourStart)
.lt(BuriedPointFailRecord::getCreateTime, nextHourStart)
.eq(BuriedPointFailRecord::getStatus, BuriedPointFailRecord.STATUS_SUCCESS); // 处理成功
Long successCount = buriedPointFailRecordMapper.selectCount(successQuery);
// 查询处理失败或未处理的记录数量
LambdaQueryWrapper<BuriedPointFailRecord> 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();
}
}
}

View File

@@ -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<BuriedPointFailRecord> 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<BuriedPointFailRecord> 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<ChartImageGenerator.MonitoringDataPoint> 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<ChartImageGenerator.MonitoringDataPoint> queryHourlyFailRecordData(Date startDate, Date endDate) {
List<ChartImageGenerator.MonitoringDataPoint> 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<BuriedPointFailRecord> successQuery = new LambdaQueryWrapper<>();
successQuery.ge(BuriedPointFailRecord::getCreateTime, currentHourStart).lt(BuriedPointFailRecord::getCreateTime, nextHourStart).eq(BuriedPointFailRecord::getStatus, BuriedPointFailRecord.STATUS_SUCCESS); // 处理成功
Long successCount = buriedPointFailRecordMapper.selectCount(successQuery);
// 查询处理失败或未处理的记录数量
LambdaQueryWrapper<BuriedPointFailRecord> 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();
}
}
}

View File

@@ -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<String, Object> 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;
}

View File

@@ -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<BuriedMessages> {
/**
* 确认消息是否成功发送到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);
}
}

View File

@@ -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<String, Object> 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<String, Object> getCardData(String messageId) {
try {
String jsonData = stringRedisTemplate.opsForValue().get(messageId);
return objectMapper.readValue(jsonData, Map.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -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<BuriedPointFailRecord> {
private final BuriedPointFailRecordMapper buriedPointFailRecordMapper;
private final BuriedPointProducer buriedPointProducer;
/**
* 获取未处理的失败记录
*/
@Override
public List<BuriedPointFailRecord> getUnprocessedRecords() {
LambdaQueryWrapper<BuriedPointFailRecord> 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;
}
}
}

View File

@@ -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<BuriedPointFailRecord> {
private final BuriedPointFailRecordService buriedPointFailRecordService;
/**
* 定时重试失败消息
* 每天凌晨执行一次
*/
@Scheduled(cron = "0 0 0 * * ?")
public void execute() {
retryFailedMessages();
}
@Override
protected MessageRetryService<BuriedPointFailRecord> getMessageRetryService() {
return buriedPointFailRecordService;
}
@Override
protected String getRecordId(BuriedPointFailRecord record) {
return String.valueOf(record.getId());
}
@Override
protected String getCorrelationId(BuriedPointFailRecord record) {
return record.getCorrelationId();
}
}

View File

@@ -12,6 +12,7 @@
<modules>
<module>tashow-sdk-payment</module>
<module>tashow-feishu-sdk</module>
</modules>
</project>

View File

@@ -0,0 +1,44 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-sdk</artifactId>
<version>${revision}</version>
</parent>
<artifactId>tashow-feishu-sdk</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-data-redis</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.larksuite.oapi/oapi-sdk -->
<dependency>
<groupId>com.larksuite.oapi</groupId>
<artifactId>oapi-sdk</artifactId>
<version>2.4.18</version>
</dependency>
<dependency>
<artifactId>httpclient</artifactId>
<groupId>org.apache.httpcomponents</groupId>
<version>4.5.13</version>
</dependency>
<dependency>
<artifactId>junit</artifactId>
<groupId>junit</groupId>
<scope>test</scope>
<version>4.13.2</version>
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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<String, Object> data);
/**
* 获取卡片数据
* @param messageId 消息ID
* @return 卡片数据
*/
Map<String, Object> 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<ChartImageGenerator.MonitoringDataPoint> 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<ChartImageGenerator.MonitoringDataPoint> buriedPointData, int failCount, String errorMessage) throws Exception {
HashMap<String, Object> 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<ChartImageGenerator.MonitoringDataPoint> 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<ChartImageGenerator.MonitoringDataPoint> 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<String, Object> 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<ChartImageGenerator.MonitoringDataPoint> 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<ChartImageGenerator.MonitoringDataPoint> 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<String, Object> 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<String, Object> 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;
}
}

View File

@@ -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<String, String> 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();
}
}

View File

@@ -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;
}

View File

@@ -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<MonitoringDataPoint> monitoringData) throws IOException {
generateDashboardImage(outputFile, monitoringData, null);
}
/**
* 生成监控仪表盘图像(带错误信息)
* @param outputFile 输出文件
* @param monitoringData 监控数据
* @param errorMessage 错误信息如为null则不显示
* @throws IOException 如果图像创建失败
*/
public void generateDashboardImage(File outputFile, List<MonitoringDataPoint> 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<MonitoringDataPoint> 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<MonitoringDataPoint> 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<MonitoringDataPoint> 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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": "<font color=\"grey\">负责人</font>\n<at id=all></at>",
"i18n_content": {
"en_us": "<font color=\"grey\">Alert details</font>\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": "<font color=\"grey\">失败数量</font>\n${fail_count}",
"i18n_content": {
"en_us": "<font color=\"grey\">Diagnostic info</font>\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": "<font color=\"grey\">项目</font>\nTashow平台",
"i18n_content": {
"en_us": "<font color=\"grey\">Priority level</font>\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": "<font color=\"grey\">告警时间</font>\n${current_time}",
"i18n_content": {
"en_us": "<font color=\"grey\">Incident time</font>\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": "埋点数据异常告警"
}
]
}