版本更新

This commit is contained in:
hahwu 2025-03-05 15:40:49 +08:00
parent 975c1990f6
commit bf5867c7b7
23 changed files with 733 additions and 308 deletions

View File

@ -1,3 +1,10 @@
生成一个方法
## 方法名
ToTmplStr
## 参数
string
## 逻辑
将字符串转换成一行的字符串格式,并再进行一次转义输出

42
Type/c.go Normal file
View File

@ -0,0 +1,42 @@
package Type
type CardMdData struct {
Date string
Register int
Recharge float64
RechargeUserNum int
ChurnRate float64
ARPU float64
YRegister int
YRecharge float64
YLogin int
PerOnlineTime float64
PerOrderNum float64
C1 string
C2 string
}
type MarkDown struct {
Markdown string
}
type RowData struct {
Date string
Login int
Register int
Recharge float64
SecondRemain string
ThirdRemain string
SeventhRemain string
}
type CardTable struct {
Rows string
}
type Card struct {
Title string
Subtitle string
Elements string
Tag1 string
}

View File

@ -28,24 +28,28 @@ type User struct {
}
type Operation struct {
Retain []*Retain
Register int
Recharge float64
ChurnRate float64
Date string
Retain []*Retain
Register int
Recharge float64
RechargeUserNum int
ChurnRate float64
ARPU float64
}
type Retain struct {
Date string `db:"Date"`
Register int `db:"Register"`
SecondRemain int `db:"SecondRemain"`
ThirdRemain int `db:"ThirdRemain"`
SeventhRemain int `db:"SeventhRemain"`
ThirtiethRemain int `db:"ThirtiethRemain"`
Recharge float64 `db:"Recharge"`
Login int `db:"Login"`
Ext string `db:"Ext"`
PerOnlineTime float64 // 每日在线时长
PerOrderNum float64 // 每日完成订单数
Date string `db:"Date"`
Register int `db:"Register"`
SecondRemain int `db:"SecondRemain"`
ThirdRemain int `db:"ThirdRemain"`
SeventhRemain int `db:"SeventhRemain"`
FourteenthRemain int `db:"FourteenthRemain"`
ThirtiethRemain int `db:"ThirtiethRemain"`
Recharge float64 `db:"Recharge"`
Login int `db:"Login"`
Ext string `db:"Ext"`
PerOnlineTime float64 // 每日在线时长
PerOrderNum float64 // 每日完成订单数
}
type ServerInfo struct {

View File

@ -4,3 +4,8 @@ const (
FONT_COLOR_UP = "red"
FONT_COLOR_DOWN = "green"
)
const (
FEISHU_INFO_TYPE = "text"
FEISHU_CART_TYPE = "interactive"
)

View File

@ -26,10 +26,11 @@ type MysqlConfig struct {
}
type SystemConfig struct {
NMap bool `yaml:"nmap"` // 是否开启端口扫描
FeishUrl string `yaml:"feishu_url"` // 飞书机器人url
NoticeUrl string `yaml:"notice_url"` // 通知url
OperationUrl string `yaml:"operation_url"` // 运营url
NMap bool `yaml:"nmap"` // 是否开启端口扫描
FeishUrl string `yaml:"feishu_url"` // 飞书机器人url
NoticeUrl string `yaml:"notice_url"` // 通知url
OperationUrl string `yaml:"operation_url"` // 运营url
OperationChatId string `yaml:"operation_chat_id"` // 运营群id
}
type Config struct {
@ -97,3 +98,7 @@ func GetOperationUrl() string {
func GetNMap() bool {
return config.System.NMap
}
func GetOperationChatId() string {
return config.System.OperationChatId
}

View File

@ -3,6 +3,7 @@ system:
feishu_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/70e24a79-b019-434a-b4d1-4592bbf7c311'
notice_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/64bad1f3-3a41-4dca-9037-399067ffb252'
operation_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/64bad1f3-3a41-4dca-9037-399067ffb252'
operation_chat_id: 'oc_f6e10a55f28f31e2a5677bdcb6aed599'
mysqls:
- host: '127.0.0.1'
name: 'merge_pet_test'

View File

@ -3,6 +3,7 @@ system:
feishu_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/70e24a79-b019-434a-b4d1-4592bbf7c311'
notice_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/48944500-477a-4647-a7e0-c56c43bee263'
operation_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/e3122bc9-99ca-46b4-9634-862d3c8cdc7e'
operation_chat_id: 'oc_967a93dcade6d55c3db434f23767a414'
mysqls:
- host: '127.0.0.1'
name: 'merge_pet_test'

View File

@ -2,14 +2,11 @@ package controller
import (
"backend/Type"
"backend/common"
"backend/feishu"
"backend/model"
"backend/msg"
"backend/util"
"encoding/json"
"fmt"
"log"
"strconv"
"sync"
"time"
@ -18,68 +15,12 @@ import (
func FeishuSendInfo(c *gin.Context) {
// TODO
AppConfig, err := util.GetAppConfig(3)
Result, err := util.GetOperation(3)
if err != nil {
log.Printf("failed to get app config: %v", err)
log.Printf("failed to get operation: %v", err)
return
}
Db := util.MPool.GetTopicDB(AppConfig.Topic)
defer Db.Close()
Retain := []*Type.Retain{}
ZeroTimestamp := util.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")
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 {
log.Printf("GetOperation Select error: %v", err)
return
}
var Register int
err = Db.Get(&Register, "SELECT count(`Uid`) as count FROM log_login WHERE Event = 'register'")
if err != nil {
log.Printf("GetOperation Select error: %v", err)
return
}
var Recharge float64
err = Db.Get(&Recharge, "SELECT IFNULL(SUM(Price), 0) as sum FROM log_order")
if err != nil {
log.Printf("GetOperation Select error: %v", err)
return
}
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)
if err != nil {
log.Printf("GetOperation Select error: %v", err)
return
}
type ExtStruct struct {
PerOnlineTime string `json:"PerOnlineTime"`
PerOrderNum string `json:"PerOrderNum"`
}
for _, v := range Retain {
if v.Ext == "" {
continue
}
var d ExtStruct
err := json.Unmarshal([]byte(v.Ext), &d)
if err != nil {
fmt.Printf("err :%s", err.Error())
continue
}
value, err := strconv.ParseFloat(d.PerOnlineTime, 64)
if err == nil {
v.PerOnlineTime = value
}
value, err = strconv.ParseFloat(d.PerOrderNum, 64)
if err == nil {
v.PerOrderNum = value
}
}
ChurnRate := 100 * float64(InactiveUsers) / float64(Register)
Result := &Type.Operation{Retain: Retain, Register: Register, Recharge: Recharge, ChurnRate: ChurnRate}
err = common.SendOperationMsg(Result)
err = feishu.SendOperationMsg(Result)
if err != nil {
log.Printf("failed to send operation message: %v", err)
}
@ -87,45 +28,12 @@ func FeishuSendInfo(c *gin.Context) {
func FeishuSendWeekInfo(c *gin.Context) {
// TODO
AppConfig, err := util.GetAppConfig(3)
Result, err := util.GetOperation(3)
if err != nil {
log.Printf("failed to get app config: %v", err)
log.Printf("failed to get operation: %v", err)
return
}
Db := util.MPool.GetTopicDB(AppConfig.Topic)
defer Db.Close()
Retain := []*Type.Retain{}
ZeroTimestamp := util.ZeroTimestampByTz("Europe/London") - 86400
ZeroTime := time.Unix(ZeroTimestamp, 0).In(time.UTC)
StartDate := ZeroTime.AddDate(0, 0, -30).Format("2006-01-02")
EndDate := ZeroTime.Format("2006-01-02")
err = Db.Select(&Retain, "SELECT `Date`, `Register`, `SecondRemain`, `ThirdRemain`, `SeventhRemain`, `ThirtiethRemain`, `Recharge`, `Login` FROM remain where `Date` >= ? and `Date` <= ? order by `Date` desc", StartDate, EndDate)
if err != nil {
log.Printf("GetOperation Select error: %v", err)
return
}
var Register int
err = Db.Get(&Register, "SELECT count(`Uid`) as count FROM log_login WHERE Event = 'register'")
if err != nil {
log.Printf("GetOperation Select error: %v", err)
return
}
var Recharge float64
err = Db.Get(&Recharge, "SELECT IFNULL(SUM(Price), 0) as sum FROM log_order")
if err != nil {
log.Printf("GetOperation Select error: %v", err)
return
}
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)
if err != nil {
log.Printf("GetOperation Select error: %v", err)
return
}
ChurnRate := 100 * float64(InactiveUsers) / float64(Register)
Result := &Type.Operation{Retain: Retain, Register: Register, Recharge: Recharge, ChurnRate: ChurnRate}
err = common.SendOperationMsg(Result)
err = feishu.SendOperationMsg(Result)
if err != nil {
log.Printf("failed to send operation message: %v", err)
}
@ -144,7 +52,7 @@ func FeishuUpdateApp(c *gin.Context) {
}
if r.Step == 0 {
err := common.SendUpdateCard()
err := feishu.SendUpdateCard()
if err != nil {
log.Printf("failed to send update card: %v", err)
}
@ -219,7 +127,7 @@ func FeishuServerInfo(c *gin.Context) {
}(v)
}
w.Wait()
err = common.SendServerInfoCard(result)
err = feishu.SendServerInfoCard(result)
if err != nil {
log.Println(err)
}

View File

@ -3,6 +3,7 @@ package controller
import (
"backend/Type"
"backend/common"
"backend/feishu"
"backend/model"
"backend/util"
"fmt"
@ -119,6 +120,6 @@ func AppPortNmap() {
func AppPortNmap_(App *Type.AppStruct, server *model.ServerInfo) {
err := model.PortMap(App.WsHost, fmt.Sprintf("%d", server.ServerId+App.WsPort))
if err != nil {
common.SendNoticeMsg(App.AppName, server.ServerName, err.Error())
feishu.SendNoticeMsg(App.AppName, server.ServerName, err.Error())
}
}

View File

@ -3,7 +3,6 @@ package client
import (
"backend/feishu/data"
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
@ -26,16 +25,15 @@ func GetClient() *client {
return C
}
func (c *client) SendMsg(open_id, content string) error {
func (c *client) SendMsg(open_id, msg_type, content string) error {
// 创建 Client
b, _ := json.Marshal(map[string]string{"text": content})
uuidVal := uuid.New().String()
req := larkim.NewCreateMessageReqBuilder().
ReceiveIdType(`open_id`).
Body(larkim.NewCreateMessageReqBodyBuilder().
ReceiveId(open_id).
MsgType(`text`).
Content(string(b)).
MsgType(msg_type).
Content(content).
Uuid(uuidVal).
Build()).
Build()
@ -56,16 +54,15 @@ func (c *client) SendMsg(open_id, content string) error {
return nil
}
func (c *client) SendGroupMsg(chat_id, content string) error {
func (c *client) SendGroupMsg(chat_id, msg_type, content string) error {
// 创建 Client
b, _ := json.Marshal(map[string]string{"text": content})
uuidVal := uuid.New().String()
req := larkim.NewCreateMessageReqBuilder().
ReceiveIdType(`chat_id`).
Body(larkim.NewCreateMessageReqBodyBuilder().
ReceiveId(chat_id).
MsgType(`text`).
Content(string(b)).
MsgType(msg_type).
Content(content).
Uuid(uuidVal).
Build()).
Build()

View File

@ -1,11 +1,15 @@
package common
package feishu
import (
"backend/Type"
"backend/common"
"backend/feishu/client"
"backend/util"
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
@ -23,7 +27,7 @@ func SendFeishuMsg(msg string) error {
}
// 创建HTTP请求
req, err := http.NewRequest("POST", GetFeishuUrl(), bytes.NewBuffer(payloadBytes))
req, err := http.NewRequest("POST", common.GetFeishuUrl(), bytes.NewBuffer(payloadBytes))
if err != nil {
return err
}
@ -76,7 +80,7 @@ func SendNoticeMsg(AppName, ServerName, notice string) error {
}
// 创建HTTP请求
req, err := http.NewRequest("POST", GetNoticeUrl(), bytes.NewBuffer(payloadBytes))
req, err := http.NewRequest("POST", common.GetNoticeUrl(), bytes.NewBuffer(payloadBytes))
if err != nil {
return err
}
@ -99,110 +103,64 @@ func SendNoticeMsg(AppName, ServerName, notice string) error {
}
// table_raw_array_1
func SendOperationMsg(data *Type.Operation) error {
// retainStr := ""
// for _, v := range data.Retain {
// retainStr += fmt.Sprintf("| %s | %d | %d | %.2f | %.2f%% | %.2f%% | %.2f%% | %.2f%% |\n", v.Date, v.Register, v.Login, v.Recharge, 100*float64(v.SecondRemain)/float64(v.Register), 100*float64(v.ThirdRemain)/float64(v.Register), 100*float64(v.SeventhRemain)/float64(v.Register), 100*float64(v.ThirtiethRemain)/float64(v.Register))
// }
RetainData := make([]map[string]interface{}, 0)
for _, v := range data.Retain {
var retain2, retain3, retain7 string
if v.Register == 0 {
retain2 = "0.00%"
retain3 = "0.00%"
retain7 = "0.00%"
} else {
retain2 = fmt.Sprintf("%.2f%%", 100*float64(v.SecondRemain)/float64(v.Register))
retain3 = fmt.Sprintf("%.2f%%", 100*float64(v.ThirdRemain)/float64(v.Register))
retain7 = fmt.Sprintf("%.2f%%", 100*float64(v.SeventhRemain)/float64(v.Register))
func SendOperationMsg(Operation *Type.Operation) error {
card_md_data := Type.CardMdData{
Date: Operation.Date,
Register: Operation.Register,
Recharge: Operation.Recharge,
RechargeUserNum: Operation.RechargeUserNum,
ChurnRate: Operation.ChurnRate,
ARPU: Operation.ARPU,
YRegister: Operation.Retain[0].Register,
YRecharge: Operation.Retain[0].Recharge,
YLogin: Operation.Retain[0].Login,
PerOnlineTime: Operation.Retain[0].PerOnlineTime,
PerOrderNum: Operation.Retain[0].PerOrderNum,
C1: util.Ternary(Operation.Retain[0].PerOnlineTime > Operation.Retain[1].PerOnlineTime, common.FONT_COLOR_UP, common.FONT_COLOR_DOWN).(string),
C2: util.Ternary(Operation.Retain[0].PerOrderNum > Operation.Retain[1].PerOrderNum, common.FONT_COLOR_UP, common.FONT_COLOR_DOWN).(string),
}
data1 := Type.MarkDown{
Markdown: util.ToTmplStr(util.ParseTmpl("./template/card_md_1.tmpl", card_md_data)),
}
MarkDown := util.ParseTmpl("./template/card_md.tmpl", data1)
var table_tmpl string
var table_tmpl_rows []string
for _, v := range Operation.Retain {
row_data := Type.RowData{
Date: v.Date,
Login: v.Login,
Register: v.Register,
Recharge: v.Recharge,
SecondRemain: fmt.Sprintf("%.2f%%", 100*util.FloatDiv(v.SecondRemain, v.Register, 5)),
ThirdRemain: fmt.Sprintf("%.2f%%", 100*util.FloatDiv(v.ThirdRemain, v.Register, 5)),
SeventhRemain: fmt.Sprintf("%.2f%%", 100*util.FloatDiv(v.SeventhRemain, v.Register, 5)),
}
RetainData = append(RetainData, map[string]interface{}{
"Date": v.Date,
"Reg": v.Register,
"Login": v.Login,
"Pay": v.Recharge,
"Retain2": retain2,
"Retain3": retain3,
"Retain7": retain7,
// "Retain14": fmt.Sprintf("%.2f%%", 100*float64(v.ThirtiethRemain)/float64(v.Register)),
})
}
var c1, c2 string
if data.Retain[0].PerOnlineTime > data.Retain[1].PerOnlineTime {
c1 = FONT_COLOR_UP
} else {
c1 = FONT_COLOR_DOWN
}
if data.Retain[0].PerOrderNum > data.Retain[1].PerOrderNum {
c2 = FONT_COLOR_UP
} else {
c2 = FONT_COLOR_DOWN
}
str := fmt.Sprintf(`
# 日期 %s
-----------------------------
## 总体数据
- **注册**%d
- **充值**%.2f
- **流失率**%.2f%%
> 超过7天未登录视为流失
## 昨日数据
- **注册**%d
- **充值**%.2f
- **登录**%d
- **平均在线时长**<font color="%s">%.2f</font>分钟
- **平均完成订单数**<font color="%s">%.2f</font>
----------------------
## 留存数据
`, time.Now().In(time.UTC).Format("2006-01-02"),
data.Register, data.Recharge, data.ChurnRate, data.Retain[0].Register, data.Retain[0].Recharge, data.Retain[0].Login,
c1, data.Retain[0].PerOnlineTime, c2, data.Retain[0].PerOrderNum)
// 创建请求体
payload := map[string]interface{}{
"msg_type": "interactive",
"card": map[string]interface{}{
"type": "template",
"data": map[string]interface{}{
"template_id": "AAqBcfmUwQya1",
"template_version_name": "1.0.8",
"template_variable": map[string]interface{}{
"msg": str,
"appName": "merge_pet_london",
"table_raw_array_1": RetainData,
},
},
},
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return err
table_tmpl_rows = append(table_tmpl_rows, util.ParseTmpl("./template/card_table_row.tmpl", row_data))
}
// 创建HTTP请求
req, err := http.NewRequest("POST", GetOperationUrl(), bytes.NewBuffer(payloadBytes))
if err != nil {
return err
table_tmpl_data := Type.CardTable{
Rows: strings.Join(table_tmpl_rows, ","),
}
req.Header.Set("Content-Type", "application/json")
table_tmpl = util.ParseTmpl("./template/card_table.tmpl", table_tmpl_data)
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
elements := []string{
MarkDown,
table_tmpl,
}
defer resp.Body.Close()
// 检查响应状态码
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to send message, status code: %d", resp.StatusCode)
elementsStr := strings.Join(elements, ",")
data := Type.Card{
Title: "运营日报",
Elements: elementsStr,
Tag1: "UK",
}
s := util.ParseTmpl("./template/card.tmpl", data)
if !json.Valid([]byte(s)) {
return fmt.Errorf("invalid JSON format")
}
c := client.GetClient()
c.SendGroupMsg(common.GetOperationChatId(), common.FEISHU_CART_TYPE, s)
return nil
}
@ -224,7 +182,7 @@ func SendUpdateCard() error {
}
// 创建HTTP请求
req, err := http.NewRequest("POST", GetOperationUrl(), bytes.NewBuffer(payloadBytes))
req, err := http.NewRequest("POST", common.GetOperationUrl(), bytes.NewBuffer(payloadBytes))
if err != nil {
return err
}
@ -279,7 +237,7 @@ func SendServerInfoCard(data *Type.ServerInfo) error {
}
// 创建HTTP请求
req, err := http.NewRequest("POST", GetFeishuUrl(), bytes.NewBuffer(payloadBytes))
req, err := http.NewRequest("POST", common.GetFeishuUrl(), bytes.NewBuffer(payloadBytes))
if err != nil {
return err
}
@ -364,7 +322,7 @@ func SendWeekOperationMsg(data *Type.Operation) error {
}
// 创建HTTP请求
req, err := http.NewRequest("POST", GetOperationUrl(), bytes.NewBuffer(payloadBytes))
req, err := http.NewRequest("POST", common.GetOperationUrl(), bytes.NewBuffer(payloadBytes))
if err != nil {
return err
}

View File

@ -20,7 +20,7 @@ func Server() {
OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
log.Printf("[ OnP2MessageReceiveV1 access ], data: %s\n", larkcore.Prettify(event))
chat_id := *event.Event.Message.ChatId
client.C.SendGroupMsg(chat_id, "hello")
client.C.SendGroupMsg(chat_id, "text", "hello")
return nil
})
eventHandler.OnP2BotMenuV6(func(ctx context.Context, event *larkapplication.P2BotMenuV6) error {

239
log/t.json Normal file
View File

@ -0,0 +1,239 @@
{
"schema": "2.0",
"header": {
"title": {
"tag": "plain_text",
"content": "运营周报"
},
"subtitle": {
"tag": "plain_text",
"content": ""
},
"text_tag_list": [
{
"tag": "text_tag",
"element_id": "custom_id_psd1",
"text": {
"tag": "plain_text",
"content": "UK"
},
"color": "orange"
}
],
"template": "green",
"icon": {
"tag": "standard_icon",
"token": "calendar_colorful",
"color": "green"
},
"padding": "12px 8px 12px 8px"
},
"body":{
"elements": [
{
"tag": "markdown",
"element_id": "custom_md_id_s12",
"margin": "0px 0px 0px 0px",
"content": "# 日期 2025-03-05 \n-----------------------------\n## 总体数据\n\n- **注册**1312\n- **充值**68.87\n- **充值人数**35\n- **流失率**77.74%\n- **ARPU**0.052\n\n> 超过7天未登录视为流失 \n\n---------------------------\n## 昨日数据\n\n- **注册**4\n- **充值**0\n- **登录**64\n- **平均在线时长**<font color="red">41.08%</font>\n- **平均完成订单数**<font color="red">8.27</font>\n\n## 留存数据",
"text_size": "normal",
"text_align": "left"
},{
"tag": "table",
"element_id": "custom_table_id_x12",
"margin": "0px 0px 0px 0px",
"page_size": 10,
"row_height": "low",
"row_max_height": "50px",
"freeze_first_column": true,
"header_style": {
"text_align": "center",
"text_size": "normal",
"background_style": "grey",
"text_color": "grey",
"bold": true,
"lines": 1
},
"columns": [
{
"name": "customer_date",
"display_name": "Date",
"width": "auto",
"data_type": "text",
"horizontal_align": "center" ,
"width": "105px"
},
{
"name": "customer_reg",
"display_name": "Reg",
"data_type": "number",
"horizontal_align": "center",
"width": "80px"
},
{
"name": "customer_login",
"display_name": "Login",
"data_type": "number",
"horizontal_align": "center",
"width": "80px"
},
{
"name": "customer_pay",
"display_name": "Pay",
"data_type": "number",
"horizontal_align": "center",
"format": {
"symbol": "$",
"precision": 2,
"separator": true
},
"width": "80px"
},
{
"name": "customer_retain2",
"display_name": "Retain2",
"data_type": "text",
"horizontal_align": "center",
"width": "80px"
},
{
"name": "customer_retain3",
"display_name": "Retain3",
"data_type": "text",
"horizontal_align": "center",
"width": "80px"
},
{
"name": "customer_retain7",
"display_name": "Retain7",
"data_type": "text",
"horizontal_align": "center",
"width": "80px"
}
],
"rows": [
{
"customer_date": "2025-03-04",
"customer_reg": 4,
"customer_login": 64,
"customer_pay": 0,
"customer_retain2": "0.00%",
"customer_retain3": "0.00%",
"customer_retain7": "0.00%"
},{
"customer_date": "2025-03-03",
"customer_reg": 0,
"customer_login": 69,
"customer_pay": 0,
"customer_retain2": "0.00%",
"customer_retain3": "0.00%",
"customer_retain7": "0.00%"
},{
"customer_date": "2025-03-02",
"customer_reg": 1,
"customer_login": 61,
"customer_pay": 0.49,
"customer_retain2": "0.00%",
"customer_retain3": "0.00%",
"customer_retain7": "0.00%"
},{
"customer_date": "2025-03-01",
"customer_reg": 10,
"customer_login": 76,
"customer_pay": 2.98,
"customer_retain2": "10.00%",
"customer_retain3": "0.00%",
"customer_retain7": "0.00%"
},{
"customer_date": "2025-02-28",
"customer_reg": 12,
"customer_login": 59,
"customer_pay": 0,
"customer_retain2": "33.33%",
"customer_retain3": "25.00%",
"customer_retain7": "0.00%"
},{
"customer_date": "2025-02-27",
"customer_reg": 29,
"customer_login": 123,
"customer_pay": 3.99,
"customer_retain2": "3.45%",
"customer_retain3": "3.45%",
"customer_retain7": "0.00%"
},{
"customer_date": "2025-02-26",
"customer_reg": 28,
"customer_login": 112,
"customer_pay": 3.47,
"customer_retain2": "25.00%",
"customer_retain3": "14.29%",
"customer_retain7": "7.14%"
},{
"customer_date": "2025-02-25",
"customer_reg": 38,
"customer_login": 136,
"customer_pay": 0,
"customer_retain2": "21.05%",
"customer_retain3": "15.79%",
"customer_retain7": "7.90%"
},{
"customer_date": "2025-02-24",
"customer_reg": 23,
"customer_login": 111,
"customer_pay": 1.47,
"customer_retain2": "34.78%",
"customer_retain3": "21.74%",
"customer_retain7": "17.39%"
},{
"customer_date": "2025-02-23",
"customer_reg": 35,
"customer_login": 125,
"customer_pay": 0.99,
"customer_retain2": "28.57%",
"customer_retain3": "25.71%",
"customer_retain7": "20.00%"
},{
"customer_date": "2025-02-22",
"customer_reg": 39,
"customer_login": 126,
"customer_pay": 1.47,
"customer_retain2": "30.77%",
"customer_retain3": "17.95%",
"customer_retain7": "2.56%"
},{
"customer_date": "2025-02-21",
"customer_reg": 62,
"customer_login": 163,
"customer_pay": 4.45,
"customer_retain2": "24.19%",
"customer_retain3": "9.68%",
"customer_retain7": "9.68%"
},{
"customer_date": "2025-02-20",
"customer_reg": 50,
"customer_login": 133,
"customer_pay": 0,
"customer_retain2": "36.00%",
"customer_retain3": "16.00%",
"customer_retain7": "16.00%"
},{
"customer_date": "2025-02-19",
"customer_reg": 86,
"customer_login": 167,
"customer_pay": 7.47,
"customer_retain2": "19.77%",
"customer_retain3": "13.95%",
"customer_retain7": "8.14%"
},{
"customer_date": "2025-02-18",
"customer_reg": 74,
"customer_login": 149,
"customer_pay": 11.9,
"customer_retain2": "35.13%",
"customer_retain3": "25.68%",
"customer_retain7": "17.57%"
}
]
}
]
}
}

View File

@ -2,6 +2,7 @@ package model
import (
"backend/common"
"backend/feishu"
"backend/msg"
util "backend/util"
"fmt"
@ -134,7 +135,7 @@ func (s *Server) UpdateApp() (string, error) {
DB := util.MPool.GetGameDB()
defer DB.Close()
DB.Exec("UPDATE app SET `Update` = ? WHERE `AppId` = ?", util.Now(), s.AppId)
common.SendFeishuMsg(fmt.Sprintf("AppName: %s, 执行文件更新完成", AppConfig.AppName))
feishu.SendFeishuMsg(fmt.Sprintf("AppName: %s, 执行文件更新完成", AppConfig.AppName))
return output, nil
}
@ -164,7 +165,7 @@ func (s *Server) UpdateAppFeishu() (string, error) {
DB := util.MPool.GetGameDB()
defer DB.Close()
DB.Exec("UPDATE app SET `Update` = ? WHERE `AppId` = ?", util.Now(), s.AppId)
common.SendFeishuMsg(fmt.Sprintf("AppName: %s, 执行文件更新完成", AppConfig.AppName))
feishu.SendFeishuMsg(fmt.Sprintf("AppName: %s, 执行文件更新完成", AppConfig.AppName))
return output, nil
}
@ -187,7 +188,7 @@ func (s *Server) RestartServer() (string, error) {
if err != nil {
return "", err
}
common.SendFeishuMsg(fmt.Sprintf("AppName: %s, ServerName: %s, 重启完成", AppConfig.AppName, s.ServerName))
feishu.SendFeishuMsg(fmt.Sprintf("AppName: %s, ServerName: %s, 重启完成", AppConfig.AppName, s.ServerName))
return output, nil
}
@ -207,7 +208,7 @@ func (s *Server) ReloadServer() (string, error) {
if err != nil {
log.Printf("failed to send admin message: %v", err)
}
common.SendFeishuMsg(fmt.Sprintf("AppName: %s, ServerName: %s, 配置重载完成", AppConfig.AppName, s.ServerName))
feishu.SendFeishuMsg(fmt.Sprintf("AppName: %s, ServerName: %s, 配置重载完成", AppConfig.AppName, s.ServerName))
return "success", nil
}

Binary file not shown.

36
template/card.tmpl Normal file
View File

@ -0,0 +1,36 @@
{
"schema": "2.0",
"header": {
"title": {
"tag": "plain_text",
"content": "{{.Title}}"
},
"subtitle": {
"tag": "plain_text",
"content": "{{.Subtitle}}"
},
"text_tag_list": [
{
"tag": "text_tag",
"element_id": "custom_id_psd1",
"text": {
"tag": "plain_text",
"content": "{{.Tag1}}"
},
"color": "orange"
}
],
"template": "green",
"icon": {
"tag": "standard_icon",
"token": "calendar_colorful",
"color": "green"
},
"padding": "12px 8px 12px 8px"
},
"body":{
"elements": [
{{.Elements}}
]
}
}

8
template/card_md.tmpl Normal file
View File

@ -0,0 +1,8 @@
{
"tag": "markdown",
"element_id": "custom_md_id_s12",
"margin": "0px 0px 0px 0px",
"content": "{{.Markdown}}",
"text_size": "normal",
"text_align": "left"
}

22
template/card_md_1.tmpl Normal file
View File

@ -0,0 +1,22 @@
# 日期 {{.Date}}
-----------------------------
## 总体数据
- **注册**{{.Register}}
- **充值**{{.Recharge}}
- **充值人数**{{.RechargeUserNum}}
- **流失率**{{.ChurnRate}}%
- **ARPU**{{.ARPU}}
> 超过7天未登录视为流失
---------------------------
## 昨日数据
- **注册**{{.YRegister}}
- **充值**{{.YRecharge}}
- **登录**{{.YLogin}}
- **平均在线时长**<font color="{{.C1}}">{{.PerOnlineTime}}%</font>
- **平均完成订单数**<font color="{{.C2}}">{{.PerOrderNum}}</font>
## 留存数据

77
template/card_table.tmpl Normal file
View File

@ -0,0 +1,77 @@
{
"tag": "table",
"element_id": "custom_table_id_x12",
"margin": "0px 0px 0px 0px",
"page_size": 10,
"row_height": "low",
"row_max_height": "50px",
"freeze_first_column": true,
"header_style": {
"text_align": "center",
"text_size": "normal",
"background_style": "grey",
"text_color": "grey",
"bold": true,
"lines": 1
},
"columns": [
{
"name": "customer_date",
"display_name": "Date",
"width": "auto",
"data_type": "text",
"horizontal_align": "center" ,
"width": "105px"
},
{
"name": "customer_reg",
"display_name": "Reg",
"data_type": "number",
"horizontal_align": "center",
"width": "80px"
},
{
"name": "customer_login",
"display_name": "Login",
"data_type": "number",
"horizontal_align": "center",
"width": "80px"
},
{
"name": "customer_pay",
"display_name": "Pay",
"data_type": "number",
"horizontal_align": "center",
"format": {
"symbol": "$",
"precision": 2,
"separator": true
},
"width": "80px"
},
{
"name": "customer_retain2",
"display_name": "Retain2",
"data_type": "text",
"horizontal_align": "center",
"width": "80px"
},
{
"name": "customer_retain3",
"display_name": "Retain3",
"data_type": "text",
"horizontal_align": "center",
"width": "80px"
},
{
"name": "customer_retain7",
"display_name": "Retain7",
"data_type": "text",
"horizontal_align": "center",
"width": "80px"
}
],
"rows": [
{{.Rows}}
]
}

View File

@ -0,0 +1,9 @@
{
"customer_date": "{{.Date}}",
"customer_reg": {{.Register}},
"customer_login": {{.Login}},
"customer_pay": {{.Recharge}},
"customer_retain2": "{{.SecondRemain}}",
"customer_retain3": "{{.ThirdRemain}}",
"customer_retain7": "{{.SeventhRemain}}"
}

View File

@ -1,89 +1,40 @@
package main
import (
"backend/Type"
"backend/common"
"backend/controller"
"backend/model"
"backend/msg"
"backend/util"
"fmt"
"log"
"sync"
"testing"
"time"
)
func TestXxx1(t *testing.T) {
// controller.FeishuSendInfo(nil)
str := `
## 日期2021-09-01
## 代办事项
- [ ] 任务1
- [ ] 任务2
- [ ] 任务3
`
str = util.ToTmplStr(str)
fmt.Println(str)
}
func TestXxx(t *testing.T) {
controller.FeishuSendInfo(nil)
// AppConfig, err := util.GetAppConfig(3)
// if err != nil {
// log.Printf("failed to get app config: %v", err)
// return
// }
// Db := util.MPool.GetTopicDB(AppConfig.Topic)
// defer Db.Close()
// ZeroTimestamp := util.ZeroTimestampByTz("Europe/London") - 86400
// p := model.GetPerOnlineTime(Db, ZeroTimestamp)
// fmt.Println(p)
}
func TestFeishu(t *testing.T) {
Db := util.MPool.GetGameDB()
var server []*model.ServerInfo
defer Db.Close()
err := Db.Select(&server, "SELECT `AppId`, `ServerId`, `ServerName`, `Status`, `CreateTime`, `OpenServerTime` FROM server where AppId = ?", 1)
if err != nil {
return
}
var w sync.WaitGroup
result := &Type.ServerInfo{}
result.ServerList = make([]*Type.Server, 0)
for _, v := range server {
ws, err := util.GetWebsocket(v.AppId, v.ServerId)
if err != nil {
v.Status = 0
continue
}
ws.SetReadDeadline(time.Now().Add(2 * time.Second))
w.Add(1)
go func(v *model.ServerInfo) {
defer w.Done()
req := &msg.ReqServerInfo{}
r, err := util.SendAdminMsg(ws, req)
rs := &Type.Server{}
rs.ServerName = v.ServerName
if err != nil {
log.Printf("failed to send admin message: %v", err)
rs.Status = "Inactive"
return
}
rs.Status = "Active"
if r["StartTime"] != nil {
rs.StartTime = time.Unix(int64(r["StartTime"].(float64)), 0).Format("2006-01-02 15:04:05")
}
if r["PlayerNum"] != nil {
rs.PlayerNum = int(r["PlayerNum"].(float64))
}
if r["TotalAlloc"] != nil {
rs.Mem = r["TotalAlloc"].(string)
}
v.Status = 1
result.ServerList = append(result.ServerList, rs)
if r["Sys"] != nil {
result.Mem = r["Sys"].(string)
}
if r["CPU"] != nil {
result.Cpu = r["CPU"].(string)
}
ws.Close()
}(v)
}
w.Wait()
err = common.SendServerInfoCard(result)
if err != nil {
log.Println(err)
data := struct {
Title string
Subtitle string
Elements string
}{
Title: "模拟测试",
Subtitle: "模拟测试副标题",
Elements: "",
}
s := util.ParseTmpl("./template/card.tmpl", data)
fmt.Println(s)
}
func TestEncrypt(t *testing.T) {

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log"
"strconv"
"time"
)
@ -205,3 +206,76 @@ func GetPerOnlineTime(db *Db, Zero int64) float64 {
}
return float64(sum/int64(len(d2))) / 60
}
func GetOperation(AppId int) (*Type.Operation, error) {
AppConfig, err := GetAppConfig(AppId)
if err != nil {
return nil, fmt.Errorf("failed to get app config: %v", err)
}
Db := MPool.GetTopicDB(AppConfig.Topic)
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")
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)
}
var Register int
err = Db.Get(&Register, "SELECT count(`Uid`) as count FROM log_login WHERE Event = 'register'")
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
}
var Recharge float64
err = Db.Get(&Recharge, "SELECT IFNULL(SUM(Price), 0) as sum FROM log_order")
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
}
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)
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
}
var RechargeUserNum int
err = Db.Get(&RechargeUserNum, "SELECT count(distinct Uid) as count FROM log_order")
if err != nil {
return nil, fmt.Errorf("failed to select data: %v", err)
}
type ExtStruct struct {
PerOnlineTime string `json:"PerOnlineTime"`
PerOrderNum string `json:"PerOrderNum"`
}
for _, v := range Retain {
if v.Ext == "" {
continue
}
var d ExtStruct
err := json.Unmarshal([]byte(v.Ext), &d)
if err != nil {
fmt.Printf("err :%s", err.Error())
continue
}
value, err := strconv.ParseFloat(d.PerOnlineTime, 64)
if err == nil {
v.PerOnlineTime = value
}
value, err = strconv.ParseFloat(d.PerOrderNum, 64)
if err == nil {
v.PerOrderNum = value
}
}
ChurnRate := 100 * float64(InactiveUsers) / float64(Register)
R := &Type.Operation{
Retain: Retain,
Register: Register,
Recharge: Decimal(Recharge, 2),
ChurnRate: Decimal(ChurnRate, 2),
RechargeUserNum: RechargeUserNum,
Date: time.Now().In(time.UTC).Format("2006-01-02"),
ARPU: FloatDiv(Recharge, Register, 3),
}
return R, nil
}

View File

@ -2,6 +2,7 @@ package util
import (
"backend/msg"
"bytes"
"crypto/aes"
"crypto/cipher"
crand "crypto/rand"
@ -9,8 +10,11 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"reflect"
"strconv"
"strings"
"text/template"
"time"
"golang.org/x/net/websocket"
@ -106,6 +110,49 @@ func FloatDecimals(f float64, n int) float64 {
return r
}
// 将两个参数转换成浮点数如果无法转换则为0实现两个浮点数的除法除数为0的话返回0
func FloatDiv(a, b interface{}, Decimal int) float64 {
af := toFloat64(a)
bf := toFloat64(b)
if bf == 0 {
return 0
}
result := af / bf
format := fmt.Sprintf("%%.%df", Decimal)
r, _ := strconv.ParseFloat(fmt.Sprintf(format, result), 64)
return r
}
func Decimal(f float64, n int) float64 {
format := fmt.Sprintf("%%.%df", n)
r, _ := strconv.ParseFloat(fmt.Sprintf(format, f), 64)
return r
}
// 辅助函数将interface{}转换为float64
func toFloat64(v interface{}) float64 {
switch val := v.(type) {
case int:
return float64(val)
case int32:
return float64(val)
case int64:
return float64(val)
case float32:
return float64(val)
case float64:
return val
case string:
r, err := strconv.ParseFloat(val, 64)
if err != nil {
return 0
}
return r
default:
return 0
}
}
const (
SECRET_KEY = ")VQbB(vpy=U(wcp)"
)
@ -153,3 +200,35 @@ func Decrypt(cipherText string) (string, error) {
return string(cipherTextBytes), nil
}
func ParseTmpl(file string, data interface{}) string {
t, err := template.ParseFiles(file)
if err != nil {
log.Fatalf("failed to parse template file: %v", err)
}
var buf bytes.Buffer
err = t.Execute(&buf, data)
if err != nil {
log.Fatalf("failed to execute template: %v", err)
}
s := buf.String()
return s
}
// 将字符串转换成一行的字符串格式,并再进行一次转义输出
func ToTmplStr(input string) string {
// 转义换行符
escapedStr := strings.ReplaceAll(input, "\r", "")
escapedStr = strings.ReplaceAll(escapedStr, "\n", "\\n")
escapedStr = strings.ReplaceAll(escapedStr, "\"", "\\\"")
// 转义输出
// return template.HTMLEscapeString(escapedStr)
return escapedStr
}
func Ternary(condition bool, trueVal, falseVal interface{}) interface{} {
if condition {
return trueVal
}
return falseVal
}