初始化
This commit is contained in:
79
tashow-module/tashow-module-app/pom.xml
Normal file
79
tashow-module/tashow-module-app/pom.xml
Normal file
@@ -0,0 +1,79 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.tashow.cloud</groupId>
|
||||
<artifactId>tashow-module</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>tashow-module-app</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Registry 注册中心相关 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Config 配置中心相关 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RPC 远程调用相关 -->
|
||||
<dependency>
|
||||
<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>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<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>
|
||||
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 应用服务启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@ComponentScan(basePackages = {"com.tashow.cloud.app", "com.tashow.cloud.sdk.feishu"})
|
||||
public class AppServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AppServerApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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,64 @@
|
||||
package com.tashow.cloud.app.controller;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
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.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class FeishuController {
|
||||
private static final Logger log = LoggerFactory.getLogger(FeishuController.class);
|
||||
private static final String ACTION_COMPLETE_ALARM = "complete_alarm";
|
||||
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("/card")
|
||||
@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 && ACTION_COMPLETE_ALARM.equals(value.getStr("action"))) {
|
||||
String messageId = data.getStr("open_message_id");
|
||||
Map<String, Object> templateData = feiShuCardDataService.getCardData(messageId);
|
||||
templateData.put("open_id", data.getStr("open_id"));
|
||||
templateData.put("complete_time", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
JSONObject fromValue = action.getJSONObject("form_value");
|
||||
templateData.put("notes", fromValue.getStr("notes_input"));
|
||||
return feiShuAlertClient.buildCardWithData(larkConfig.getSuccessCards(), templateData);
|
||||
}
|
||||
}
|
||||
if (data.containsKey("encrypt")) {
|
||||
Decryptor decryptor = new Decryptor(larkConfig.getEncryptKey());
|
||||
return decryptor.decrypt(data.getStr("encrypt"));
|
||||
}
|
||||
return "{}";
|
||||
} catch (Exception e) {
|
||||
log.error("卡片处理异常", e);
|
||||
return "{\"code\":1,\"msg\":\"处理异常: " + e.getMessage() + "\"}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.tashow.cloud.app.controller;
|
||||
|
||||
|
||||
public class LoginController {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.tashow.cloud.app.controller;
|
||||
import com.tashow.cloud.app.mapper.BuriedPointMapper;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 测试控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class TestController {
|
||||
/**
|
||||
* 基础测试接口
|
||||
*/
|
||||
@GetMapping("/test")
|
||||
@PermitAll
|
||||
public String test() {
|
||||
return "test";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.tashow.cloud.app.dal.dataobject;
|
||||
// 数据库对象
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.tashow.cloud.app.dal.dto;
|
||||
// 视图层与业务层传输对象
|
||||
@@ -0,0 +1 @@
|
||||
package com.tashow.cloud.app.dal;
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.tashow.cloud.app.dal.vo;
|
||||
// 视图参数接收
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.tashow.cloud.app.interceptor;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
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.common.util.servlet.ServletUtils;
|
||||
import com.tashow.cloud.common.util.spring.SpringUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* 后端静默埋点拦截器
|
||||
* 用于收集API请求信息并异步发送到消息队列
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class BuriedPointInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final BuriedPointProducer buriedPointProducer;
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
if (!(handler instanceof HandlerMethod handlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
BuriedMessages message = new BuriedMessages(
|
||||
request,
|
||||
handlerMethod
|
||||
);
|
||||
buriedPointProducer.asyncSendMessage(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.tashow.cloud.app.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.tashow.cloud.app.model.MqMessageRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 埋点消息发送记录Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface BuriedPointFailRecordMapper extends BaseMapper<MqMessageRecord> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.tashow.cloud.app.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.tashow.cloud.app.model.BuriedPoint;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface BuriedPointMapper extends BaseMapper<BuriedPoint> {
|
||||
|
||||
@Select("SELECT * FROM app_burying WHERE event_id = #{eventId} LIMIT 1")
|
||||
BuriedPoint selectByEventId(@Param("eventId") Integer eventId);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.tashow.cloud.app.model;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.tashow.cloud.app.mq.message.BuriedMessages;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 埋点数据实体类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "app_burying")
|
||||
public class BuriedPoint {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Integer id;
|
||||
/**
|
||||
* 事件唯一ID
|
||||
*/
|
||||
@TableField(value = "event_id")
|
||||
private Integer eventId;
|
||||
|
||||
/**
|
||||
* 事件时间戳
|
||||
*/
|
||||
@TableField(value = "event_time")
|
||||
private Long eventTime;
|
||||
|
||||
/**
|
||||
* 服务名称
|
||||
*/
|
||||
@TableField(value = "service")
|
||||
private String service;
|
||||
|
||||
/**
|
||||
* 方法/接口
|
||||
*/
|
||||
@TableField(value = "method")
|
||||
private String method;
|
||||
|
||||
/**
|
||||
* 用户标识
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 会话标识
|
||||
*/
|
||||
@TableField(value = "session_id")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 客户端IP
|
||||
*/
|
||||
@TableField(value = "client_ip")
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 服务器IP
|
||||
*/
|
||||
@TableField(value = "server_ip")
|
||||
private String serverIp;
|
||||
|
||||
/**
|
||||
* 事件类型
|
||||
*/
|
||||
@TableField(value = "event_type")
|
||||
private String eventType;
|
||||
|
||||
/**
|
||||
* 页面路径/功能模块
|
||||
*/
|
||||
@TableField(value = "page_path")
|
||||
private String pagePath;
|
||||
|
||||
/**
|
||||
* 元素标识
|
||||
*/
|
||||
@TableField(value = "element_id")
|
||||
private String elementId;
|
||||
|
||||
/**
|
||||
* 操作时长(毫秒)
|
||||
*/
|
||||
@TableField(value = "duration")
|
||||
private Long duration;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time")
|
||||
private Date createTime;
|
||||
|
||||
|
||||
@TableField(value = "update_time")
|
||||
private Date updateTime;
|
||||
|
||||
@TableField(value = "status")
|
||||
private Integer status;
|
||||
|
||||
|
||||
public BuriedPoint(BuriedMessages message) {
|
||||
this.eventId = message.getId();
|
||||
this.eventTime = System.currentTimeMillis();
|
||||
this.userId = message.getUserId();
|
||||
this.eventType = message.getEventType();
|
||||
this.service = message.getService();
|
||||
this.method = message.getMethod();
|
||||
this.sessionId = message.getSessionId();
|
||||
this.clientIp = message.getClientIp();
|
||||
this.serverIp = message.getServerIp();
|
||||
this.status = message.getStatusCode();
|
||||
this.pagePath = message.getPagePath();
|
||||
this.elementId = message.getElementId();
|
||||
this.createTime = new Date();
|
||||
this.updateTime = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
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("mq_message_record")
|
||||
public class MqMessageRecord {
|
||||
/**
|
||||
* 状态常量定义
|
||||
*/
|
||||
public static final int STATUS_UNPROCESSED = 10; // 未处理
|
||||
public static final int STATUS_SUCCESS = 20; // 处理成功
|
||||
public static final int STATUS_FAILED = 30; // 发送失败
|
||||
@TableId
|
||||
private Integer id;
|
||||
|
||||
|
||||
/**
|
||||
* 交换机名称
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.tashow.cloud.app.mq.config;
|
||||
|
||||
import com.tashow.cloud.app.interceptor.BuriedPointInterceptor;
|
||||
import com.tashow.cloud.app.mq.message.BuriedMessages;
|
||||
import com.tashow.cloud.app.mq.producer.buriedPoint.BuriedPointProducer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.core.*;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 埋点功能配置类
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class BuriedPointConfiguration implements WebMvcConfigurer {
|
||||
|
||||
private final BuriedPointProducer buriedPointProducer;
|
||||
|
||||
/**
|
||||
* 创建埋点队列
|
||||
*/
|
||||
@Bean
|
||||
public Queue buriedPointQueue() {
|
||||
return new Queue(BuriedMessages.QUEUE, true, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建埋点交换机
|
||||
*/
|
||||
@Bean
|
||||
public DirectExchange buriedPointExchange() {
|
||||
return new DirectExchange(BuriedMessages.EXCHANGE, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建埋点绑定关系
|
||||
*/
|
||||
@Bean
|
||||
public Binding buriedPointBinding() {
|
||||
return BindingBuilder.bind(buriedPointQueue())
|
||||
.to(buriedPointExchange())
|
||||
.with(BuriedMessages.ROUTING_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建埋点拦截器
|
||||
*/
|
||||
@Bean
|
||||
public BuriedPointInterceptor buriedPointInterceptor() {
|
||||
return new BuriedPointInterceptor(buriedPointProducer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册埋点拦截器
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册拦截器,拦截所有请求
|
||||
registry.addInterceptor(buriedPointInterceptor())
|
||||
// 可以根据需要添加或排除特定路径
|
||||
.addPathPatterns("/**")
|
||||
// 排除静态资源、Swagger等路径
|
||||
.excludePathPatterns(
|
||||
"/swagger-ui/**",
|
||||
"/swagger-resources/**",
|
||||
"/v3/api-docs/**",
|
||||
"/webjars/**",
|
||||
"/static/**",
|
||||
"/card",
|
||||
"/error"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.tashow.cloud.app.mq.consumer.buriedPoint;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.tashow.cloud.app.mapper.BuriedPointMapper;
|
||||
import com.tashow.cloud.app.mapper.BuriedPointFailRecordMapper;
|
||||
import com.tashow.cloud.app.model.BuriedPoint;
|
||||
import com.tashow.cloud.app.model.MqMessageRecord;
|
||||
import com.tashow.cloud.app.mq.message.BuriedMessages;
|
||||
import com.tashow.cloud.app.service.feishu.BuriedPointMonitorService;
|
||||
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.Value;
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.Date;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
/**
|
||||
* 埋点消息消费者
|
||||
*/
|
||||
@Component
|
||||
@RabbitListener(queues = BuriedMessages.QUEUE)
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class BuriedPointConsumer extends AbstractRabbitMQConsumer<BuriedMessages> {
|
||||
private final BuriedPointMapper buriedPointMapper;
|
||||
private final BuriedPointMonitorService buriedPointMonitorService;
|
||||
|
||||
@Value("${spring.application.name:tashow-app}")
|
||||
private String applicationName;
|
||||
|
||||
@RabbitHandler
|
||||
public void handleMessage(BuriedMessages message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
|
||||
onMessage(message, channel, deliveryTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理埋点消息
|
||||
* @param message 消息对象
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean processMessage(BuriedMessages message) {
|
||||
try {
|
||||
BuriedPoint existingPoint = buriedPointMapper.selectByEventId(message.getId());
|
||||
if (existingPoint != null) {
|
||||
existingPoint.setStatus(message.getStatusCode());
|
||||
existingPoint.setUpdateTime(new Date());
|
||||
return buriedPointMapper.updateById(existingPoint) > 0;
|
||||
}
|
||||
BuriedPoint buriedPoint = new BuriedPoint(message);
|
||||
buriedPoint.setService(applicationName);
|
||||
buriedPointMapper.insert(buriedPoint);
|
||||
|
||||
if(buriedPoint.getStatus() == BuriedMessages.STATUS_ERROR){
|
||||
buriedPointMonitorService.checkFailRecordsAndAlert("埋点数据处理异常");
|
||||
}
|
||||
return true;
|
||||
} catch (DuplicateKeyException e) {
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[埋点消费者] 保存数据失败", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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.MqMessageRecord;
|
||||
import com.tashow.cloud.app.service.feishu.BuriedPointMonitorService;
|
||||
import com.tashow.cloud.mq.handler.FailRecordHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.Date;
|
||||
/**
|
||||
* MQ消息记录处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class BuriedPointFailRecordHandler implements FailRecordHandler {
|
||||
private final BuriedPointFailRecordMapper buriedPointFailRecordMapper;
|
||||
private final BuriedPointMonitorService buriedPointMonitorService;
|
||||
/**
|
||||
* 保存消息记录=
|
||||
*/
|
||||
@Override
|
||||
public void saveMessageRecord(Integer id, String exchange, String routingKey, String cause, String messageContent, int status) {
|
||||
try {
|
||||
MqMessageRecord existingRecord = findExistingRecord(id);
|
||||
if (existingRecord != null) {
|
||||
existingRecord.setRetryCount(existingRecord.getRetryCount() + 1);
|
||||
existingRecord.setMessageContent(messageContent);
|
||||
existingRecord.setStatus(status);
|
||||
existingRecord.setCause(cause);
|
||||
existingRecord.setUpdateTime(new Date());
|
||||
buriedPointFailRecordMapper.updateById(existingRecord);
|
||||
} else {
|
||||
MqMessageRecord record = new MqMessageRecord();
|
||||
record.setId(id);
|
||||
record.setExchange(exchange);
|
||||
record.setRoutingKey(routingKey);
|
||||
record.setCause(cause);
|
||||
record.setMessageContent(messageContent);
|
||||
record.setRetryCount(0);
|
||||
record.setStatus(status);
|
||||
record.setCreateTime(new Date());
|
||||
record.setUpdateTime(new Date());
|
||||
buriedPointFailRecordMapper.insert(record);
|
||||
if (status == MqMessageRecord.STATUS_FAILED) {
|
||||
buriedPointMonitorService.checkFailRecordsAndAlert(cause);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[MQ消息处理器] 保存消息记录异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息状态
|
||||
*/
|
||||
@Override
|
||||
public void updateMessageStatus(Integer id) {
|
||||
try {
|
||||
MqMessageRecord record = findExistingRecord(id);
|
||||
if (record != null) {
|
||||
record.setStatus(MqMessageRecord.STATUS_SUCCESS);
|
||||
record.setUpdateTime(new Date());
|
||||
buriedPointFailRecordMapper.updateById(record);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[MQ消息处理器] 更新消息状态异常: {}", id, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息状态并设置失败原因
|
||||
*/
|
||||
@Override
|
||||
public void updateMessageStatusWithCause(Integer id, String cause) {
|
||||
try {
|
||||
MqMessageRecord record = findExistingRecord(id);
|
||||
if (record != null) {
|
||||
record.setStatus(MqMessageRecord.STATUS_FAILED);
|
||||
record.setCause(cause);
|
||||
record.setUpdateTime(new Date());
|
||||
buriedPointFailRecordMapper.updateById(record);
|
||||
buriedPointMonitorService.checkFailRecordsAndAlert(cause);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[MQ消息处理器] 更新消息状态和原因异常: {}", id, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找已存在的失败记录
|
||||
*/
|
||||
private MqMessageRecord findExistingRecord(Integer id) {
|
||||
LambdaQueryWrapper<MqMessageRecord> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(MqMessageRecord::getId, id);
|
||||
return buriedPointFailRecordMapper.selectOne(queryWrapper);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.tashow.cloud.app.mq.message;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.tashow.cloud.common.util.json.JsonUtils;
|
||||
import com.tashow.cloud.common.util.servlet.ServletUtils;
|
||||
import com.tashow.cloud.common.util.spring.SpringUtils;
|
||||
import com.tashow.cloud.mq.core.BaseMqMessage;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.tashow.cloud.web.apilog.core.interceptor.ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD;
|
||||
|
||||
/**
|
||||
* 埋点消息
|
||||
*/
|
||||
@Data
|
||||
public class BuriedMessages extends BaseMqMessage {
|
||||
private static final String ATTRIBUTE_REQUEST_ID = "BuriedPoint.RequestId";
|
||||
|
||||
|
||||
/**
|
||||
* 交换机名称
|
||||
*/
|
||||
public static final String EXCHANGE = "tashow.buried.point.exchange";
|
||||
|
||||
/**
|
||||
* 队列名称
|
||||
*/
|
||||
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 String service;
|
||||
|
||||
public BuriedMessages() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求创建埋点消息
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param handlerMethod 处理方法
|
||||
*/
|
||||
public BuriedMessages(HttpServletRequest request, HandlerMethod handlerMethod) {
|
||||
try {
|
||||
int requestId = (int)(Math.abs(IdUtil.getSnowflakeNextId()) % Integer.MAX_VALUE);
|
||||
this.setId(requestId);
|
||||
this.eventTime = new Date();
|
||||
this.service = SpringUtils.getApplicationName();
|
||||
this.method = request.getMethod() + " " + request.getRequestURI() +
|
||||
JsonUtils.toJsonString(request.getParameterMap());
|
||||
Object userId = request.getSession().getAttribute("USER_ID");
|
||||
this.userId = userId != null ? userId.toString() : "anonymous";
|
||||
this.sessionId = request.getSession().getId();
|
||||
this.clientIp = ServletUtils.getClientIP(request);
|
||||
try {
|
||||
this.serverIp = InetAddress.getLocalHost().getHostAddress();
|
||||
} catch (UnknownHostException e) {
|
||||
this.serverIp = "unknown";
|
||||
}
|
||||
String controllerName = handlerMethod.getBeanType().getSimpleName();
|
||||
String actionName = handlerMethod.getMethod().getName();
|
||||
this.pagePath = controllerName + "#" + actionName;
|
||||
this.eventType = "API_REQUEST_START";
|
||||
this.setStatusCode(STATUS_PROCESSING);
|
||||
request.setAttribute(ATTRIBUTE_REQUEST_ID, this.getId());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("创建埋点消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.tashow.cloud.app.mq.producer.buriedPoint;
|
||||
|
||||
import com.tashow.cloud.app.mq.message.BuriedMessages;
|
||||
import com.tashow.cloud.common.util.json.JsonUtils;
|
||||
import com.tashow.cloud.mq.rabbitmq.producer.AbstractRabbitMQProducer;
|
||||
import org.springframework.amqp.core.ReturnedMessage;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 埋点消息生产者
|
||||
*/
|
||||
@Component
|
||||
public class BuriedPointProducer extends AbstractRabbitMQProducer<BuriedMessages> {
|
||||
|
||||
@Override
|
||||
public void returnedMessage(ReturnedMessage returned) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExchange() {
|
||||
return BuriedMessages.EXCHANGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRoutingKey() {
|
||||
return BuriedMessages.ROUTING_KEY;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.tashow.cloud.app.security.config;
|
||||
|
||||
import com.tashow.cloud.infraapi.enums.ApiConstants;
|
||||
import com.tashow.cloud.security.security.config.AuthorizeRequestsCustomizer;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
|
||||
/**
|
||||
* Infra 模块的 Security 配置
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false, value = "infraSecurityConfiguration")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Value("${spring.boot.admin.context-path:''}")
|
||||
private String adminSeverContextPath;
|
||||
|
||||
@Bean("infraAuthorizeRequestsCustomizer")
|
||||
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
|
||||
return new AuthorizeRequestsCustomizer() {
|
||||
@Override
|
||||
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
// Spring Boot Actuator 的安全配置
|
||||
registry.requestMatchers("/actuator").permitAll()
|
||||
.requestMatchers("/actuator/**").permitAll();
|
||||
// Druid 监控
|
||||
registry.requestMatchers("/druid/**").permitAll();
|
||||
// Spring Boot Admin Server 的安全配置
|
||||
registry.requestMatchers(adminSeverContextPath).permitAll()
|
||||
.requestMatchers(adminSeverContextPath + "/**").permitAll();
|
||||
// 文件读取
|
||||
registry.requestMatchers(buildAdminApi("/infra/file/*/get/**")).permitAll();
|
||||
|
||||
// TODO 芋艿:这个每个项目都需要重复配置,得捉摸有没通用的方案
|
||||
// RPC 服务的安全配置
|
||||
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位
|
||||
*/
|
||||
package com.tashow.cloud.app.security.core;
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.tashow.cloud.app.service.feishu;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.tashow.cloud.app.mapper.BuriedPointFailRecordMapper;
|
||||
import com.tashow.cloud.app.mapper.BuriedPointMapper;
|
||||
import com.tashow.cloud.app.model.BuriedPoint;
|
||||
import com.tashow.cloud.app.model.MqMessageRecord;
|
||||
import com.tashow.cloud.app.mq.message.BuriedMessages;
|
||||
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.stereotype.Service;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 埋点监控服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BuriedPointMonitorService {
|
||||
private static final int ALERT_THRESHOLD = 3;
|
||||
private static final int MONITORING_HOURS = 12;
|
||||
private final Map<String, Long> alertCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final BuriedPointFailRecordMapper buriedPointFailRecordMapper;
|
||||
private final BuriedPointMapper buriedPointMapper;
|
||||
private final FeiShuAlertClient feiShuAlertClient;
|
||||
private final FeiShuCardDataService feiShuCardDataService;
|
||||
private final LarkConfig larkConfig;
|
||||
|
||||
/**
|
||||
* 检查失败记录并发送告警
|
||||
*/
|
||||
public boolean checkFailRecordsAndAlert(String cause) {
|
||||
try {
|
||||
Date now = new Date();
|
||||
Date hoursAgo = getDateHoursAgo(now, MONITORING_HOURS);
|
||||
boolean sentAlert = false;
|
||||
List<Date[]> timeRanges = getHourRanges(hoursAgo, now);
|
||||
long mqFailCount = countFailures(buriedPointFailRecordMapper, MqMessageRecord.class, hoursAgo, now);
|
||||
long buriedFailCount = countFailures(buriedPointMapper, BuriedPoint.class, hoursAgo, now);
|
||||
if (mqFailCount > ALERT_THRESHOLD||buriedFailCount > ALERT_THRESHOLD) {
|
||||
if (!hasRecentlySentAlert(cause)) {
|
||||
sendAlert(mqFailCount, cause, getMqStats(timeRanges));
|
||||
alertCache.put(cause, System.currentTimeMillis());
|
||||
sentAlert = true;
|
||||
}
|
||||
}
|
||||
|
||||
return sentAlert;
|
||||
} catch (Exception e) {
|
||||
log.error("[埋点监控] 检查失败记录异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否最近已发送过相同类型的告警
|
||||
*/
|
||||
private boolean hasRecentlySentAlert(String alertType) {
|
||||
Long lastSentTime = alertCache.get(alertType);
|
||||
if (lastSentTime == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long hourInMillis = MONITORING_HOURS * 60 * 60 * 1000L;
|
||||
return (System.currentTimeMillis() - lastSentTime) < hourInMillis;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取消息队列统计数据
|
||||
*/
|
||||
private List<ChartImageGenerator.MonitoringDataPoint> getMqStats(List<Date[]> timeRanges) {
|
||||
Map<Date, Integer> successData = batchQueryMqStatus(timeRanges, MqMessageRecord.STATUS_SUCCESS);
|
||||
Map<Date, Integer> failedData = batchQueryMqFailures(timeRanges);
|
||||
|
||||
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:00");
|
||||
return timeRanges.stream()
|
||||
.map(range -> new ChartImageGenerator.MonitoringDataPoint(
|
||||
timeFormat.format(range[0]),
|
||||
successData.getOrDefault(range[0], 0),
|
||||
failedData.getOrDefault(range[0], 0)
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取埋点表统计数据
|
||||
*/
|
||||
private List<ChartImageGenerator.MonitoringDataPoint> getBuriedStats(List<Date[]> timeRanges) {
|
||||
// 批量查询每个时间区间的数据
|
||||
Map<Date, Integer> successData = batchQueryBuriedStatus(timeRanges, BuriedMessages.STATUS_SUCCESS);
|
||||
Map<Date, Integer> failedData = batchQueryBuriedStatus(timeRanges, BuriedMessages.STATUS_ERROR);
|
||||
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:00");
|
||||
return timeRanges.stream()
|
||||
.map(range -> new ChartImageGenerator.MonitoringDataPoint(
|
||||
timeFormat.format(range[0]),
|
||||
successData.getOrDefault(range[0], 0),
|
||||
failedData.getOrDefault(range[0], 0)
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询MQ状态数据
|
||||
*/
|
||||
private Map<Date, Integer> batchQueryMqStatus(List<Date[]> timeRanges, int status) {
|
||||
Map<Date, Integer> result = new HashMap<>();
|
||||
for (Date[] range : timeRanges) {
|
||||
LambdaQueryWrapper<MqMessageRecord> query = new LambdaQueryWrapper<>();
|
||||
query.ge(MqMessageRecord::getCreateTime, range[0])
|
||||
.lt(MqMessageRecord::getCreateTime, range[1])
|
||||
.eq(MqMessageRecord::getStatus, status);
|
||||
result.put(range[0], buriedPointFailRecordMapper.selectCount(query).intValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询MQ失败数据
|
||||
*/
|
||||
private Map<Date, Integer> batchQueryMqFailures(List<Date[]> timeRanges) {
|
||||
Map<Date, Integer> result = new HashMap<>();
|
||||
for (Date[] range : timeRanges) {
|
||||
LambdaQueryWrapper<MqMessageRecord> query = new LambdaQueryWrapper<>();
|
||||
query.ge(MqMessageRecord::getCreateTime, range[0])
|
||||
.lt(MqMessageRecord::getCreateTime, range[1])
|
||||
.in(MqMessageRecord::getStatus, Arrays.asList(
|
||||
MqMessageRecord.STATUS_UNPROCESSED, MqMessageRecord.STATUS_FAILED));
|
||||
result.put(range[0], buriedPointFailRecordMapper.selectCount(query).intValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询埋点状态数据
|
||||
*/
|
||||
private Map<Date, Integer> batchQueryBuriedStatus(List<Date[]> timeRanges, int status) {
|
||||
Map<Date, Integer> result = new HashMap<>();
|
||||
for (Date[] range : timeRanges) {
|
||||
LambdaQueryWrapper<BuriedPoint> query = new LambdaQueryWrapper<>();
|
||||
query.ge(BuriedPoint::getCreateTime, range[0])
|
||||
.lt(BuriedPoint::getCreateTime, range[1])
|
||||
.eq(BuriedPoint::getStatus, status);
|
||||
result.put(range[0], buriedPointMapper.selectCount(query).intValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算失败数量
|
||||
*/
|
||||
private <T> long countFailures(Object mapper, Class<T> entityClass, Date startDate, Date endDate) {
|
||||
try {
|
||||
if (entityClass == BuriedPoint.class) {
|
||||
LambdaQueryWrapper<BuriedPoint> query = new LambdaQueryWrapper<>();
|
||||
query.ge(BuriedPoint::getCreateTime, startDate)
|
||||
.le(BuriedPoint::getCreateTime, endDate)
|
||||
.eq(BuriedPoint::getStatus, BuriedMessages.STATUS_ERROR);
|
||||
return ((BuriedPointMapper)mapper).selectCount(query);
|
||||
} else {
|
||||
LambdaQueryWrapper<MqMessageRecord> query = new LambdaQueryWrapper<>();
|
||||
query.ge(MqMessageRecord::getCreateTime, startDate)
|
||||
.le(MqMessageRecord::getCreateTime, endDate)
|
||||
.eq(MqMessageRecord::getStatus, MqMessageRecord.STATUS_FAILED);
|
||||
return ((BuriedPointFailRecordMapper)mapper).selectCount(query);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送告警
|
||||
*/
|
||||
private void sendAlert(long failCount, String alertMsg, List<ChartImageGenerator.MonitoringDataPoint> data) {
|
||||
try {
|
||||
String imageKey = feiShuAlertClient.uploadImage(data, alertMsg);
|
||||
String title = alertMsg.split(":")[0].trim();
|
||||
|
||||
Map<String, Object> templateData = Map.of(
|
||||
"alert_title", title,
|
||||
"image_key", Map.of("img_key", imageKey),
|
||||
"current_time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),
|
||||
"fail_count", failCount
|
||||
);
|
||||
|
||||
String messageId = feiShuAlertClient.sendCardMessage(
|
||||
larkConfig.getChatId(),
|
||||
larkConfig.getExceptionCards(),
|
||||
new HashMap<>(templateData)
|
||||
);
|
||||
feiShuCardDataService.saveCardData(messageId, templateData);
|
||||
} catch (Exception e) {
|
||||
log.error("[埋点监控] 发送告警失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小时范围列表
|
||||
*/
|
||||
private List<Date[]> getHourRanges(Date startDate, Date endDate) {
|
||||
List<Date[]> ranges = new ArrayList<>();
|
||||
Calendar cal = Calendar.getInstance();
|
||||
|
||||
cal.setTime(endDate);
|
||||
cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
Date endHour = cal.getTime();
|
||||
|
||||
cal.add(Calendar.HOUR_OF_DAY, -(MONITORING_HOURS - 1));
|
||||
Date startHour = startDate.after(cal.getTime()) ? startDate : cal.getTime();
|
||||
|
||||
cal.setTime(startHour);
|
||||
while (!cal.getTime().after(endHour)) {
|
||||
Date hourStart = cal.getTime();
|
||||
cal.add(Calendar.HOUR_OF_DAY, 1);
|
||||
Date hourEnd = cal.getTime().after(endDate) ? endDate : cal.getTime();
|
||||
ranges.add(new Date[]{hourStart, hourEnd});
|
||||
|
||||
if (hourEnd.equals(endDate)) break;
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间前N小时的时间
|
||||
*/
|
||||
private Date getDateHoursAgo(Date date, int hours) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(date);
|
||||
calendar.add(Calendar.HOUR_OF_DAY, -hours);
|
||||
return calendar.getTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.tashow.cloud.app.service.feishu;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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 {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FeiShuCardDataService.class);
|
||||
private static final String REDIS_KEY_PREFIX = "feishu:card:";
|
||||
private static final int CARD_EXPIRATION_DAYS = 30;
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
public FeiShuCardDataService(StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存卡片数据到Redis
|
||||
*/
|
||||
public boolean saveCardData(String messageId, Map<String, Object> data) {
|
||||
if (messageId == null || data == null) return false;
|
||||
|
||||
try {
|
||||
String jsonData = objectMapper.writeValueAsString(data);
|
||||
stringRedisTemplate.opsForValue().set(
|
||||
REDIS_KEY_PREFIX + messageId,
|
||||
jsonData,
|
||||
CARD_EXPIRATION_DAYS,
|
||||
TimeUnit.DAYS
|
||||
);
|
||||
return true;
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("保存卡片数据失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Redis获取卡片数据
|
||||
*/
|
||||
public Map<String, Object> getCardData(String messageId) {
|
||||
try {
|
||||
String redisKey = REDIS_KEY_PREFIX + messageId;
|
||||
String jsonData = stringRedisTemplate.opsForValue().get(redisKey);
|
||||
|
||||
if (jsonData == null) return new HashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> result = objectMapper.readValue(jsonData, Map.class);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("获取卡片数据失败: {}", e.getMessage());
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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.MqMessageRecord;
|
||||
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<MqMessageRecord> {
|
||||
|
||||
private final BuriedPointFailRecordMapper buriedPointFailRecordMapper;
|
||||
private final BuriedPointProducer buriedPointProducer;
|
||||
|
||||
/**
|
||||
* 获取未处理的失败记录
|
||||
*/
|
||||
@Override
|
||||
public List<MqMessageRecord> getUnprocessedRecords() {
|
||||
LambdaQueryWrapper<MqMessageRecord> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(MqMessageRecord::getStatus, MqMessageRecord.STATUS_FAILED)
|
||||
.orderByAsc(MqMessageRecord::getCreateTime);
|
||||
return buriedPointFailRecordMapper.selectList(queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试失败消息
|
||||
*/
|
||||
@Override
|
||||
public void retryFailedMessage(Integer recordId) {
|
||||
try {
|
||||
Long id = Long.valueOf(recordId);
|
||||
MqMessageRecord record = buriedPointFailRecordMapper.selectById(id);
|
||||
BuriedMessages message = JsonUtils.parseObject(record.getMessageContent(), BuriedMessages.class);
|
||||
buriedPointProducer.asyncSendMessage(message);
|
||||
} catch (Exception e) {
|
||||
log.error("[埋点重试] 重试失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.tashow.cloud.app.task;
|
||||
import com.tashow.cloud.app.model.MqMessageRecord;
|
||||
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<MqMessageRecord> {
|
||||
|
||||
private final BuriedPointFailRecordService buriedPointFailRecordService;
|
||||
|
||||
/**
|
||||
* 定时重试失败消息
|
||||
* 每天凌晨执行一次
|
||||
*/
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
// @Scheduled(cron = "0/10 * * * * ?")
|
||||
public void execute() {
|
||||
retryFailedMessages();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageRetryService<MqMessageRecord> getMessageRetryService() {
|
||||
return buriedPointFailRecordService;
|
||||
}
|
||||
@Override
|
||||
protected Integer getRecordId(MqMessageRecord record) {
|
||||
return record.getId();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
--- #################### 注册中心 + 配置中心相关配置 ####################
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: 43.139.42.137:8848 # Nacos 服务器地址
|
||||
username: nacos # Nacos 账号
|
||||
password: nacos # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: 5c8b8fe6-9a89-4ae3-975e-ef3bf560ff82 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
metadata:
|
||||
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
|
||||
config: # 【注册中心】配置项
|
||||
namespace: 5c8b8fe6-9a89-4ae3-975e-ef3bf560ff82 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
@@ -0,0 +1,12 @@
|
||||
server:
|
||||
port: 48083
|
||||
spring:
|
||||
application:
|
||||
name: app-server
|
||||
profiles:
|
||||
active: local
|
||||
config:
|
||||
import:
|
||||
- optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置
|
||||
- optional:nacos:application.yaml # 加载【Nacos】的配置
|
||||
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置
|
||||
Reference in New Issue
Block a user