提交
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}*/
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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() + "\"}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class BuriedPoint {
|
||||
*/
|
||||
@TableField(value = "event_time")
|
||||
private Long eventTime;
|
||||
|
||||
|
||||
/**
|
||||
* 服务名称
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user