版本更新

This commit is contained in:
hahwu 2025-04-02 10:52:51 +08:00
parent 3f1130d367
commit a5f4fe4946
22 changed files with 5756 additions and 402 deletions

View File

@ -49,3 +49,6 @@ type CardVchartHeat struct {
type CardVchartData struct {
Values string
}
type CardNotifyData struct {
}

View File

@ -84,3 +84,13 @@ type DecorateData struct {
Value []int `json:"value"`
Value2 []int `json:"value2"`
}
type NotifyData struct {
Host string `json:"host"`
EventName string `json:"event_name"`
Severity string `json:"severity"`
AlarmTime string `json:"alarm_time"`
Info string `json:"info"`
Id string `json:"id"`
NotifyMsg string `json:"notify_msg"`
}

View File

@ -41,7 +41,7 @@ type Config struct {
var config *Config
func init() {
func Init() {
err := loadConfig()
if err != nil {
log.Fatalf("Failed to load config: %v", err)

View File

@ -14,7 +14,6 @@ import (
)
func FeishuSendInfo(c *gin.Context) {
// TODO
Result, err := util.GetOperation(3)
if err != nil {
log.Printf("failed to get operation: %v", err)
@ -27,7 +26,6 @@ func FeishuSendInfo(c *gin.Context) {
}
func FeishuSendInfo2(c *gin.Context) {
// TODO
err := feishu.SendOperationMsg2(3)
if err != nil {
log.Printf("failed to send operation message: %v", err)
@ -35,7 +33,6 @@ func FeishuSendInfo2(c *gin.Context) {
}
func FeishuSendWeekInfo(c *gin.Context) {
// TODO
Result, err := util.GetOperation(3)
if err != nil {
log.Printf("failed to get operation: %v", err)
@ -140,3 +137,16 @@ func FeishuServerInfo(c *gin.Context) {
log.Println(err)
}
}
func FeishuNotify(c *gin.Context) {
r := Type.NotifyData{}
if err := c.ShouldBindJSON(&r); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
err := feishu.SendNotifyMsg(&r)
if err != nil {
log.Printf("failed to send notify message: %v", err)
}
}

View File

@ -14,6 +14,15 @@ import (
"time"
)
var NOTIFY_TITLE_COLOR = map[string]string{
"Not classified": "grey",
"Information": "blue",
"Warning": "yellow",
"Average": "orange",
"High": "red",
"Disaster": "carmine",
}
func SendFeishuMsg(msg string) error {
// 创建请求体
payload := map[string]interface{}{
@ -475,7 +484,7 @@ func SendOperationMsg2(AppId int) error {
}
elementsStr := strings.Join(elements, ",")
data := Type.Card{
Title: "运营报",
Title: "运营报",
Elements: elementsStr,
Tag1: "UK",
}
@ -489,3 +498,35 @@ func SendOperationMsg2(AppId int) error {
c.SendGroupMsg(common.GetOperationChatId(), common.FEISHU_CART_TYPE, s)
return nil
}
func SendNotifyMsg(data *Type.NotifyData) error {
TitleColor := NOTIFY_TITLE_COLOR[data.Severity]
payload := map[string]interface{}{
"type": "template",
"data": map[string]interface{}{
"template_id": "AAqB4RTngW8Wt",
"template_version_name": "1.0.7",
"template_variable": map[string]interface{}{
"Title": "待处理",
"TitleColor": TitleColor,
"InputMsg": "处理情况说明,选填",
"HostName": data.Host,
"EventName": data.EventName,
"EventSeverity": data.Severity,
"alarm_time": data.AlarmTime,
"NotifyMsg": data.NotifyMsg,
},
},
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return err
}
c := client.GetClient()
err = c.SendGroupMsg(common.GetOperationChatId(), common.FEISHU_CART_TYPE, string(payloadBytes))
if err != nil {
log.Printf("sendNotifyMsg error %v", err)
}
return nil
}

54
feishu/handle/card.go Normal file
View File

@ -0,0 +1,54 @@
package handle
import (
"backend/Type"
"backend/util"
"context"
"encoding/json"
"log"
"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher/callback"
)
var handle map[string]func(ctx context.Context, event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error)
func init() {
handle = make(map[string]func(ctx context.Context, event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error))
handle["complete_alarm"] = NotifyCard
}
func Handle(ctx context.Context, event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error) {
if h, ok := handle[event.Event.Action.Value["action"].(string)]; ok {
return h(ctx, event)
}
return nil, nil
}
func NotifyCard(ctx context.Context, event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error) {
d := Type.NotifyData{
Host: event.Event.Action.Value["HostName"].(string),
EventName: event.Event.Action.Value["EventName"].(string),
Severity: event.Event.Action.Value["EventSeverity"].(string),
AlarmTime: event.Event.Action.Value["AlarmTime"].(string),
Info: event.Event.Action.FormValue["notes_input"].(string),
NotifyMsg: event.Event.Action.Value["NotifyMsg"].(string),
Id: event.Event.Operator.OpenID,
}
str := util.ParseTmpl("./template/card_notify.tmpl", d)
r := make(map[string]interface{})
err := json.Unmarshal([]byte(str), &r)
if err != nil {
log.Println(str)
log.Printf("notify card %v", err)
}
return &callback.CardActionTriggerResponse{
Toast: &callback.Toast{
Type: "info",
Content: "已收到",
},
Card: &callback.Card{
Type: "raw",
Data: r,
},
}, nil
}

View File

@ -1,8 +1,10 @@
package server
import (
"backend/common"
"backend/feishu/client"
"backend/feishu/data"
"backend/feishu/handle"
"context"
"log"
@ -15,6 +17,9 @@ import (
)
func Server() {
if common.GetNMap() {
return
}
// 注册事件回调OnP2MessageReceiveV1 为接收消息 v2.0OnCustomizedEvent 内的 message 为接收消息 v1.0。
eventHandler := dispatcher.NewEventDispatcher("", "").
OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
@ -29,7 +34,7 @@ func Server() {
})
eventHandler.OnP2CardActionTrigger(func(ctx context.Context, event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error) {
log.Printf("[ OnP2CardActionTrigger access ], data: %s\n", larkcore.Prettify(event))
return nil, nil
return handle.Handle(ctx, event)
})
// 创建Client

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
{
"schema": "2.0",
"config": {
"update_multi": true,
"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移动端打开异常率达到5%",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
},
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "markdown",
"content": "<font color=\"grey\">错误信息</font>\n服务请求数量过多超出限频",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"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>\nP0",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
},
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "markdown",
"content": "<font color=\"grey\">告警时间</font>\n${alarm_time}",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"direction": "vertical",
"horizontal_spacing": "8px",
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
}
],
"margin": "0px 0px 0px 0px"
},
{
"tag": "hr",
"margin": "0px 0px 0px 0px"
},
{
"tag": "form",
"elements": [
{
"tag": "input",
"placeholder": {
"tag": "plain_text",
"content": "处理情况说明,选填"
},
"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": "处理完成"
},
"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": "[待处理] 告警通知:服务器出错请及时处理"
},
"subtitle": {
"tag": "plain_text",
"content": ""
},
"template": "red",
"icon": {
"tag": "standard_icon",
"token": "succeed-hollow_filled"
},
"padding": "12px 12px 12px 12px"
}
}

11
main.go
View File

@ -1,7 +1,9 @@
package main
import (
"backend/common"
"backend/controller"
"backend/feishu/server"
"backend/util"
"log"
"os"
@ -11,6 +13,11 @@ import (
// GOOS=linux GOARCH=amd64 go build -o /data/backend/release/backend main.go
func init() {
common.Init()
// 初始化日志文件夹
}
func main() {
r := gin.Default()
// 以追加模式打开或创建日志文件
@ -58,9 +65,9 @@ func main() {
api.POST("/feishu/sendWeekInfo", controller.FeishuSendWeekInfo)
api.POST("/feishu/updateApp", controller.FeishuUpdateApp)
api.POST("/feishu/serverInfo", controller.FeishuServerInfo)
api.POST("/feishu/notify", controller.FeishuNotify)
}
go controller.AppPortNmap()
go util.ScheduleDailyTask()
// go server.Server()
go server.Server()
r.Run(":5320") // 在 0.0.0.0:5320 上监听并服务
}

View File

@ -65,8 +65,8 @@ func (s *Statistics) StatisticsInfo() (interface{}, error) {
if LogDb == nil {
return nil, fmt.Errorf("failed to get mysql database")
}
StartTime := util.ZeroTimestampByTz(AppConfig.Tz)
EndTime := StartTime + 86400
StartTime, _ := util.GetZeroTimestamp(AppConfig.Tz, 0)
EndTime, _ := util.GetZeroTimestamp(AppConfig.Tz, 1)
var Register int
var TotalRegistger int
var Recharge float64

View File

@ -12,11 +12,17 @@ type Log struct {
CurrentPage int `json:"CurrentPage"`
AppId int `json:"AppId"`
EventParam string `json:"Event"`
StartTime string `json:"StartTime"`
EndTime string `json:"EndTime"`
ItemId int `json:"ItemId"`
}
type ResAsset struct {
Total int `json:"total"`
Data []*ResAssetDetail `json:"data"`
Sum int `json:"sum"` // 总和
NSum int `json:"nsum"` // 负数和
PSum int `json:"psum"` // 正数和
}
type ResEvent struct {
@ -71,28 +77,67 @@ func (m *Log) Asset() (*ResAsset, error) {
if err != nil {
return nil, err
}
StartTime, err := util.ParseTimeToTimestamp(m.StartTime, AppConfig.Tz)
if err != nil {
return nil, err
}
EndTime, err := util.ParseTimeToTimestamp(m.EndTime, AppConfig.Tz)
if err != nil {
return nil, err
}
Db := util.MPool.GetTopicDB(AppConfig.Topic)
defer Db.Close()
if Db == nil {
return nil, fmt.Errorf("failed to get mysql database")
}
assets := []*Event{}
err = Db.Select(&assets, "SELECT * FROM log_event WHERE Uid = ? and `Event` = 'asset_change' ORDER BY Timestamp DESC LIMIT ?, ?", m.Uid, (m.CurrentPage-1)*m.PageSize, m.PageSize)
value := make([]interface{}, 0)
totalValue := make([]interface{}, 0)
Sql := "SELECT * FROM log_event WHERE Uid = ? and `Event` = 'asset_change' and Timestamp >= ? and Timestamp <= ? "
TotalSql := "SELECT COUNT(*) FROM log_event WHERE Uid = ? and `Event` = 'asset_change' and Timestamp >= ? and Timestamp <= ? "
totalValue = append(totalValue, m.Uid, StartTime, EndTime)
value = append(value, m.Uid, StartTime, EndTime)
if m.ItemId != 0 {
Sql += "and Param like ? "
value = append(value, fmt.Sprintf("%%\"item_id\":%d%%", m.ItemId))
TotalSql += "and Param like ? "
totalValue = append(totalValue, fmt.Sprintf("%%\"item_id\":%d%%", m.ItemId))
}
if m.EventParam != "" {
Sql += "and Param like ? "
value = append(value, fmt.Sprintf("%%\"change_type\":\"%s\"%%", m.EventParam))
TotalSql += "and Param like ? "
totalValue = append(totalValue, fmt.Sprintf("%%\"change_type\":\"%s\"%%", m.EventParam))
}
value = append(value, (m.CurrentPage-1)*m.PageSize, m.PageSize)
Sql += "ORDER BY Timestamp DESC LIMIT ?, ?"
err = Db.Select(&assets, Sql, value...)
if err != nil {
return nil, fmt.Errorf("failed to get asset list: %v", err)
}
var total int
err = Db.QueryRow("SELECT COUNT(*) FROM log_event WHERE Uid = ? and `Event` = 'asset_change'", m.Uid).Scan(&total)
err = Db.QueryRow(TotalSql, totalValue...).Scan(&total)
if err != nil {
return nil, fmt.Errorf("failed to get asset count: %v", err)
}
resData := []*ResAssetDetail{}
Sum := 0
NSum := 0
PSum := 0
for _, asset := range assets {
param := map[string]interface{}{}
err := json.Unmarshal([]byte(asset.Param), &param)
if err != nil {
continue
}
Sum += util.Int(param["change_num"])
if param["change_type"].(string) == "gain" {
PSum += util.Int(param["change_num"])
} else {
NSum += util.Int(param["change_num"])
}
resData = append(resData, &ResAssetDetail{
Uid: asset.Uid,
ChangeType: param["change_type"].(string),
@ -105,6 +150,9 @@ func (m *Log) Asset() (*ResAsset, error) {
return &ResAsset{
Total: total,
Data: resData,
Sum: Sum,
NSum: NSum,
PSum: PSum,
}, nil
}
@ -119,17 +167,43 @@ func (m *Log) Event() (*ResEvent, error) {
if Db == nil {
return nil, fmt.Errorf("failed to get mysql database")
}
assets := []*Event{}
if m.EventParam != "" {
err = Db.Select(&assets, "SELECT * FROM log_event WHERE Uid = ? and `Event` = ? ORDER BY Timestamp DESC LIMIT ?, ?", m.Uid, m.EventParam, (m.CurrentPage-1)*m.PageSize, m.PageSize)
} else {
err = Db.Select(&assets, "SELECT * FROM log_event WHERE Uid = ? and `Event` != 'asset_change' ORDER BY Timestamp DESC LIMIT ?, ?", m.Uid, (m.CurrentPage-1)*m.PageSize, m.PageSize)
StartTime, err := util.ParseTimeToTimestamp(m.StartTime, AppConfig.Tz)
if err != nil {
return nil, err
}
EndTime, err := util.ParseTimeToTimestamp(m.EndTime, AppConfig.Tz)
if err != nil {
return nil, err
}
assets := []*Event{}
Sql := "SELECT * FROM log_event WHERE Uid = ? and `Event` != 'asset_change' and Timestamp >= ? and Timestamp <= ? "
Value := make([]interface{}, 0)
Value = append(Value, m.Uid, StartTime, EndTime)
if m.EventParam != "" {
Sql += "and `Event` = ? "
Value = append(Value, m.EventParam)
}
Sql += "ORDER BY Timestamp DESC LIMIT ?, ?"
Value = append(Value, (m.CurrentPage-1)*m.PageSize, m.PageSize)
err = Db.Select(&assets, Sql, Value...)
// if m.EventParam != "" {
// err = Db.Select(&assets, "SELECT * FROM log_event WHERE Uid = ? and `Event` = ? ORDER BY Timestamp DESC LIMIT ?, ?", m.Uid, m.EventParam, (m.CurrentPage-1)*m.PageSize, m.PageSize)
// } else {
// err = Db.Select(&assets, "SELECT * FROM log_event WHERE Uid = ? and `Event` != 'asset_change' ORDER BY Timestamp DESC LIMIT ?, ?", m.Uid, (m.CurrentPage-1)*m.PageSize, m.PageSize)
// }
if err != nil {
return nil, fmt.Errorf("failed to get event list: %v", err)
}
var total int
err = Db.QueryRow("SELECT COUNT(*) FROM log_event WHERE Uid = ? and `Event` != 'asset_change'", m.Uid).Scan(&total)
totalSql := "SELECT COUNT(*) FROM log_event WHERE Uid = ? and `Event` != 'asset_change' and Timestamp >= ? and Timestamp <= ? "
totalValue := make([]interface{}, 0)
totalValue = append(totalValue, m.Uid, StartTime, EndTime)
if m.EventParam != "" {
totalSql += "and `Event` = ? "
totalValue = append(totalValue, m.EventParam)
}
err = Db.QueryRow(totalSql, totalValue...).Scan(&total)
//err = Db.QueryRow("SELECT COUNT(*) FROM log_event WHERE Uid = ? and `Event` != 'asset_change'", m.Uid).Scan(&total)
if err != nil {
return nil, fmt.Errorf("failed to get event count: %v", err)
}

View File

@ -21,6 +21,7 @@ type Mail struct {
Items string `json:"items" db:"items"`
RegisterTime int64 `json:"register_time" db:"register_time"`
MailType int `json:"mail_type" db:"mail_type"`
SendType int `json:"send_type" db:"send_type"`
ToUids string `json:"to_uids" db:"to_uids"`
CreateTime int64 `json:"create_time" db:"create_time"`
}
@ -39,7 +40,7 @@ func (m *Mail) MailList() (*Result, error) {
Db := util.MPool.GetMysqlDB(AppCfg.ServerName, AppCfg.MysqlName, m.ServerId)
defer Db.Close()
var mail []*Mail
err = Db.Select(&mail, "SELECT `mail_id`, `title`, `content`, `start_time`, `end_time`, `items`, `register_time`, `mail_type`, `to_uids`, `create_time` FROM system_mail_info")
err = Db.Select(&mail, "SELECT `mail_id`, `title`, `content`, `start_time`, `end_time`, `items`, `register_time`, `mail_type`,`send_type`, `to_uids`, `create_time` FROM system_mail_info")
if err != nil {
return nil, fmt.Errorf("failed to scan rows: %v", err)
}
@ -60,7 +61,7 @@ func (m *Mail) SendMail() error {
}
Db := util.MPool.GetMysqlDB(AppCfg.ServerName, AppCfg.MysqlName, m.ServerId)
defer Db.Close()
_, err = Db.Exec("INSERT INTO system_mail_info (`title`, `content`, `start_time`, `end_time`, `items`, `register_time`, `mail_type`, `to_uids`, `create_time`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", m.Title, m.Content, m.StartTime, m.EndTime, m.Items, m.RegisterTime, m.MailType, m.ToUids, m.CreateTime)
_, err = Db.Exec("INSERT INTO system_mail_info (`title`, `content`, `start_time`, `end_time`, `items`, `register_time`, `mail_type`, `send_type`, `to_uids`, `create_time`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.Title, m.Content, m.StartTime, m.EndTime, m.Items, m.RegisterTime, m.MailType, m.SendType, m.ToUids, m.CreateTime)
if err != nil {
return fmt.Errorf("failed to insert mail: %v", err)
}

Binary file not shown.

BIN
release/zabbix Normal file

Binary file not shown.

62
scripts/zabbix/main.go Normal file
View File

@ -0,0 +1,62 @@
package main
import (
"backend/msg"
"backend/util"
"fmt"
"os"
"strconv"
"time"
"golang.org/x/net/websocket"
)
func main() {
// 获取命令行参数
args := os.Args
Status := 0
if len(args) < 2 {
fmt.Print(Status)
return
}
Host := args[1]
Port, err := strconv.Atoi(args[2])
if err != nil {
fmt.Print(Status)
return
}
ws, err := getwebsocket(Host, Port)
if err != nil {
fmt.Print(Status)
return
}
ws.SetReadDeadline(time.Now().Add(2 * time.Second))
req := &msg.ReqServerInfo{}
_, err = util.SendAdminMsg(ws, req)
if err != nil {
//log.Printf("failed to send admin message: %v", err)
fmt.Print(Status)
return
}
Status = 1
fmt.Print(Status)
}
func getwebsocket(Host string, Port int) (*websocket.Conn, error) {
origin := "http://localhost/"
url := fmt.Sprintf("ws://%s:%d/", Host, Port)
config, err := websocket.NewConfig(url, origin)
if err != nil {
return nil, fmt.Errorf("failed to create websocket config: %v", err)
}
// config.Dialer = &net.Dialer{
// Timeout: 5 * time.Second,
// }
ws, err := websocket.DialConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to connect to websocket: %v", err)
}
return ws, nil
}

BIN
scripts/zabbix/zabbix Normal file

Binary file not shown.

173
template/c1.tmpl Normal file
View File

@ -0,0 +1,173 @@
{
"schema": "2.0",
"config": {
"update_multi": true,
"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**{{.Host}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
},
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "markdown",
"content": "<font color=\"grey\">错误信息</font>\n**{{.EventName}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"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>\n**{{.Severity}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
},
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "markdown",
"content": "<font color=\"grey\">告警时间</font>\n**{{.AlarmTime}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"direction": "vertical",
"horizontal_spacing": "8px",
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
}
],
"margin": "0px 0px 0px 0px"
},
{
"tag": "hr",
"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>\n**{{.Info}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
},
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "markdown",
"content": "<font color=\"grey\">处理人</font>\n<at id='{{.Id}}'></at>",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
}
],
"margin": "0px 0px 0px 0px"
}
]
},
"header": {
"title": {
"tag": "plain_text",
"content": "[已处理] 告警通知:{{.NotifyMsg}}"
},
"subtitle": {
"tag": "plain_text",
"content": ""
},
"template": "red",
"icon": {
"tag": "standard_icon",
"token": "succeed-hollow_filled"
},
"padding": "12px 12px 12px 12px"
}
}

173
template/card_notify.tmpl Normal file
View File

@ -0,0 +1,173 @@
{
"schema": "2.0",
"config": {
"update_multi": true,
"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**{{.Host}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
},
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "markdown",
"content": "<font color=\"grey\">错误信息</font>\n**{{.EventName}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"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>\n**{{.Severity}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
},
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "markdown",
"content": "<font color=\"grey\">告警时间</font>\n**{{.AlarmTime}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"direction": "vertical",
"horizontal_spacing": "8px",
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
}
],
"margin": "0px 0px 0px 0px"
},
{
"tag": "hr",
"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>\n**{{.Info}}**",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
},
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "markdown",
"content": "<font color=\"grey\">处理人</font>\n<at id='{{.Id}}'></at>",
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
}
],
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
}
],
"margin": "0px 0px 0px 0px"
}
]
},
"header": {
"title": {
"tag": "plain_text",
"content": "[已处理] 告警通知:{{.NotifyMsg}}"
},
"subtitle": {
"tag": "plain_text",
"content": ""
},
"template": "green",
"icon": {
"tag": "standard_icon",
"token": "succeed-hollow_filled"
},
"padding": "12px 12px 12px 12px"
}
}

View File

@ -1,26 +1,27 @@
package main
import (
"backend/Type"
"backend/controller"
"backend/feishu"
"backend/util"
"fmt"
"testing"
)
func TestXxx1(t *testing.T) {
// controller.FeishuSendInfo(nil)
str := `
## 日期2021-09-01
## 代办事项
- [ ] 任务1
- [ ] 任务2
- [ ] 任务3
`
str = util.ToTmplStr(str)
fmt.Println(str)
d := Type.NotifyData{
Host: "Zabbix tencent-test",
AlarmTime: "2025-03-06 19:12:56",
Severity: "High",
EventName: "游戏进程down-test-node-1",
NotifyMsg: "服务器出错请及时处理",
}
feishu.SendNotifyMsg(&d)
}
func TestXxx(t *testing.T) {
controller.FeishuSendInfo2(nil)
controller.FeishuSendInfo(nil)
// util.GetOrderData(3)
}
func TestFeishu(t *testing.T) {

View File

@ -51,7 +51,7 @@ func remain(Day int, LogDb *Db, AppConfig *Type.AppStruct) {
}
}()
now := ZeroTimestampByTz(AppConfig.Tz) + int64(Day)*86400
now, _ := GetZeroTimestamp(AppConfig.Tz, Day)
Date := time.Unix(now, 0).In(loc).Format("2006-01-02")
SecondRemain, _ := DayRemain(now, 1, LogDb) // 计算次留
ThirdRemain, _ := DayRemain(now, 2, LogDb) // 计算三留
@ -218,10 +218,8 @@ func GetOperation(AppId int) (*Type.Operation, error) {
defer Db.Close()
Retain := []*Type.Retain{}
ZeroTimestamp := ZeroTimestampByTz("Europe/London") - 86400
ZeroTime := time.Unix(ZeroTimestamp, 0).In(time.UTC)
StartDate := ZeroTime.AddDate(0, 0, -14).Format("2006-01-02")
EndDate := ZeroTime.Format("2006-01-02")
StartDate, _ := GetDateStr("Europe/London", -14)
EndDate, _ := GetDateStr("Europe/London", -1)
err = Db.Select(&Retain, "SELECT `Date`, `Register`, `SecondRemain`, `ThirdRemain`, `SeventhRemain`, `ThirtiethRemain`, `Recharge`, `Login`, `Ext` FROM remain where `Date` >= ? and `Date` <= ? order by `Date` desc", StartDate, EndDate)
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
@ -236,8 +234,9 @@ func GetOperation(AppId int) (*Type.Operation, error) {
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
}
InactiveTime, _ := GetZeroTimestamp("Europe/London", -8)
var InactiveUsers int
err = Db.Get(&InactiveUsers, "SELECT count(distinct Uid) as count from (SELECT Uid, MAX(Timestamp) as LastLogin FROM log_login WHERE Event = 'Login_log' GROUP BY Uid) as lt where lastlogin < ?", ZeroTimestamp-7*86400)
err = Db.Get(&InactiveUsers, "SELECT count(distinct Uid) as count from (SELECT Uid, MAX(Timestamp) as LastLogin FROM log_login WHERE Event = 'Login_log' GROUP BY Uid) as lt where lastlogin < ?", InactiveTime)
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
}
@ -292,7 +291,7 @@ func GetDailyHeat(AppId int) (*Type.HeatData, error) {
if LogDb == nil {
return nil, fmt.Errorf("failed to get mysql database")
}
now := ZeroTimestampByTz(AppConfig.Tz)
now, _ := GetZeroTimestamp(AppConfig.Tz, 0)
value := make([]int, 0, 24)
value2 := make([]int, 0, 24)
key := make([]string, 0, 24)
@ -354,7 +353,7 @@ func GetLevelData(AppId int) (*Type.LevelData, error) {
Uid int `db:"Uid"`
}
InactiveUsers := []Users{}
ZeroTimestamp := ZeroTimestampByTz(AppConfig.Tz)
ZeroTimestamp, _ := GetZeroTimestamp(AppConfig.Tz, 0)
err = LogDb.Select(&InactiveUsers, "SELECT Uid from (SELECT Uid, MAX(Timestamp) as LastLogin FROM log_login WHERE Event = 'Login_log' GROUP BY Uid) as lt where lastlogin < ?", ZeroTimestamp-7*86400)
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
@ -445,7 +444,7 @@ func GetDecorateData(AppId int) (*Type.DecorateData, error) {
Uid int `db:"Uid"`
}
InactiveUsers := []Users{}
ZeroTimestamp := ZeroTimestampByTz(AppConfig.Tz)
ZeroTimestamp, _ := GetZeroTimestamp(AppConfig.Tz, 0)
err = LogDb.Select(&InactiveUsers, "SELECT Uid from (SELECT Uid, MAX(Timestamp) as LastLogin FROM log_login WHERE Event = 'Login_log' GROUP BY Uid) as lt where lastlogin < ?", ZeroTimestamp-7*86400)
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
@ -516,3 +515,97 @@ func GetDecorateData(AppId int) (*Type.DecorateData, error) {
Value2: value2,
}, nil
}
func GetOrderData(AppId int) (interface{}, error) {
AppConfig, err := GetAppConfig(AppId)
if err != nil {
return nil, err
}
LogDb := MPool.GetTopicDB(AppConfig.Topic)
defer LogDb.Close()
if LogDb == nil {
return nil, fmt.Errorf("failed to get mysql database")
}
type data struct {
Param string `db:"param"`
Uid int `db:"Uid"`
Timestamp int64 `db:"Timestamp"`
}
dataList := []data{}
err = LogDb.Select(&dataList, "SELECT `param`, `Uid`, `Timestamp` FROM log_event WHERE `Event` = 'order_finish'")
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
}
type Users struct {
Uid int `db:"Uid"`
}
InactiveUsers := []Users{}
ZeroTimestamp, _ := GetZeroTimestamp(AppConfig.Tz, 0)
err = LogDb.Select(&InactiveUsers, "SELECT Uid from (SELECT Uid, MAX(Timestamp) as LastLogin FROM log_login WHERE Event = 'Login_log' GROUP BY Uid) as lt where lastlogin < ?", ZeroTimestamp-7*86400)
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
}
d0 := make([]int, 0)
for _, v := range InactiveUsers {
d0 = append(d0, v.Uid)
}
d1 := make(map[int]data)
for _, v := range dataList {
if _, ok := d1[v.Uid]; !ok {
d1[v.Uid] = v
}
if v.Timestamp > d1[v.Uid].Timestamp {
d1[v.Uid] = v
}
}
type data2 struct {
Num int
Churn int
}
d2 := make(map[int]*data2)
for _, v := range d1 {
var jd struct {
Step int `json:"order_id"`
}
err := json.Unmarshal([]byte(v.Param), &jd)
if err != nil {
continue
}
if _, ok := d2[jd.Step]; !ok {
d2[jd.Step] = &data2{
Num: 0,
Churn: 0,
}
}
d2[jd.Step].Num++
if InArray(v.Uid, d0) {
d2[jd.Step].Churn++
}
}
key := make([]int, 0, len(d2))
for k := range d2 {
key = append(key, k)
}
sort.Slice(key, func(i, j int) bool {
return i < j
})
value := make([]int, 0, len(d2))
value2 := make([]int, 0, len(d2))
for _, v := range key {
value = append(value, d2[v].Num)
value2 = append(value2, d2[v].Churn)
}
for i := 1; i < 30; i++ {
_, ok := d2[i]
if !ok {
continue
}
fmt.Printf("key:%d, value:%.2f%%, value2:%d\n", i+1, float64(d2[i].Num)/13, d2[i].Churn)
}
return map[string]interface{}{
"Key": key,
"Value": value,
"Value2": value2,
}, nil
}

