初始化

This commit is contained in:
xuelijun
2025-09-20 18:41:07 +08:00
commit a2d1fcde12
1437 changed files with 95746 additions and 0 deletions

View 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>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package com.tashow.cloud.app.controller;
public class LoginController {
}

View File

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

View File

@@ -0,0 +1,2 @@
package com.tashow.cloud.app.dal.dataobject;
// 数据库对象

View File

@@ -0,0 +1,2 @@
package com.tashow.cloud.app.dal.dto;
// 视图层与业务层传输对象

View File

@@ -0,0 +1 @@
package com.tashow.cloud.app.dal;

View File

@@ -0,0 +1,2 @@
package com.tashow.cloud.app.dal.vo;
// 视图参数接收

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
/**
* 占位
*/
package com.tashow.cloud.app.security.core;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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】的配置