提交
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
|
||||
<modules>
|
||||
<module>tashow-sdk-payment</module>
|
||||
<module>tashow-feishu-sdk</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
|
||||
44
tashow-sdk/tashow-feishu-sdk/pom.xml
Normal file
44
tashow-sdk/tashow-feishu-sdk/pom.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.tashow.cloud</groupId>
|
||||
<artifactId>tashow-sdk</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>tashow-feishu-sdk</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.tashow.cloud</groupId>
|
||||
<artifactId>tashow-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.larksuite.oapi/oapi-sdk -->
|
||||
<dependency>
|
||||
<groupId>com.larksuite.oapi</groupId>
|
||||
<artifactId>oapi-sdk</artifactId>
|
||||
<version>2.4.18</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<version>4.5.13</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<artifactId>junit</artifactId>
|
||||
<groupId>junit</groupId>
|
||||
<scope>test</scope>
|
||||
<version>4.13.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tashow.cloud</groupId>
|
||||
<artifactId>tashow-common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -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<String, Object> data);
|
||||
/**
|
||||
* 获取卡片数据
|
||||
* @param messageId 消息ID
|
||||
* @return 卡片数据
|
||||
*/
|
||||
Map<String, Object> 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<ChartImageGenerator.MonitoringDataPoint> 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<ChartImageGenerator.MonitoringDataPoint> buriedPointData, int failCount, String errorMessage) throws Exception {
|
||||
HashMap<String, Object> 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<ChartImageGenerator.MonitoringDataPoint> 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<ChartImageGenerator.MonitoringDataPoint> 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<String, Object> 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<ChartImageGenerator.MonitoringDataPoint> 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<ChartImageGenerator.MonitoringDataPoint> 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<String, Object> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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<MonitoringDataPoint> monitoringData) throws IOException {
|
||||
generateDashboardImage(outputFile, monitoringData, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成监控仪表盘图像(带错误信息)
|
||||
* @param outputFile 输出文件
|
||||
* @param monitoringData 监控数据
|
||||
* @param errorMessage 错误信息,如为null则不显示
|
||||
* @throws IOException 如果图像创建失败
|
||||
*/
|
||||
public void generateDashboardImage(File outputFile, List<MonitoringDataPoint> 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<MonitoringDataPoint> 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<MonitoringDataPoint> 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<MonitoringDataPoint> 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
266
tashow-sdk/tashow-feishu-sdk/src/main/resources/card.json
Normal file
266
tashow-sdk/tashow-feishu-sdk/src/main/resources/card.json
Normal file
@@ -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": "<font color=\"grey\">负责人</font>\n<at id=all></at>",
|
||||
"i18n_content": {
|
||||
"en_us": "<font color=\"grey\">Alert details</font>\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": "<font color=\"grey\">失败数量</font>\n${fail_count}",
|
||||
"i18n_content": {
|
||||
"en_us": "<font color=\"grey\">Diagnostic info</font>\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": "<font color=\"grey\">项目</font>\nTashow平台",
|
||||
"i18n_content": {
|
||||
"en_us": "<font color=\"grey\">Priority level</font>\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": "<font color=\"grey\">告警时间</font>\n${current_time}",
|
||||
"i18n_content": {
|
||||
"en_us": "<font color=\"grey\">Incident time</font>\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": "埋点数据异常告警"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user