View File

@ -1,6 +1,9 @@
package util
import "time"
import (
"fmt"
"time"
)
func ZeroTimestamp() int64 {
now := time.Now()
@ -8,14 +11,96 @@ func ZeroTimestamp() int64 {
return midnight.Unix()
}
func ZeroTimestampByTz(tz string) int64 {
loc, _ := time.LoadLocation(tz)
now := time.Now().In(loc)
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
return midnight.Unix()
}
func GetHour(ts int64, tz string) int {
loc, _ := time.LoadLocation(tz)
return time.Unix(ts, 0).In(loc).Hour()
}
// 将ISO 8601格式的时间字符串转换为指定时区的时间戳
// timeStr: ISO 8601格式的时间字符串"2025-03-17T16:00:00.000Z"
// timezone: 时区字符串,如:"Asia/Shanghai""America/New_York"留空则使用UTC
func ParseTimeToTimestamp(timeStr string, timezone string) (int64, error) {
// 先解析为UTC时间
t, err := time.Parse(time.RFC3339, timeStr)
if err != nil {
return 0, err
}
// 如果没有指定时区直接返回UTC时间戳
if timezone == "" {
return t.Unix(), nil
}
// 加载指定时区
loc, err := time.LoadLocation(timezone)
if err != nil {
return 0, fmt.Errorf("invalid timezone: %v", err)
}
// 将时间转换到指定时区
t = t.In(loc)
return t.Unix(), nil
}
// GetZeroTimestamp 获取指定时区的当天0点时间戳
// timezone: 时区字符串,如:"Asia/Shanghai""America/New_York"留空则使用UTC
// offset: 日期偏移0表示今天-1表示昨天1表示明天以此类推
func GetZeroTimestamp(timezone string, offset int) (int64, error) {
var loc *time.Location
var err error
// 如果没有指定时区使用UTC
if timezone == "" {
loc = time.UTC
} else {
// 加载指定时区
loc, err = time.LoadLocation(timezone)
if err != nil {
return 0, fmt.Errorf("invalid timezone: %v", err)
}
}
// 获取当前时间并转换到指定时区
now := time.Now().In(loc)
// 应用日期偏移
if offset != 0 {
now = now.AddDate(0, 0, offset)
}
// 获取当天0点时间
zero := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
return zero.Unix(), nil
}
// GetDateStr 获取指定时区的日期字符串
// timezone: 时区字符串,如:"Asia/Shanghai""America/New_York"留空则使用UTC
// offset: 日期偏移0表示今天-1表示昨天1表示明天以此类推
// 返回格式:"2006-01-02"
func GetDateStr(timezone string, offset int) (string, error) {
var loc *time.Location
var err error
// 如果没有指定时区使用UTC
if timezone == "" {
loc = time.UTC
} else {
// 加载指定时区
loc, err = time.LoadLocation(timezone)
if err != nil {
return "", fmt.Errorf("invalid timezone: %v", err)
}
}
// 获取当前时间并转换到指定时区
now := time.Now().In(loc)
// 应用日期偏移
if offset != 0 {
now = now.AddDate(0, 0, offset)
}
// 格式化日期
return now.Format("2006-01-02"), nil
}