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

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