This commit is contained in:
2025-06-18 17:14:27 +08:00
parent e384dc1163
commit 98bb3529ea
41 changed files with 2776 additions and 335 deletions

View File

@@ -12,6 +12,7 @@
<modules>
<module>tashow-sdk-payment</module>
<module>tashow-feishu-sdk</module>
</modules>
</project>

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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": "埋点数据异常告警"
}
]
}