From 98bb3529eae03aa20bf636e60ce11a8b61d75ed0 Mon Sep 17 00:00:00 2001 From: ZiJIe <17738440858@163.com> Date: Wed, 18 Jun 2025 17:14:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- logs/system-server.log.2025-04-22.0.gz | Bin 3551 -> 0 bytes tashow-framework/tashow-framework-mq/pom.xml | 30 +- .../tashow/cloud/mq/core/BaseMqMessage.java | 115 +++++++ .../cloud/mq/core/CustomCorrelationData.java | 44 +++ .../cloud/mq/handler/FailRecordHandler.java | 39 +++ .../com/tashow/cloud/mq/package-info.java | 1 - .../config/RabbitMQAutoConfiguration.java | 23 +- .../config/RabbitMQConfiguration.java | 49 +++ .../consumer/AbstractRabbitMQConsumer.java | 174 ++++++++++ .../producer/AbstractRabbitMQProducer.java | 145 ++++++++ .../mq/retry/AbstractMessageRetryTask.java | 91 +++++ .../cloud/mq/retry/MessageRetryService.java | 36 ++ tashow-module/tashow-module-app/pom.xml | 36 +- .../cloud/app/AppServerApplication.java | 5 +- .../tashow/cloud/app/config/AppConfig.java | 20 ++ .../cloud/app/config/FeiShuClientConfig.java | 24 ++ .../tashow/cloud/app/config/FeishuConfig.java | 26 ++ .../app/controller/FeishuController.java | 61 ++++ .../cloud/app/controller/TestController.java | 1 - .../tashow/cloud/app/ext/HttpTranslator.java | 47 +++ .../tashow/cloud/app/ext/ServletAdapter.java | 57 +++ .../interceptor/BuriedPointInterceptor.java | 5 +- .../mapper/BuriedPointFailRecordMapper.java | 12 + .../tashow/cloud/app/model/BuriedPoint.java | 2 +- .../app/model/BuriedPointFailRecord.java | 72 ++++ .../mq/config/BuriedPointConfiguration.java | 104 +----- .../buriedPoint/BuriedPointConsumer.java | 254 +++++++++----- .../handler/BuriedPointFailRecordHandler.java | 156 +++++++++ .../cloud/app/mq/message/BuriedMessages.java | 142 +++++--- .../buriedPoint/BuriedPointProducer.java | 64 +--- .../service/feishu/FeiShuCardDataService.java | 65 ++++ .../impl/BuriedPointFailRecordService.java | 85 +++++ .../cloud/app/task/BuriedPointRetryTask.java | 45 +++ tashow-sdk/pom.xml | 1 + tashow-sdk/tashow-feishu-sdk/pom.xml | 44 +++ .../sdk/feishu/client/FeiShuAlertClient.java | 274 +++++++++++++++ .../feishu/client/FeiShuMessageClient.java | 109 ++++++ .../cloud/sdk/feishu/config/LarkConfig.java | 35 ++ .../sdk/feishu/util/ChartImageGenerator.java | 324 ++++++++++++++++++ .../cloud/sdk/feishu/util/LarkClientUtil.java | 28 ++ .../src/main/resources/card.json | 266 ++++++++++++++ 41 files changed, 2776 insertions(+), 335 deletions(-) delete mode 100644 logs/system-server.log.2025-04-22.0.gz create mode 100644 tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/BaseMqMessage.java create mode 100644 tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/CustomCorrelationData.java create mode 100644 tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/handler/FailRecordHandler.java delete mode 100644 tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/package-info.java create mode 100644 tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQConfiguration.java create mode 100644 tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/consumer/AbstractRabbitMQConsumer.java create mode 100644 tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/producer/AbstractRabbitMQProducer.java create mode 100644 tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/AbstractMessageRetryTask.java create mode 100644 tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/MessageRetryService.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/AppConfig.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeiShuClientConfig.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeishuConfig.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/FeishuController.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/HttpTranslator.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/ServletAdapter.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mapper/BuriedPointFailRecordMapper.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPointFailRecord.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/handler/BuriedPointFailRecordHandler.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/feishu/FeiShuCardDataService.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/impl/BuriedPointFailRecordService.java create mode 100644 tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/task/BuriedPointRetryTask.java create mode 100644 tashow-sdk/tashow-feishu-sdk/pom.xml create mode 100644 tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuAlertClient.java create mode 100644 tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuMessageClient.java create mode 100644 tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/config/LarkConfig.java create mode 100644 tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/ChartImageGenerator.java create mode 100644 tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/LarkClientUtil.java create mode 100644 tashow-sdk/tashow-feishu-sdk/src/main/resources/card.json diff --git a/logs/system-server.log.2025-04-22.0.gz b/logs/system-server.log.2025-04-22.0.gz deleted file mode 100644 index e4bce88b4c0178ea4b45eb25f2e9e0a642da4d98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3551 zcmV<54IuI#iwFP!00000|Lt5`ZyU!Iezt-Bg1ih20hR&naBsXiN}-FD$dGJ-k{h71 z5Da&RMVOCvch*-c=s|92uX{hfYqlI5VF!zjh-eFM$Ny~8@PLL(LEtfm zM2!0gKK*L9BN9f;Z=g}7+d|&GPYwt&ide^njSgAVL|f~tN<=ijQhEGH&`>=7CA+^) zAFooeAMxPvAwPN?5z*v_THukW&cjyscZ<7ij}HIRl-Q{kwHiBy8)1V+j~$N)K?M?0 za_XX52gUD2pqTL`x%0D|=Jd3}PLrZK_dI?mm2IFP)E`w><6;?^e3l zR#DGFYyx8Y@c#qiaIwPuI%}+wh-`4;(hzk&{c3mPf=HFzL$Vxl;vc&%Jh$#HCe0^e z_*x7BcSy@yK#h>MgT>W*YfFzdcHVij`tZ@#9ujTGp+a0UkP-EXAH90GNT03~f-`Y3 zUwm!j^P8w{+!=Fw$J{57x;9(IPoJp{Ui%$3$ugEGjab>_3!@WA@j zwcE_Eha}-9UZbqxuXv35(RM%`R!@-MR_=}QR4qDZc~6oluU(>L~%UT0dnnAS#F zS!rfvGrr8M>=Lca!iJuoS}Pk0v}R^DGqdZUwNYj^y&>bw%w}eG?X)&Cvl(G#W_DF( zW@IqBGc&VSkuWne%h1d$V`|N8HUcuUvg@X`nU&4>GPAO)v9i2ny||TudAT^m4zALG z3nqnCQ+@3)*6b?vXakhcuXYnmkp&(_+i_zdIeUoIG`s*iYy0j3g7%^DO~AvPNE%F1 zc`|pH)GnW+qtIoLcU~ObGUt(GP!Pnc9R?s)smnwk2?Mn7>SU5L27=R(6!Z*cT`rDf z4LBy6)Expb&=A5QzAtaUYx6p435?cc>mmI#)D9GZ_5?0dk&4mAx>?A^<132?l0@_< z!m(B3#9M63AHyDhuq$BhqFvU@b+>M6^|C=U%c5nitkarCY*Qz+ZrP=>oi4v&+Dd+- zh2CDOKA1@UXt*inq;y+cL|Y-3Gv7RneGCnU`wCdo?J8H^9WIoH5hcNaSz#`QM||}Py(w}GvMdY86*PD%LnKYX@M)K zoUKZFQ4Q)j>Oh5eaJ+QIX@{~a^}q>)l*mOnXX-_}vFuAeR2U1|yAw<7Qs^r}cra>) zyxovJw9kD_$g)n?*`ikLKM}Z>s7WH#z1)?KM=-g@K~)knKSCs4Ataw>K4iJX9YJu@ zE?q4_TE`Lgc!LBCt0he#9&)94gEy+w;i0=Fwf;Q9yl8cEbIPr(yt%QyJU6GmbV98Q zXkB<`9MyH-LrH^%p7dAmQU?f@i`S+|zHp$*So{NK0SmS`_jZWbzjs6(cyV~9;NH(s z+*lw45Sv5ABH(@;d!DUSF;4Y(5LE#0L_hL?jwDCi116OKu<8S_Tuzlzo8*8>3^j_t zCr4UOp9@`+kX(*qtLnpzd+&e?t}bowR3EMEJgVLsL20XKTpguHM|;4|7MN;6N4%nb zKHm*yRO5`u*O18hsU&ior+}9wZjE@vcfiNW<#Wr)k0h<#RI#Rj7ahp{X+n-?^s*WK zuOa=5S4V$ufMY5#7YWtGX|BCg9?#8fkXFqlb7jNI=|{T!7;e$DN`;bbm1Z=*el#y# zVT$G^oL*+W`xfZL1Ij5SOcH^tj;w9Q%j?I>vUzp9Owu?U#;KZi;mj_eVFa3ck>oI` zV#eHS$6Q;#!UWLkQHX|er$ z0Wj&e!MC-T5Ypv!yKWMZt=`q=jk3L1Wi$F-Kl<8N7!0_>bn1-JYMvW;Tmy zNkE(?#UKgD;1lH;woiS9Q;SBn3X=+Rj&c=4m^%@9w35MHf_{O z-8-Mh`C>+^?VR+LR%`v0O@mFb23~AvHQjCWsJan*$Uu$Cx){C9z^g{!oC4h5kV-E38Z{Q*Kyv?Aoy|ckvxeDGrSDp=l)jv_}HT>B#4|Q zN#gu!;7YfAZfyJ~{dK z7bjo;=k?cLho@@&7o-C!WwjA-8ciM$8n~!mUA%hvp$}#6Y=h9I0a=|YF9()=jBk!15ZEt{N&?5oQ>LIxs5E+faz`;!_Fk` zmQ8br--W;TiL)~MH3A2br0wSG-rnQ)Tm#w8>H*IQhgSn z$VTY|`#6BVl3$8Yp!MdK*KRO>llXGFVOZ%S3MEG+_V0EF3nphagG5mrA@>MF-$aaFw(sQGjJmZ|SVxv&P1}>E=itOq^xK~g1Y>fa{%*?G^i#6U+ChPN4cY(p|GD441% z(0nqng2J}iViw2s1zfW7XGiQ~Xm%ERaW z)24L*N*-IKvEq9<{H+@bHu2}?jUT`H*MGnM z?&S0DPd@&O3P1n%!;_mgp8obvC!c-$^vmBo{pQ;Wx*@|nQOV`hls@AQcjanRKSu(l zx!lR2n>QwxB1N-?fh~Qk>|Bbb`HX9EC#}&V>WrgaY19la1#Bq}R5o~HUGAQCXaH%+ zusaJvs+NC4(Eh^*5VSx0{-00({-MlE=5xetF<%2UkfFa9?Fp8U1=?Bh>$}LU z@z%*)KJrV5`t`y&O>UJZZt}uU%?EaMW=|dszQp8s@(!|xvoO}U>{O(}nTSLgo)7wUZ_WvQXR-Ghq002$x1f>7~ diff --git a/tashow-framework/tashow-framework-mq/pom.xml b/tashow-framework/tashow-framework-mq/pom.xml index 3128466..835535e 100644 --- a/tashow-framework/tashow-framework-mq/pom.xml +++ b/tashow-framework/tashow-framework-mq/pom.xml @@ -2,32 +2,42 @@ - 4.0.0 - com.tashow.cloud tashow-framework + com.tashow.cloud ${revision} + 4.0.0 + tashow-framework-mq jar ${project.artifactId} - 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种 + 消息队列模块,基于RabbitMQ等中间件 + https://github.com/tashow/tashow-platform - - - com.tashow.cloud - tashow-data-redis - + org.springframework.boot spring-boot-starter-amqp + true + - org.springframework.amqp - spring-rabbit + org.springframework + spring-web + true + + + org.springframework + spring-webmvc + true + + + jakarta.servlet + jakarta.servlet-api true diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/BaseMqMessage.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/BaseMqMessage.java new file mode 100644 index 0000000..2415c34 --- /dev/null +++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/BaseMqMessage.java @@ -0,0 +1,115 @@ +package com.tashow.cloud.mq.core; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * MQ消息基类,所有消息类型都应该继承此类 + * + * @author tashow + */ +public class BaseMqMessage implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 消息ID,默认为UUID + */ + private Integer id = UUID.randomUUID().hashCode(); + + /** + * 消息状态码 + */ + private Integer statusCode; + + /** + * 消息重试次数 + */ + private Integer retryCount = 0; + + /** + * 消息错误信息 + */ + private String errorMessage; + + /** + * 消息创建时间 + */ + private Date createTime = new Date(); + + /** + * 扩展数据 + */ + private Map extraData = new HashMap<>(); + + /** + * 增加重试次数 + */ + public void incrementRetryCount() { + if (retryCount == null) { + retryCount = 0; + } + retryCount++; + } + + /** + * 添加额外数据 + */ + public void addExtraData(String key, Object value) { + if (extraData == null) { + extraData = new HashMap<>(); + } + extraData.put(key, value); + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getStatusCode() { + return statusCode; + } + + public void setStatusCode(Integer statusCode) { + this.statusCode = statusCode; + } + + public Integer getRetryCount() { + return retryCount; + } + + public void setRetryCount(Integer retryCount) { + this.retryCount = retryCount; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public Map getExtraData() { + return extraData; + } + + public void setExtraData(Map extraData) { + this.extraData = extraData; + } +} \ No newline at end of file diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/CustomCorrelationData.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/CustomCorrelationData.java new file mode 100644 index 0000000..518d0fd --- /dev/null +++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/core/CustomCorrelationData.java @@ -0,0 +1,44 @@ +package com.tashow.cloud.mq.core; + +import org.springframework.amqp.rabbit.connection.CorrelationData; + +/** + * 自定义关联数据,用于存储额外的消息数据 + * + * @author tashow + */ +public class CustomCorrelationData extends CorrelationData { + + /** + * 消息内容 + */ + private final String messageContent; + + /** + * 构造函数 + * + * @param id 关联ID + * @param messageContent 消息内容 + */ + public CustomCorrelationData(String id, String messageContent) { + super(id); + this.messageContent = messageContent; + } + + /** + * 获取消息内容 + * + * @return 消息内容 + */ + public String getMessageContent() { + return messageContent; + } + + @Override + public String toString() { + return "CustomCorrelationData{" + + "id='" + getId() + '\'' + + ", messageContent='" + messageContent + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/handler/FailRecordHandler.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/handler/FailRecordHandler.java new file mode 100644 index 0000000..ad5266f --- /dev/null +++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/handler/FailRecordHandler.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.mq.handler; + +/** + * 消息发送失败记录处理接口 + * + * @author tashow + */ +public interface FailRecordHandler { + + /** + * 保存消息发送失败记录 + * + * @param correlationId 关联ID + * @param exchange 交换机 + * @param routingKey 路由键 + * @param cause 失败原因 + * @param messageContent 消息内容 + */ + void saveFailRecord(String correlationId, String exchange, String routingKey, String cause, String messageContent); + + /** + * 检查是否达到告警阈值 + * + * @return 是否需要告警 + */ + default boolean checkAlertThreshold() { + return checkAlertThreshold(null); + } + + /** + * 检查是否达到告警阈值,带错误信息 + * + * @param cause 错误原因 + * @return 是否需要告警 + */ + default boolean checkAlertThreshold(String cause) { + return false; + } +} \ No newline at end of file diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/package-info.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/package-info.java deleted file mode 100644 index 356754a..0000000 --- a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.tashow.cloud.mq; \ No newline at end of file diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQAutoConfiguration.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQAutoConfiguration.java index f161527..843e28d 100644 --- a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQAutoConfiguration.java +++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQAutoConfiguration.java @@ -1,28 +1,19 @@ package com.tashow.cloud.mq.rabbitmq.config; -import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; -import org.springframework.amqp.support.converter.MessageConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.annotation.Bean; /** - * RabbitMQ 消息队列配置类 + * RabbitMQ 消息队列自动配置类 * - * @author 芋道源码 + * @author tashow */ @AutoConfiguration -@Slf4j @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") -public class RabbitMQAutoConfiguration { +public class RabbitMQAutoConfiguration extends RabbitMQConfiguration { - /** - * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息 - */ - @Bean - public MessageConverter createMessageConverter() { - return new Jackson2JsonMessageConverter(); - } + private static final Logger log = LoggerFactory.getLogger(RabbitMQAutoConfiguration.class); -} +} \ No newline at end of file diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQConfiguration.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQConfiguration.java new file mode 100644 index 0000000..028f91b --- /dev/null +++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/config/RabbitMQConfiguration.java @@ -0,0 +1,49 @@ +package com.tashow.cloud.mq.rabbitmq.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * RabbitMQ 配置类 + * + * @author tashow + */ +@Configuration +@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") +public class RabbitMQConfiguration { + + private static final Logger log = LoggerFactory.getLogger(RabbitMQConfiguration.class); + + /** + * 初始化RabbitTemplate + * + * @param rabbitTemplate RabbitTemplate + */ + protected void initRabbitTemplate(RabbitTemplate rabbitTemplate) { + log.info("[MQ配置] 初始化RabbitTemplate: {}", rabbitTemplate); + // 启用消息发送到交换机确认机制 + rabbitTemplate.setMandatory(true); + + if (rabbitTemplate.isConfirmListener()) { + log.info("[MQ配置] 确认回调已正确配置"); + } else { + log.error("[MQ配置] 确认回调配置失败"); + } + } + + /** + * 创建消息转换器 + * + * @return MessageConverter + */ + @Bean + public MessageConverter messageConverter() { + return new Jackson2JsonMessageConverter(); + } +} \ No newline at end of file diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/consumer/AbstractRabbitMQConsumer.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/consumer/AbstractRabbitMQConsumer.java new file mode 100644 index 0000000..ebf65bf --- /dev/null +++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/consumer/AbstractRabbitMQConsumer.java @@ -0,0 +1,174 @@ +package com.tashow.cloud.mq.rabbitmq.consumer; + +import com.rabbitmq.client.Channel; +import com.tashow.cloud.mq.core.BaseMqMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.messaging.handler.annotation.Header; + +/** + * RabbitMQ消息消费者抽象类 + * + * @param 消息类型 + * @author tashow + */ +public abstract class AbstractRabbitMQConsumer { + + private static final Logger log = LoggerFactory.getLogger(AbstractRabbitMQConsumer.class); + + /** + * 消息状态:处理中 + */ + public static final int STATUS_PROCESSING = 10; + + /** + * 消息状态:成功 + */ + public static final int STATUS_SUCCESS = 20; + + /** + * 消息状态:失败 + */ + public static final int STATUS_ERROR = 30; + + /** + * 处理消息 + * + * @param message 消息对象 + * @return 处理结果,true表示处理成功,false表示处理失败 + */ + public abstract boolean processMessage(T message); + + /** + * 获取消息重试次数 + * + * @param message 消息对象 + * @return 重试次数 + */ + public Integer getRetryCount(T message) { + return message.getRetryCount() != null ? message.getRetryCount() : 0; + } + + /** + * 更新消息状态 + * + * @param message 消息对象 + */ + public abstract void updateMessageStatus(T message); + + /** + * 更新消息重试次数 + * + * @param message 消息对象 + */ + public abstract void updateRetryCount(T message); + + /** + * 保存消息到数据库 + * + * @param message 消息对象 + * @return 保存结果 + */ + public abstract boolean saveToDatabase(T message); + + /** + * 保存消息到失败记录 + * + * @param message 消息对象 + * @param cause 失败原因 + */ + public abstract void saveToFailRecord(T message, String cause); + + /** + * 获取最大允许重试次数 + * + * @return 最大重试次数 + */ + public int getMaxRetryAllowed() { + return 3; + } + + /** + * 消息处理入口 + * + * @param message 消息对象 + * @param channel 通道 + * @param deliveryTag 投递标签 + */ + public void onMessage(T message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { + Integer dbRetryCount = getRetryCount(message); + message.setRetryCount(dbRetryCount); + + if (message.getRetryCount() != null && message.getRetryCount() >= getMaxRetryAllowed()) { + message.setStatusCode(STATUS_ERROR); + message.addExtraData("errorMessage", "已达到最大重试次数"); + saveToFailRecord(message, "已达到最大重试次数"); + safeChannelAck(channel, deliveryTag); + return; + } + + log.info("[MQ消费者] 收到消息: {}, 当前重试次数: {}/{}", message, message.getRetryCount(), getMaxRetryAllowed()); + message.setStatusCode(STATUS_PROCESSING); + + try { + boolean result = processMessage(message); + if (result) { + message.setStatusCode(STATUS_SUCCESS); + updateMessageStatus(message); + log.info("[MQ消费者] 消息处理成功,状态已更新为成功: {}", message.getId()); + } else { + throw new RuntimeException("消息处理失败"); + } + safeChannelAck(channel, deliveryTag); + } catch (Exception e) { + message.setStatusCode(STATUS_ERROR); + message.addExtraData("errorMessage", e.getMessage()); + message.setErrorMessage(e.getMessage()); + log.error("[MQ消费者] 消息处理失败: {}, 错误: {}", message.getId(), e.getMessage()); + + message.incrementRetryCount(); + updateRetryCount(message); + + if (message.getRetryCount() >= getMaxRetryAllowed()) { + saveToDatabase(message); + log.warn("[MQ消费者] 消息已达到最大重试次数: {}, 确认消息并保存到失败记录表", message.getRetryCount()); + saveToFailRecord(message, e.getMessage()); + safeChannelAck(channel, deliveryTag); + } else { + log.info("[MQ消费者] 消息将重新入队重试: {}, 当前重试次数: {}", message.getId(), message.getRetryCount()); + safeChannelNack(channel, deliveryTag, false, true); + } + } + } + + /** + * 安全确认消息 + * + * @param channel 通道 + * @param deliveryTag 投递标签 + */ + protected void safeChannelAck(Channel channel, long deliveryTag) { + try { + channel.basicAck(deliveryTag, false); + } catch (Exception e) { + log.error("[MQ消费者] 确认消息失败: {}", e.getMessage()); + } + } + + /** + * 安全拒绝消息 + * + * @param channel 通道 + * @param deliveryTag 投递标签 + * @param multiple 是否批量 + * @param requeue 是否重新入队 + */ + protected void safeChannelNack(Channel channel, long deliveryTag, boolean multiple, boolean requeue) { + try { + channel.basicNack(deliveryTag, multiple, requeue); + } catch (Exception e) { + log.error("[MQ消费者] 拒绝消息失败: {}", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/producer/AbstractRabbitMQProducer.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/producer/AbstractRabbitMQProducer.java new file mode 100644 index 0000000..970704e --- /dev/null +++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/rabbitmq/producer/AbstractRabbitMQProducer.java @@ -0,0 +1,145 @@ +package com.tashow.cloud.mq.rabbitmq.producer; + +import com.tashow.cloud.mq.core.BaseMqMessage; +import com.tashow.cloud.mq.core.CustomCorrelationData; +import com.tashow.cloud.mq.handler.FailRecordHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.ReturnedMessage; +import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.annotation.PostConstruct; +import java.util.UUID; + +/** + * RabbitMQ消息生产者抽象类 + * + * @param 消息类型 + * @author tashow + */ +public abstract class AbstractRabbitMQProducer + implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback { + + private static final Logger log = LoggerFactory.getLogger(AbstractRabbitMQProducer.class); + + @Autowired + protected RabbitTemplate rabbitTemplate; + + @Autowired(required = false) + protected FailRecordHandler failRecordHandler; + + /** + * 初始化RabbitTemplate + */ + @PostConstruct + public void initRabbitTemplate() { + log.info("[MQ生产者] 初始化RabbitTemplate: {}", rabbitTemplate); + rabbitTemplate.setMandatory(true); + rabbitTemplate.setReturnsCallback(this); + rabbitTemplate.setConfirmCallback(this); + + if (rabbitTemplate.isConfirmListener()) { + log.info("[MQ生产者] 确认回调已正确配置"); + } else { + log.error("[MQ生产者] 确认回调配置失败"); + } + } + + /** + * 将消息转换为字符串 + * + * @param message 消息对象 + * @return 消息字符串 + */ + protected abstract String convertMessageToString(T message); + + /** + * 异步发送消息,自动生成correlationId + * + * @param message 消息对象 + */ + public void asyncSendMessage(T message) { + String correlationId = UUID.randomUUID().toString(); + asyncSendMessage(message, correlationId); + } + + /** + * 异步发送消息,使用指定的correlationId + * + * @param message 消息对象 + * @param correlationId 关联ID + */ + public void asyncSendMessage(T message, String correlationId) { + log.info("[MQ生产者] 准备发送消息: {}, correlationId: {}", message, correlationId); + try { + String messageJson = convertMessageToString(message); + CustomCorrelationData correlationData = new CustomCorrelationData(correlationId, messageJson); + rabbitTemplate.convertAndSend(getExchange(), getRoutingKey(), message, correlationData); + log.info("[MQ生产者] 消息发送完成: {}, 状态: {}, 重试次数: {}, correlationId: {}", + message.getId(), message.getStatusCode(), message.getRetryCount(), correlationId); + } catch (Exception e) { + log.error("[MQ生产者] 消息发送异常: {}, correlationId: {}", e.getMessage(), correlationId, e); + throw e; + } + } + + /** + * 获取交换机名称 + * + * @return 交换机名称 + */ + public abstract String getExchange(); + + /** + * 获取路由键 + * + * @return 路由键 + */ + public abstract String getRoutingKey(); + + @Override + public void confirm(CorrelationData correlationData, boolean ack, String cause) { + if (ack) { + log.info("[MQ生产者] 消息发送确认成功: {}", correlationData.getId()); + } else { + log.error("[MQ生产者] 消息发送确认失败: {}, 原因: {}, correlationData={}", + correlationData.getId(), cause, correlationData); + if (failRecordHandler != null && correlationData instanceof CustomCorrelationData) { + CustomCorrelationData customData = (CustomCorrelationData) correlationData; + String messageContent = customData.getMessageContent(); + failRecordHandler.saveFailRecord( + correlationData.getId(), + getExchange(), + getRoutingKey(), + cause, + messageContent + ); + } else { + log.warn("[MQ生产者] 未配置FailRecordHandler或非CustomCorrelationData类型, 无法保存失败记录"); + } + } + } + + @Override + public void returnedMessage(ReturnedMessage returned) { + log.error("[MQ生产者] 消息路由失败: exchange={}, routingKey={}, replyCode={}, replyText={}, message={}", + returned.getExchange(), + returned.getRoutingKey(), + returned.getReplyCode(), + returned.getReplyText(), + new String(returned.getMessage().getBody())); + if (failRecordHandler != null) { + failRecordHandler.saveFailRecord( + returned.getMessage().getMessageProperties().getCorrelationId(), + returned.getExchange(), + returned.getRoutingKey(), + "路由失败: " + returned.getReplyText(), + new String(returned.getMessage().getBody()) + ); + } else { + log.warn("[MQ生产者] 未配置FailRecordHandler, 无法保存失败记录"); + } + } +} \ No newline at end of file diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/AbstractMessageRetryTask.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/AbstractMessageRetryTask.java new file mode 100644 index 0000000..b8cf3a0 --- /dev/null +++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/AbstractMessageRetryTask.java @@ -0,0 +1,91 @@ +package com.tashow.cloud.mq.retry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * 消息重试任务抽象实现 + * + * @param 失败记录类型 + * @author tashow + */ +public abstract class AbstractMessageRetryTask { + + private static final Logger log = LoggerFactory.getLogger(AbstractMessageRetryTask.class); + + /** + * 处理中状态码 + */ + public static final int STATUS_PROCESSING = 3; + + /** + * 失败状态码 + */ + public static final int STATUS_FAILED = 4; + + /** + * 获取消息重试服务 + * + * @return 消息重试服务 + */ + protected abstract MessageRetryService getMessageRetryService(); + + /** + * 获取记录ID + * + * @param record 记录对象 + * @return 记录ID + */ + protected abstract String getRecordId(T record); + + /** + * 获取关联ID + * + * @param record 记录对象 + * @return 关联ID + */ + protected abstract String getCorrelationId(T record); + + /** + * 执行重试 + */ + public void retryFailedMessages() { + try { + List unprocessedRecords = getMessageRetryService().getUnprocessedRecords(); + if (unprocessedRecords.isEmpty()) { + log.info("[MQ重试] 没有需要重试的消息"); + return; + } + + log.info("[MQ重试] 本次需要重试的消息数量: {}", unprocessedRecords.size()); + for (T record : unprocessedRecords) { + try { + // 先将状态更新为处理中,避免其他实例重复处理 + if (!getMessageRetryService().updateStatus(record, STATUS_PROCESSING)) { + continue; // 如果更新失败,跳过当前记录 + } + + // 执行重试 + String recordId = getRecordId(record); + boolean success = getMessageRetryService().retryFailedMessage(recordId); + + if (success) { + log.info("[MQ重试] 消息重试成功: {}", getCorrelationId(record)); + } else { + log.warn("[MQ重试] 消息重试失败: {}", getCorrelationId(record)); + getMessageRetryService().updateStatus(record, STATUS_FAILED); + } + } catch (Exception e) { + // 发生异常时,更新状态为失败 + getMessageRetryService().updateStatus(record, STATUS_FAILED); + log.error("[MQ重试] 重试消息异常: {}", getCorrelationId(record), e); + } + } + log.info("[MQ重试] 消息重试任务完成"); + } catch (Exception e) { + log.error("[MQ重试] 执行消息重试任务异常", e); + } + } +} \ No newline at end of file diff --git a/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/MessageRetryService.java b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/MessageRetryService.java new file mode 100644 index 0000000..0dc29b0 --- /dev/null +++ b/tashow-framework/tashow-framework-mq/src/main/java/com/tashow/cloud/mq/retry/MessageRetryService.java @@ -0,0 +1,36 @@ +package com.tashow.cloud.mq.retry; + +import java.util.List; + +/** + * 消息重试服务接口 + * + * @param 失败记录类型 + * @author tashow + */ +public interface MessageRetryService { + + /** + * 获取未处理的失败记录 + * + * @return 失败记录列表 + */ + List getUnprocessedRecords(); + + /** + * 重试失败消息 + * + * @param recordId 记录ID + * @return 重试结果 + */ + boolean retryFailedMessage(String recordId); + + /** + * 更新记录状态 + * + * @param record 记录对象 + * @param status 记录状态 + * @return 更新结果 + */ + boolean updateStatus(T record, int status); +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/pom.xml b/tashow-module/tashow-module-app/pom.xml index b4afe93..4de4e69 100644 --- a/tashow-module/tashow-module-app/pom.xml +++ b/tashow-module/tashow-module-app/pom.xml @@ -28,11 +28,27 @@ com.tashow.cloud tashow-framework-rpc - + + com.tashow.cloud + tashow-data-mybatis + com.tashow.cloud tashow-framework-web + + com.tashow.cloud + tashow-framework-env + + + com.tashow.cloud + tashow-infra-api + + + + com.tashow.cloud + tashow-framework-websocket + com.tashow.cloud tashow-data-redis @@ -41,5 +57,23 @@ com.tashow.cloud tashow-framework-security + + org.springframework.boot + spring-boot-starter-actuator + + + com.tashow.cloud + tashow-feishu-sdk + 1.0.0 + compile + + + org.springframework.amqp + spring-rabbit + + + com.tashow.cloud + tashow-data-redis + diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/AppServerApplication.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/AppServerApplication.java index a74dc76..5099474 100644 --- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/AppServerApplication.java +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/AppServerApplication.java @@ -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) { diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/AppConfig.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/AppConfig.java new file mode 100644 index 0000000..6a1f2d3 --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/AppConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeiShuClientConfig.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeiShuClientConfig.java new file mode 100644 index 0000000..fa88d22 --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeiShuClientConfig.java @@ -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); + }*/ +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeishuConfig.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeishuConfig.java new file mode 100644 index 0000000..cf7478b --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/config/FeishuConfig.java @@ -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; + } + + +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/FeishuController.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/FeishuController.java new file mode 100644 index 0000000..8fc964f --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/FeishuController.java @@ -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 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() + "\"}"; + } + } +} diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/TestController.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/TestController.java index 35ce5c5..6770341 100644 --- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/TestController.java +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/controller/TestController.java @@ -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; diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/HttpTranslator.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/HttpTranslator.java new file mode 100644 index 0000000..095b40e --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/HttpTranslator.java @@ -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> toHeaderMap(HttpServletRequest req) { + Map> headers = new HashMap<>(); + Enumeration names = req.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + List 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 values = keyValues.getValue(); + values.stream().forEach(v -> response.addHeader(key, v)); + }); + if (eventResp.getBody() != null) { + response.getWriter().write(new String(eventResp.getBody())); + } + } +} diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/ServletAdapter.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/ServletAdapter.java new file mode 100644 index 0000000..1fcfbd5 --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/ext/ServletAdapter.java @@ -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); + } +} diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/interceptor/BuriedPointInterceptor.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/interceptor/BuriedPointInterceptor.java index 5be550d..5616d25 100644 --- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/interceptor/BuriedPointInterceptor.java +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/interceptor/BuriedPointInterceptor.java @@ -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()) { diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mapper/BuriedPointFailRecordMapper.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mapper/BuriedPointFailRecordMapper.java new file mode 100644 index 0000000..87236ee --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mapper/BuriedPointFailRecordMapper.java @@ -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 { +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPoint.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPoint.java index ad5b745..435621e 100644 --- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPoint.java +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPoint.java @@ -33,7 +33,7 @@ public class BuriedPoint { */ @TableField(value = "event_time") private Long eventTime; - + /** * 服务名称 */ diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPointFailRecord.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPointFailRecord.java new file mode 100644 index 0000000..7ef0be3 --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/model/BuriedPointFailRecord.java @@ -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; +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/config/BuriedPointConfiguration.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/config/BuriedPointConfiguration.java index 6cd9ca7..35a8eca 100644 --- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/config/BuriedPointConfiguration.java +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/config/BuriedPointConfiguration.java @@ -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 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" ); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/consumer/buriedPoint/BuriedPointConsumer.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/consumer/buriedPoint/BuriedPointConsumer.java index 9c7e035..03a8734 100644 --- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/consumer/buriedPoint/BuriedPointConsumer.java +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/consumer/buriedPoint/BuriedPointConsumer.java @@ -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 { 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 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 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 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 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 queryHourlyFailRecordData(Date startDate, Date endDate) { + List 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 successQuery = new LambdaQueryWrapper<>(); + successQuery.ge(BuriedPointFailRecord::getCreateTime, currentHourStart) + .lt(BuriedPointFailRecord::getCreateTime, nextHourStart) + .eq(BuriedPointFailRecord::getStatus, BuriedPointFailRecord.STATUS_SUCCESS); // 处理成功 + Long successCount = buriedPointFailRecordMapper.selectCount(successQuery); + + // 查询处理失败或未处理的记录数量 + LambdaQueryWrapper 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(); + } + } } diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/handler/BuriedPointFailRecordHandler.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/handler/BuriedPointFailRecordHandler.java new file mode 100644 index 0000000..524ddf0 --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/handler/BuriedPointFailRecordHandler.java @@ -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 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 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 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 queryHourlyFailRecordData(Date startDate, Date endDate) { + List 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 successQuery = new LambdaQueryWrapper<>(); + successQuery.ge(BuriedPointFailRecord::getCreateTime, currentHourStart).lt(BuriedPointFailRecord::getCreateTime, nextHourStart).eq(BuriedPointFailRecord::getStatus, BuriedPointFailRecord.STATUS_SUCCESS); // 处理成功 + Long successCount = buriedPointFailRecordMapper.selectCount(successQuery); + + // 查询处理失败或未处理的记录数量 + LambdaQueryWrapper 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(); + } + } +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/message/BuriedMessages.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/message/BuriedMessages.java index 7a072e1..fa54a12 100644 --- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/message/BuriedMessages.java +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/message/BuriedMessages.java @@ -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 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; } diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/producer/buriedPoint/BuriedPointProducer.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/producer/buriedPoint/BuriedPointProducer.java index 648691c..c1db0be 100644 --- a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/producer/buriedPoint/BuriedPointProducer.java +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/mq/producer/buriedPoint/BuriedPointProducer.java @@ -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 { - /** - * 确认消息是否成功发送到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); } } \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/feishu/FeiShuCardDataService.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/feishu/FeiShuCardDataService.java new file mode 100644 index 0000000..1cb7ea4 --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/feishu/FeiShuCardDataService.java @@ -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 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 getCardData(String messageId) { + try { + String jsonData = stringRedisTemplate.opsForValue().get(messageId); + return objectMapper.readValue(jsonData, Map.class); + } catch (Exception e) { + throw new RuntimeException(e); + + } + } + +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/impl/BuriedPointFailRecordService.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/impl/BuriedPointFailRecordService.java new file mode 100644 index 0000000..f458aaa --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/service/impl/BuriedPointFailRecordService.java @@ -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 { + + private final BuriedPointFailRecordMapper buriedPointFailRecordMapper; + private final BuriedPointProducer buriedPointProducer; + + /** + * 获取未处理的失败记录 + */ + @Override + public List getUnprocessedRecords() { + LambdaQueryWrapper 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; + } + } +} \ No newline at end of file diff --git a/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/task/BuriedPointRetryTask.java b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/task/BuriedPointRetryTask.java new file mode 100644 index 0000000..686c022 --- /dev/null +++ b/tashow-module/tashow-module-app/src/main/java/com/tashow/cloud/app/task/BuriedPointRetryTask.java @@ -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 { + + private final BuriedPointFailRecordService buriedPointFailRecordService; + + /** + * 定时重试失败消息 + * 每天凌晨执行一次 + */ + @Scheduled(cron = "0 0 0 * * ?") + public void execute() { + retryFailedMessages(); + } + + @Override + protected MessageRetryService getMessageRetryService() { + return buriedPointFailRecordService; + } + + @Override + protected String getRecordId(BuriedPointFailRecord record) { + return String.valueOf(record.getId()); + } + + @Override + protected String getCorrelationId(BuriedPointFailRecord record) { + return record.getCorrelationId(); + } +} \ No newline at end of file diff --git a/tashow-sdk/pom.xml b/tashow-sdk/pom.xml index f3b4015..58ee94c 100644 --- a/tashow-sdk/pom.xml +++ b/tashow-sdk/pom.xml @@ -12,6 +12,7 @@ tashow-sdk-payment + tashow-feishu-sdk diff --git a/tashow-sdk/tashow-feishu-sdk/pom.xml b/tashow-sdk/tashow-feishu-sdk/pom.xml new file mode 100644 index 0000000..097a044 --- /dev/null +++ b/tashow-sdk/tashow-feishu-sdk/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + + com.tashow.cloud + tashow-sdk + ${revision} + + + tashow-feishu-sdk + jar + + + + + + com.tashow.cloud + tashow-data-redis + + + + com.larksuite.oapi + oapi-sdk + 2.4.18 + + + httpclient + org.apache.httpcomponents + 4.5.13 + + + + + junit + junit + test + 4.13.2 + + + com.tashow.cloud + tashow-common + + + diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuAlertClient.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuAlertClient.java new file mode 100644 index 0000000..f1e378c --- /dev/null +++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuAlertClient.java @@ -0,0 +1,274 @@ +package com.tashow.cloud.sdk.feishu.client; +import com.lark.oapi.Client; +import com.lark.oapi.service.im.v1.model.*; +import com.tashow.cloud.sdk.feishu.util.ChartImageGenerator; +import com.lark.oapi.service.im.v1.model.ext.MessageTemplate; +import com.lark.oapi.service.im.v1.model.ext.MessageTemplateData; +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.*; +import com.tashow.cloud.sdk.feishu.config.LarkConfig; +import com.tashow.cloud.sdk.feishu.util.LarkClientUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 飞书告警客户端 + * 用于处理系统告警消息的发送 + */ +@Service +public class FeiShuAlertClient { + private final Logger log = LoggerFactory.getLogger(FeiShuAlertClient.class); + private final Client client; + private final LarkConfig larkConfig; + private final ChartImageGenerator chartImageGenerator; + public interface CardDataHandler { + /** + * 保存卡片数据 + * @param messageId 消息ID + * @param data 卡片数据 + */ + void saveCardData(String messageId, Map data); + /** + * 获取卡片数据 + * @param messageId 消息ID + * @return 卡片数据 + */ + Map getCardData(String messageId); + } + private CardDataHandler cardDataHandler; + + @Autowired + public FeiShuAlertClient(LarkClientUtil larkClientUtil, LarkConfig larkConfig, + ChartImageGenerator chartImageGenerator, ObjectMapper objectMapper) { + this.client = larkClientUtil.getLarkClient(); + this.larkConfig = larkConfig; + this.chartImageGenerator = chartImageGenerator; + } + + + + /** + * 创建报警群并拉人入群 + * + * @return 创建的群聊ID + * @throws Exception 异常信息 + */ + public String createAlertChat() throws Exception { + CreateChatReq req = CreateChatReq.newBuilder() + .userIdType("open_id") + .createChatReqBody(CreateChatReqBody.newBuilder() + .name("[待处理] 线上事故处理") + .description("线上紧急事故处理") + .userIdList(larkConfig.getAlertUserOpenIds()) + .build()) + .build(); + CreateChatResp resp = client.im().chat().create(req); + if (!resp.success()) { + throw new Exception(String.format("client.im.chat.create failed, code: %d, msg: %s, logId: %s", + resp.getCode(), resp.getMsg(), resp.getRequestId())); + } + return resp.getData().getChatId(); + } + + /** + * 发送埋点报警消息 + * + * @param chatId 会话ID + * @param buriedPointData 埋点数据 + * @param failCount 失败数量 + * @throws Exception 异常信息 + */ + public void sendBuriedPointAlertMessage(String chatId, List buriedPointData, int failCount) throws Exception { + sendBuriedPointAlertMessage(chatId, buriedPointData, failCount, null); + } + + /** + * 发送带错误信息的埋点报警消息 + * + * @param chatId 会话ID + * @param buriedPointData 埋点数据 + * @param failCount 失败数量 + * @param errorMessage 错误信息 + * @throws Exception 异常信息 + */ + public void sendBuriedPointAlertMessage(String chatId, List buriedPointData, int failCount, String errorMessage) throws Exception { + HashMap templateData = new HashMap<>(); + String imageKey = uploadImage(buriedPointData, errorMessage); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String currentTime = sdf.format(new Date()); + templateData.put("alert_title", "埋点数据异常告警"); + templateData.put("image_key", imageKey); + templateData.put("current_time", currentTime); + templateData.put("fail_count", failCount); + sendCardMessage(chatId, "AAqdpjayeOVp2", templateData); + } + + /** + * 发送带错误信息的埋点报警消息(创建群) + * + * @param buriedPointData 埋点数据 + * @param failCount 失败数量 + * @param errorMessage 错误信息 + * @throws Exception 异常信息 + */ + public void sendBuriedPointAlertMessage(List buriedPointData, int failCount, String errorMessage) throws Exception { + String chatId = createAlertChat(); + sendBuriedPointAlertMessage(chatId, buriedPointData, failCount, errorMessage); + } + + /** + * 发送报警消息 + * + * @param chatId 会话ID + * @param msgType 消息类型 + * @param content 消息内容 + * @return 消息ID + * @throws Exception 异常信息 + */ + public String sendMessage(String chatId, String msgType, String content) throws Exception { + CreateMessageReq req = CreateMessageReq.newBuilder() + .receiveIdType("chat_id") + .createMessageReqBody(CreateMessageReqBody.newBuilder() + .receiveId(chatId) + .msgType(msgType) + .content(content) + .build()) + .build(); + + CreateMessageResp resp = client.im().message().create(req); + if (!resp.success()) { + throw new Exception(String.format("client.im.message.create failed, code: %d, msg: %s, logId: %s", + resp.getCode(), resp.getMsg(), resp.getRequestId())); + } + + return resp.getData().getMessageId(); + } + + /** + * 更新卡片消息 + * + * @param messageId 消息ID + * @param content 新的卡片内容 + * @throws Exception 异常信息 + */ + public void updateCardMessage(String messageId, String content) throws Exception { + PatchMessageReq req = PatchMessageReq.newBuilder() + .messageId(messageId) + .patchMessageReqBody(PatchMessageReqBody.newBuilder() + .content(content) + .build()) + .build(); + PatchMessageResp resp = client.im().message().patch(req); + + if (!resp.success()) { + throw new Exception(String.format("client.im.message.patch failed, code: %d, msg: %s, logId: %s", + resp.getCode(), resp.getMsg(), resp.getRequestId())); + } + } + + /** + * 上传指定数据的监控图表(带错误信息) + * + * @param monitoringData 监控数据 + * @param errorMessage 错误信息 + * @return 上传后的图片KEY + * @throws Exception 异常信息 + */ + public String uploadImage(List monitoringData, String errorMessage) throws Exception { + // 动态生成监控图表 + File tempFile = File.createTempFile("alert", ".png"); + // 使用提供的数据生成图表 + chartImageGenerator.generateDashboardImage(tempFile, monitoringData, errorMessage); + CreateImageReq req = CreateImageReq.newBuilder() + .createImageReqBody(CreateImageReqBody.newBuilder() + .imageType("message") + .image(tempFile) + .build()) + .build(); + + CreateImageResp resp = client.im().image().create(req); + if (!resp.success()) { + throw new Exception(String.format("client.im.image.create failed, code: %d, msg: %s, logId: %s", + resp.getCode(), resp.getMsg(), resp.getRequestId())); + } + tempFile.delete(); + return resp.getData().getImageKey(); + } + + /** + * 使用模板数据构建卡片内容 + * + * @param templateId 卡片模板ID + * @param templateData 模板数据 + * @return 卡片JSON内容 + */ + public String buildCardWithData(String templateId, Map templateData) { + return new MessageTemplate.Builder() + .data(new MessageTemplateData.Builder().templateId(templateId) + .templateVariable(templateData) + .build()) + .build(); + } + + /** + * 构建埋点异常卡片 + * + * @param buttonName 按钮名称 + * @param buriedPointData 埋点数据 + * @param failCount 失败数量 + * @return 卡片JSON + * @throws Exception 异常信息 + */ + private String buildBuriedPointCard(String buttonName, List buriedPointData, int failCount) throws Exception { + return buildBuriedPointCard(buttonName, buriedPointData, failCount, null); + } + + /** + * 构建埋点异常卡片(带错误信息) + * + * @param buttonName 按钮名称 + * @param buriedPointData 埋点数据 + * @param failCount 失败数量 + * @param errorMessage 错误信息 + * @return 卡片JSON + * @throws Exception 异常信息 + */ + private String buildBuriedPointCard(String buttonName, List buriedPointData, int failCount, String errorMessage) throws Exception { + String imageKey = uploadImage(buriedPointData, errorMessage); + // 获取当前时间 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String currentTime = sdf.format(new Date()); + + HashMap templateData = new HashMap<>(); + templateData.put("alert_title", "埋点数据异常告警"); + templateData.put("image_key", imageKey); + templateData.put("current_time", currentTime); + templateData.put("fail_count", failCount); + templateData.put("button_name", buttonName); + + return buildCardWithData("AAqdpjayeOVp2", templateData); + } + + /** + * 发送卡片消息并保存数据 + * + * @param chatId 会话ID + * @param templateId 卡片模板ID + * @param templateData 模板数据 + * @return 消息ID + * @throws Exception 异常信息 + */ + public String sendCardMessage(String chatId, String templateId, Map templateData) throws Exception { + String cardContent = buildCardWithData(templateId, templateData); + String messageId = sendMessage(chatId, "interactive", cardContent); + if (cardDataHandler != null && messageId != null) { + cardDataHandler.saveCardData(messageId, templateData); + } + + return messageId; + } +} diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuMessageClient.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuMessageClient.java new file mode 100644 index 0000000..e2dd2a1 --- /dev/null +++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/client/FeiShuMessageClient.java @@ -0,0 +1,109 @@ +package com.tashow.cloud.sdk.feishu.client; +import com.lark.oapi.Client; +import com.lark.oapi.core.utils.Jsons; +import com.lark.oapi.service.im.v1.model.*; +import com.tashow.cloud.sdk.feishu.config.LarkConfig; +import com.tashow.cloud.sdk.feishu.util.LarkClientUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 飞书普通消息客户端 + * 用于处理与警报无关的消息发送 + */ +@Service +public class FeiShuMessageClient { + + private final Client client; + private final LarkConfig larkConfig; + + @Autowired + public FeiShuMessageClient(LarkClientUtil larkClientUtil, LarkConfig larkConfig) { + this.client = larkClientUtil.getLarkClient(); + this.larkConfig = larkConfig; + } + + /** + * 发送文本消息 + * @param chatId 会话ID + * @param text 消息文本 + * @return 发送结果 + * @throws Exception 异常信息 + */ + public boolean sendTextMessage(String chatId, String text) throws Exception { + Map content = new HashMap<>(); + content.put("text", text); + CreateMessageReq req = CreateMessageReq.newBuilder() + .receiveIdType("chat_id") + .createMessageReqBody(CreateMessageReqBody.newBuilder() + .receiveId(chatId) + .msgType("text") + .content(Jsons.DEFAULT.toJson(content)) + .build()) + .build(); + CreateMessageResp resp = client.im().message().create(req); + if (!resp.success()) { + System.out.println("发送失败原因: " + resp.getMsg() + ", 错误码: " + resp.getCode()); + } + return resp.success(); + } + + /** + * 发送富文本消息 + * @param chatId 会话ID() + * @param title 标题 + * @param content 内容 + * @return 发送结果 + * @throws Exception 异常信息 + */ + public boolean sendPostMessage(String chatId, String title, String content) throws Exception { + // 正确的富文本消息格式 + String postJson = String.format("{\"zh_cn\":{\"title\":\"%s\",\"content\":[[{\"tag\":\"text\",\"text\":\"%s\"}]]}}", + title, content); + CreateMessageReq req = CreateMessageReq.newBuilder() + .receiveIdType("chat_id") + .createMessageReqBody(CreateMessageReqBody.newBuilder() + .receiveId(chatId) + .msgType("post") + .content(postJson) + .build()) + .build(); + + CreateMessageResp resp = client.im().message().create(req); + if (!resp.success()) { + System.out.println("发送失败原因: " + resp.getMsg() + ", 错误码: " + resp.getCode()); + } + return resp.success(); + } + + /** + * 获取会话历史消息 + * @param chatId 会话ID + * @throws Exception 异常信息 + */ + public void listChatHistory(String chatId) throws Exception { + ListMessageReq req = ListMessageReq.newBuilder().containerIdType("chat").containerId(chatId).build(); + + ListMessageResp resp = client.im().message().list(req); + + if (!resp.success()) { + throw new Exception(String.format("client.im.message.list failed, code: %d, msg: %s, logId: %s", resp.getCode(), resp.getMsg(), resp.getRequestId())); + } + File file = new File("./src/main/java/com/larksuite/oapi/quick_start/robot/chat_history.txt"); + FileWriter writer = new FileWriter(file); + for (Message item : resp.getData().getItems()) { + String senderId = item.getSender().getId(); + String content = item.getBody().getContent(); + String createTime = item.getCreateTime(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + createTime = sdf.format(new Date(Long.parseLong(createTime))); + writer.write(String.format("chatter(%s) at (%s) send: %s\n", senderId, createTime, content)); + } + writer.close(); + } +} \ No newline at end of file diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/config/LarkConfig.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/config/LarkConfig.java new file mode 100644 index 0000000..f283442 --- /dev/null +++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/config/LarkConfig.java @@ -0,0 +1,35 @@ +package com.tashow.cloud.sdk.feishu.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +/** + * 飞书配置类 + * 用于管理飞书应用的配置信息 + */ +@Component +@Data +public class LarkConfig { + + @Value("${lark.app.id}") + private String appId; + + @Value("${lark.app.secret}") + private String appSecret; + + @Value("${lark.app.encrypt-key}") + private String encryptKey; + + @Value("${lark.app.verification-token}") + private String verificationToken; + + @Value("${lark.alert.chat-id}") + private String chatId; + + @Value("${lark.alert.user-open-ids}") + private String[] alertUserOpenIds; + + +} \ No newline at end of file diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/ChartImageGenerator.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/ChartImageGenerator.java new file mode 100644 index 0000000..6183f49 --- /dev/null +++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/ChartImageGenerator.java @@ -0,0 +1,324 @@ +package com.tashow.cloud.sdk.feishu.util; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.List; +import javax.imageio.ImageIO; +import org.springframework.stereotype.Component; + +/** + * 图表生成工具类 + * 用于生成监控数据图表 + */ +@Component +public class ChartImageGenerator { + + /** + * 监控数据点类 + */ + public static class MonitoringDataPoint { + private String timestamp; // 时间戳,格式如 "13:54" + private int successCount; // 成功数量 + private int failureCount; // 失败数量 + + public MonitoringDataPoint(String timestamp, int successCount, int failureCount) { + this.timestamp = timestamp; + this.successCount = successCount; + this.failureCount = failureCount; + } + + public String getTimestamp() { + return timestamp; + } + + public int getSuccessCount() { + return successCount; + } + + public int getFailureCount() { + return failureCount; + } + } + + /** + * 生成监控仪表盘图像 + * @param outputFile 输出文件 + * @param monitoringData 监控数据 + * @throws IOException 如果图像创建失败 + */ + public void generateDashboardImage(File outputFile, List monitoringData) throws IOException { + generateDashboardImage(outputFile, monitoringData, null); + } + + /** + * 生成监控仪表盘图像(带错误信息) + * @param outputFile 输出文件 + * @param monitoringData 监控数据 + * @param errorMessage 错误信息,如为null则不显示 + * @throws IOException 如果图像创建失败 + */ + public void generateDashboardImage(File outputFile, List monitoringData, String errorMessage) throws IOException { + int width = 850; + int height = 350; // 减小高度,原来是550 + int padding = 70; + int topPadding = 30; // 减少顶部空白,使用单独的顶部padding值 + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = image.createGraphics(); + + // 启用抗锯齿 + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + // 设置背景为白色 + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, width, height); + + // 添加科技感背景网格 + drawTechBackground(g2d, width, height); + + // 计算图表区域 + int chartWidth = width - padding * 2; + int chartHeight = height - padding - topPadding; // 调整图表高度计算 + + // 绘制水平网格线 + Font labelFont = new Font("Microsoft YaHei", Font.PLAIN, 12); + g2d.setFont(labelFont); + FontMetrics metrics = g2d.getFontMetrics(labelFont); + + // 找出最大值以确定y轴的刻度 + int maxValue = 0; + for (MonitoringDataPoint point : monitoringData) { + maxValue = Math.max(maxValue, Math.max(point.getSuccessCount(), point.getFailureCount())); + } + + // 向上调整10%,确保有足够空间显示数据 + maxValue = (int)(maxValue * 1.1); + + // 如果最大值太小,设置一个最小值确保图表可读性 + if (maxValue < 10) { + maxValue = 10; + } + + // 向上取整到合适的刻度 + if (maxValue <= 100) { + // 小于100时,取整到10的倍数 + maxValue = ((maxValue + 9) / 10) * 10; + } else if (maxValue <= 1000) { + // 100-1000时,取整到50的倍数 + maxValue = ((maxValue + 49) / 50) * 50; + } else { + // 大于1000时,取整到100的倍数 + maxValue = ((maxValue + 99) / 100) * 100; + } + + // 动态计算y轴刻度 + int yDivisions = 5; // Y轴分段数 + int yStep = maxValue / yDivisions; + + // 绘制水平网格线 + for (int i = 0; i <= yDivisions; i++) { + int y = height - padding - (i * chartHeight / yDivisions); + + // 科技感网格线 + g2d.setColor(new Color(220, 220, 240, 100)); + g2d.setStroke(new BasicStroke(0.8f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 0, new float[]{3}, 0)); + g2d.drawLine(padding, y, width - padding, y); + + // 添加y轴标签 + String yLabel = String.format("%d", i * yStep); + int labelWidth = metrics.stringWidth(yLabel); + g2d.setColor(new Color(80, 80, 120)); + g2d.drawString(yLabel, padding - labelWidth - 10, y + metrics.getHeight() / 2 - 2); + } + + // 绘制垂直网格线和X轴标签 + int totalPoints = monitoringData.size(); + for (int i = 0; i < totalPoints; i++) { + int x = padding + (i * chartWidth / (totalPoints - 1)); + + // 科技感垂直网格线 + g2d.setColor(new Color(220, 220, 240, 100)); + g2d.setStroke(new BasicStroke(0.8f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 0, new float[]{3}, 0)); + g2d.drawLine(x, topPadding, x, height - padding); // 调整网格线顶部起点 + + // 添加每个点对应的时间标签 + if (i % 2 == 0 || i == totalPoints - 1) { // 每隔1个点显示标签,减少拥挤感 + String timeLabel = monitoringData.get(i).getTimestamp(); + int labelWidth = metrics.stringWidth(timeLabel); + g2d.setColor(new Color(80, 80, 120)); + g2d.drawString(timeLabel, x - labelWidth / 2, height - padding + 20); + } + } + + // 绘制成功线(荧光蓝色)- 使用带标签的方法替代原方法 + drawGlowingLineWithLabels(g2d, + calculateXPoints(totalPoints, padding, chartWidth), + calculateSuccessYPoints(monitoringData, totalPoints, height, padding, chartHeight, yDivisions, yStep), + new Color(0, 191, 255), new Color(0, 120, 215), 4.0f, + monitoringData, true); + + // 绘制失败线(荧光红色)- 使用带标签的方法替代原方法 + drawGlowingLineWithLabels(g2d, + calculateXPoints(totalPoints, padding, chartWidth), + calculateFailureYPoints(monitoringData, totalPoints, height, padding, chartHeight, yDivisions, yStep), + new Color(255, 50, 100), new Color(200, 30, 80), 4.0f, + monitoringData, false); + + // 绘制图表边框 + g2d.setColor(new Color(210, 210, 230)); + g2d.setStroke(new BasicStroke(1.5f)); + g2d.drawRect(padding, topPadding, chartWidth, chartHeight); // 调整边框位置 + + // 释放资源 + g2d.dispose(); + + // 在底部添加错误信息 + // 重新获取图像的Graphics2D对象 + g2d = image.createGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2d.setColor(new Color(100, 100, 130)); + g2d.setFont(new Font("Microsoft YaHei", Font.PLAIN, 12)); + + // 使用动态传入的错误信息,而非硬编码 + if (errorMessage != null && !errorMessage.trim().isEmpty()) { + g2d.drawString(errorMessage, 70, height - 10); + } + + g2d.dispose(); + + // 保存图像 + ImageIO.write(image, "png", outputFile); + } + + /** + * 添加科技感背景 + */ + private void drawTechBackground(Graphics2D g2d, int width, int height) { + g2d.setColor(new Color(240, 240, 250, 120)); + g2d.setStroke(new BasicStroke(0.5f)); + + // 小网格 + int smallGridSize = 15; + for (int x = 0; x < width; x += smallGridSize) { + g2d.drawLine(x, 0, x, height); + } + for (int y = 0; y < height; y += smallGridSize) { + g2d.drawLine(0, y, width, y); + } + } + + /** + * 绘制发光线条 + */ + private void drawGlowingLine(Graphics2D g2d, int[] xPoints, int[] yPoints, Color mainColor, Color glowColor, float thickness) { + int totalPoints = xPoints.length; + + // 绘制发光效果(外层) + g2d.setColor(new Color(glowColor.getRed(), glowColor.getGreen(), glowColor.getBlue(), 80)); + g2d.setStroke(new BasicStroke(thickness + 4.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + for (int i = 0; i < totalPoints - 1; i++) { + g2d.drawLine(xPoints[i], yPoints[i], xPoints[i + 1], yPoints[i + 1]); + } + + // 绘制发光效果(中层) + g2d.setColor(new Color(glowColor.getRed(), glowColor.getGreen(), glowColor.getBlue(), 120)); + g2d.setStroke(new BasicStroke(thickness + 2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + for (int i = 0; i < totalPoints - 1; i++) { + g2d.drawLine(xPoints[i], yPoints[i], xPoints[i + 1], yPoints[i + 1]); + } + + // 绘制主线 + g2d.setColor(mainColor); + g2d.setStroke(new BasicStroke(thickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + for (int i = 0; i < totalPoints - 1; i++) { + g2d.drawLine(xPoints[i], yPoints[i], xPoints[i + 1], yPoints[i + 1]); + } + + // 绘制高亮数据点 + for (int i = 0; i < totalPoints; i++) { + // 外发光 + g2d.setColor(new Color(glowColor.getRed(), glowColor.getGreen(), glowColor.getBlue(), 80)); + g2d.fillOval(xPoints[i] - 6, yPoints[i] - 6, 12, 12); + + // 中发光 + g2d.setColor(new Color(mainColor.getRed(), mainColor.getGreen(), mainColor.getBlue(), 150)); + g2d.fillOval(xPoints[i] - 4, yPoints[i] - 4, 8, 8); + + // 内部点 + g2d.setColor(Color.WHITE); + g2d.fillOval(xPoints[i] - 2, yPoints[i] - 2, 4, 4); + } + } + + /** + * 绘制带有数值标签的发光线条 + */ + private void drawGlowingLineWithLabels(Graphics2D g2d, int[] xPoints, int[] yPoints, + Color mainColor, Color glowColor, float thickness, + List data, boolean isSuccess) { + // 先绘制基本的发光线条 + drawGlowingLine(g2d, xPoints, yPoints, mainColor, glowColor, thickness); + // 添加数值标签 - 使用普通字体而非粗体 + g2d.setFont(new Font("Microsoft YaHei", Font.PLAIN, 11)); + FontMetrics metrics = g2d.getFontMetrics(); + + for (int i = 0; i < xPoints.length; i++) { + // 获取当前值 + int currentValue = isSuccess ? data.get(i).getSuccessCount() : data.get(i).getFailureCount(); + + // 始终显示所有数据点的数值标签 + String label = String.valueOf(currentValue); + int labelWidth = metrics.stringWidth(label); + + // 设置标签文本 + g2d.setColor(mainColor.darker()); + g2d.drawString(label, xPoints[i] - labelWidth/2, yPoints[i] - 5); + } + } + + /** + * 计算X坐标点 + */ + private int[] calculateXPoints(int totalPoints, int padding, int chartWidth) { + int[] points = new int[totalPoints]; + for (int i = 0; i < totalPoints; i++) { + points[i] = padding + (i * chartWidth / (totalPoints - 1)); + } + return points; + } + + /** + * 计算成功线的Y坐标点 + */ + private int[] calculateSuccessYPoints(List data, int totalPoints, int height, + int padding, int chartHeight, int yDivisions, int yStep) { + int[] points = new int[totalPoints]; + for (int i = 0; i < totalPoints; i++) { + int successScaled = (int)((double)data.get(i).getSuccessCount() * chartHeight / (yDivisions * yStep)); + points[i] = height - padding - successScaled; + } + return points; + } + + /** + * 计算失败线的Y坐标点 + */ + private int[] calculateFailureYPoints(List data, int totalPoints, int height, + int padding, int chartHeight, int yDivisions, int yStep) { + int[] points = new int[totalPoints]; + for (int i = 0; i < totalPoints; i++) { + int failureScaled = (int)((double)data.get(i).getFailureCount() * chartHeight / (yDivisions * yStep)); + points[i] = height - padding - failureScaled; + } + return points; + } +} \ No newline at end of file diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/LarkClientUtil.java b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/LarkClientUtil.java new file mode 100644 index 0000000..5375137 --- /dev/null +++ b/tashow-sdk/tashow-feishu-sdk/src/main/java/com/tashow/cloud/sdk/feishu/util/LarkClientUtil.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.sdk.feishu.util; +import com.lark.oapi.Client; +import com.tashow.cloud.sdk.feishu.config.LarkConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 飞书客户端工具类 + * 用于创建和获取飞书客户端实例 + */ +@Component +public class LarkClientUtil { + + private final LarkConfig larkConfig; + + @Autowired + public LarkClientUtil(LarkConfig larkConfig) { + this.larkConfig = larkConfig; + } + + /** + * 获取飞书客户端实例 + * @return 飞书客户端 + */ + public Client getLarkClient() { + return Client.newBuilder(larkConfig.getAppId(), larkConfig.getAppSecret()).build(); + } +} \ No newline at end of file diff --git a/tashow-sdk/tashow-feishu-sdk/src/main/resources/card.json b/tashow-sdk/tashow-feishu-sdk/src/main/resources/card.json new file mode 100644 index 0000000..327b1b8 --- /dev/null +++ b/tashow-sdk/tashow-feishu-sdk/src/main/resources/card.json @@ -0,0 +1,266 @@ +{ + "name": "12", + "dsl": { + "schema": "2.0", + "config": { + "update_multi": true, + "locales": [ + "en_us" + ], + "style": { + "text_size": { + "normal_v2": { + "default": "normal", + "pc": "normal", + "mobile": "heading" + } + } + } + }, + "body": { + "direction": "vertical", + "padding": "12px 12px 12px 12px", + "elements": [ + { + "tag": "column_set", + "horizontal_spacing": "8px", + "horizontal_align": "left", + "columns": [ + { + "tag": "column", + "width": "weighted", + "elements": [ + { + "tag": "markdown", + "content": "负责人\n", + "i18n_content": { + "en_us": "Alert details\nMobile client crash rate at 5%" + }, + "text_align": "left", + "text_size": "normal_v2", + "margin": "0px 0px 0px 0px", + "icon": { + "tag": "standard_icon", + "token": "contacts_outlined", + "color": "grey" + } + } + ], + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "weight": 1 + }, + { + "tag": "column", + "width": "weighted", + "elements": [ + { + "tag": "markdown", + "content": "失败数量\n${fail_count}", + "i18n_content": { + "en_us": "Diagnostic info\nService request volume exceeds rate limit" + }, + "text_align": "left", + "text_size": "normal_v2", + "margin": "0px 0px 0px 0px", + "icon": { + "tag": "standard_icon", + "token": "meego_colorful", + "color": "grey" + } + } + ], + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "weight": 1 + } + ], + "margin": "0px 0px 0px 0px" + }, + { + "tag": "column_set", + "horizontal_spacing": "8px", + "horizontal_align": "left", + "columns": [ + { + "tag": "column", + "width": "weighted", + "elements": [ + { + "tag": "markdown", + "content": "项目\nTashow平台", + "i18n_content": { + "en_us": "Priority level\nP0" + }, + "text_align": "left", + "text_size": "normal_v2", + "margin": "0px 0px 0px 0px", + "icon": { + "tag": "standard_icon", + "token": "file-form_colorful", + "color": "grey" + } + } + ], + "direction": "vertical", + "horizontal_spacing": "8px", + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "weight": 1 + }, + { + "tag": "column", + "width": "weighted", + "elements": [ + { + "tag": "markdown", + "content": "告警时间\n${current_time}", + "i18n_content": { + "en_us": "Incident time\n${alarm_time}" + }, + "text_align": "left", + "text_size": "normal_v2", + "margin": "0px 0px 0px 0px", + "icon": { + "tag": "standard_icon", + "token": "calendar_colorful", + "color": "grey" + } + } + ], + "direction": "vertical", + "horizontal_spacing": "8px", + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "weight": 1 + } + ], + "margin": "0px 0px 0px 0px" + }, + { + "tag": "form", + "elements": [ + { + "tag": "img", + "img_key": "img_v3_02nc_085db227-0547-40eb-90a1-dd80434b229g", + "preview": true, + "transparent": false, + "scale_type": "fit_horizontal", + "margin": "0px 0px 0px 0px" + }, + { + "tag": "input", + "placeholder": { + "tag": "plain_text", + "content": "处理情况说明,选填", + "i18n_content": { + "en_us": "Action taken (if any)" + } + }, + "default_value": "", + "width": "fill", + "name": "notes_input", + "margin": "0px 0px 0px 0px" + }, + { + "tag": "column_set", + "horizontal_align": "left", + "columns": [ + { + "tag": "column", + "width": "auto", + "elements": [ + { + "tag": "button", + "text": { + "tag": "plain_text", + "content": "处理完成", + "i18n_content": { + "en_us": "Mark as Resolved" + } + }, + "type": "primary", + "width": "default", + "behaviors": [ + { + "type": "callback", + "value": { + "action": "complete_alarm", + "time": "${alarm_time}" + } + } + ], + "form_action_type": "submit", + "name": "Button_m6vy7xom" + } + ], + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top" + } + ], + "margin": "0px 0px 0px 0px" + } + ], + "direction": "vertical", + "padding": "4px 0px 4px 0px", + "margin": "0px 0px 0px 0px", + "name": "Form_m6vy7xol" + } + ] + }, + "header": { + "title": { + "tag": "plain_text", + "content": "${alert_title}", + "i18n_content": { + "en_us": "[Action Needed] Alert: Process Error - Please Address Promptly" + } + }, + "subtitle": { + "tag": "plain_text", + "content": "" + }, + "template": "red", + "icon": { + "tag": "standard_icon", + "token": "warning-hollow_filled" + }, + "padding": "12px 12px 12px 12px" + } + }, + "variables": [ + { + "type": "text", + "apiName": "var_m6vy7ngf", + "name": "alarm_time", + "desc": "告警时间", + "mockData": "2025-01-01 10:10:08" + }, + { + "type": "text", + "apiName": "var_mc1d8e1w", + "name": "fail_count", + "desc": "", + "mockData": "0" + }, + { + "type": "text", + "apiName": "var_mc1d8e1z", + "name": "current_time", + "desc": "", + "mockData": "2025-06-17 17:32:13" + }, + { + "type": "text", + "apiName": "var_mc1d8e6b", + "name": "alert_title", + "desc": "", + "mockData": "埋点数据异常告警" + } + ] +} \ No newline at end of file