Browse Source

feat(GameShield): 添加获取在线SDK列表功能

- 新增 GameShieldRuleIdRequest 结构体用于处理规则ID请求
- 添加 SDKInfo 结构体表示SDK在线信息- 实现 GetGameShieldOnlineList 接口和服务方法
- 添加 FetchPageContent 方法以获取页面内容
- 实现 ParseSDKOnlineHTMLTable 方法解析SDK在线情况表格- 更新 HTTP路由以支持新功能
fusu 3 months ago
parent
commit
592e881879

+ 14 - 2
api/v1/GameShield.go

@@ -41,6 +41,18 @@ type KeyAndFieldResponse struct {
 	FieldId int
 	FieldId int
 }
 }
 
 
-type DelGameShieldRequest struct {
-	RuleId int `json:"id" form:"id"`
+type GameShieldRuleIdRequest struct {
+	RuleId int `json:"rule_id" form:"rule_id" binding:"required"`
+}
+
+type SDKInfo struct {
+	RuleID     string `json:"rule_id"`     // 规则ID
+	ClientIP   string `json:"client_ip"`   // 客户端IP
+	GatewayIP  string `json:"gateway_ip"`  // 网关IP
+	SDKUUID    string `json:"sdk_uuid"`    // SDK-UUID
+	SessionID  string `json:"session_id"`  // 会话ID
+	SDKType    string `json:"sdk_type"`    // SDK类型
+	SDKVersion string `json:"sdk_version"` // SDK版本
+	System     string `json:"system"`      // 系统
+	ExtraInfo  string `json:"extra_info"`  // 附加信息
 }
 }

+ 15 - 1
internal/handler/gameshield.go

@@ -88,7 +88,7 @@ func (h *GameShieldHandler) GetGameShieldKey(ctx *gin.Context) {
 }
 }
 
 
 func (h *GameShieldHandler) DeleteGameShield(ctx *gin.Context) {
 func (h *GameShieldHandler) DeleteGameShield(ctx *gin.Context) {
-	req := new(v1.DelGameShieldRequest)
+	req := new(v1.GameShieldRuleIdRequest)
 	if err := ctx.ShouldBind(req); err != nil {
 	if err := ctx.ShouldBind(req); err != nil {
 		v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, err.Error())
 		v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, err.Error())
 		return
 		return
@@ -100,3 +100,17 @@ func (h *GameShieldHandler) DeleteGameShield(ctx *gin.Context) {
 	}
 	}
 	v1.HandleSuccess(ctx, res)
 	v1.HandleSuccess(ctx, res)
 }
 }
+
+func (h *GameShieldHandler) GetGameShieldOnlineList(ctx *gin.Context) {
+	req := new(v1.GameShieldRuleIdRequest)
+	if err := ctx.ShouldBind(req); err != nil {
+		v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, err.Error())
+		return
+	}
+	res, err := h.gameShieldService.GetGameShieldOnlineList(ctx, req.RuleId)
+	if err != nil {
+		v1.HandleError(ctx, http.StatusInternalServerError, err, err.Error())
+		return
+	}
+	v1.HandleSuccess(ctx, res)
+}

+ 1 - 0
internal/server/http.go

@@ -82,6 +82,7 @@ func NewHTTPServer(
 			noAuthRouter.POST("/gameShield/getKey", gameShieldHandler.GetGameShieldKey)
 			noAuthRouter.POST("/gameShield/getKey", gameShieldHandler.GetGameShieldKey)
 			noAuthRouter.POST("/gameShield/edit", gameShieldHandler.EditGameShield)
 			noAuthRouter.POST("/gameShield/edit", gameShieldHandler.EditGameShield)
 			noAuthRouter.POST("/gameShield/delete", gameShieldHandler.DeleteGameShield)
 			noAuthRouter.POST("/gameShield/delete", gameShieldHandler.DeleteGameShield)
+			noAuthRouter.POST("/gameShield/getOnline", gameShieldHandler.GetGameShieldOnlineList)
 			noAuthRouter.POST("/webForward/add", webForwardingHandler.AddWebForwarding)
 			noAuthRouter.POST("/webForward/add", webForwardingHandler.AddWebForwarding)
 			noAuthRouter.POST("/webForward/edit", webForwardingHandler.EditWebForwarding)
 			noAuthRouter.POST("/webForward/edit", webForwardingHandler.EditWebForwarding)
 			noAuthRouter.POST("/webForward/delete", webForwardingHandler.DeleteWebForwarding)
 			noAuthRouter.POST("/webForward/delete", webForwardingHandler.DeleteWebForwarding)

+ 65 - 0
internal/service/gameShieldCrawler.go

@@ -2,6 +2,8 @@ package service
 
 
 import (
 import (
 	"bytes"
 	"bytes"
+	"compress/flate"
+	"compress/gzip"
 	"context"
 	"context"
 	"crypto/rand"
 	"crypto/rand"
 	"crypto/tls"
 	"crypto/tls"
@@ -16,6 +18,7 @@ import (
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"strings"
 	"strings"
+	"time"
 )
 )
 
 
 type CrawlerService interface {
 type CrawlerService interface {
@@ -25,6 +28,7 @@ type CrawlerService interface {
 	GetField(ctx context.Context, appName string) (map[string]interface{}, error)
 	GetField(ctx context.Context, appName string) (map[string]interface{}, error)
 	GetKey(ctx context.Context, appName string) (string, error)
 	GetKey(ctx context.Context, appName string) (string, error)
 	DeleteRule(ctx context.Context, ruleID int, ruleUrl string) (string, error)
 	DeleteRule(ctx context.Context, ruleID int, ruleUrl string) (string, error)
+	FetchPageContent(ctx context.Context, url string, cookie string) ([]byte, error)
 }
 }
 
 
 type CrawlerConfig struct {
 type CrawlerConfig struct {
@@ -297,3 +301,64 @@ func (service *crawlerService) DeleteRule(ctx context.Context, ruleID int, ruleU
 
 
 	return res, nil
 	return res, nil
 }
 }
+
+func (service *crawlerService) FetchPageContent(ctx context.Context, url string, cookie string) ([]byte, error) {
+	fetchUrl := service.config.URL + url
+	// 配置 HTTP 客户端
+	client := &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig:     &tls.Config{InsecureSkipVerify: true},
+			MaxIdleConns:        100,
+			MaxIdleConnsPerHost: 100,
+			IdleConnTimeout:     90 * time.Second,
+		},
+		Timeout: 30 * time.Second,
+	}
+
+	// 构造请求
+	req, err := http.NewRequestWithContext(ctx, "GET", fetchUrl, nil)
+	if err != nil {
+		return nil, fmt.Errorf("创建请求失败: %v", err)
+	}
+
+	// 设置请求头
+	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
+	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
+	req.Header.Set("Accept-Encoding", "gzip, deflate, br")
+	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7")
+	req.Header.Set("Cookie", cookie)
+
+	// 发起请求
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("请求发送失败: %v", err)
+	}
+	defer resp.Body.Close()
+
+	// 检查响应状态码
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("请求失败,状态码: %d", resp.StatusCode)
+	}
+
+	// 处理压缩响应
+	var reader io.Reader = resp.Body
+	switch resp.Header.Get("Content-Encoding") {
+	case "gzip":
+		gzipReader, err := gzip.NewReader(resp.Body)
+		if err != nil {
+			return nil, fmt.Errorf("解压 gzip 响应失败: %v", err)
+		}
+		defer gzipReader.Close()
+		reader = gzipReader
+	case "deflate":
+		reader = flate.NewReader(resp.Body)
+	}
+
+	// 读取响应内容
+	content, err := io.ReadAll(reader)
+	if err != nil {
+		return nil, fmt.Errorf("读取响应内容失败: %v", err)
+	}
+
+	return content, nil
+}

+ 21 - 0
internal/service/gameshield.go

@@ -17,6 +17,7 @@ type GameShieldService interface {
 	DeleteGameShield(ctx context.Context, req int) (string, error)
 	DeleteGameShield(ctx context.Context, req int) (string, error)
 	GetGameShieldKey(ctx context.Context, id int) (string, error)
 	GetGameShieldKey(ctx context.Context, id int) (string, error)
 	GetKeyAndEditGameShield(ctx context.Context, hostId int, dunName string) (string, error)
 	GetKeyAndEditGameShield(ctx context.Context, hostId int, dunName string) (string, error)
+	GetGameShieldOnlineList(ctx context.Context, hostId int) ([]v1.SDKInfo, error)
 }
 }
 
 
 func NewGameShieldService(
 func NewGameShieldService(
@@ -167,3 +168,23 @@ func (service *gameShieldService) GetKeyAndEditGameShield(ctx context.Context, h
 	}
 	}
 	return "", nil
 	return "", nil
 }
 }
+
+func (service *gameShieldService) GetGameShieldOnlineList(ctx context.Context, hostId int) ([]v1.SDKInfo, error) {
+	strHostId := strconv.Itoa(hostId)
+	cookie, err := service.required.Required(ctx)
+	if err != nil {
+		return nil, err
+	}
+	respBody, err := service.crawlerService.FetchPageContent(ctx, "admin/sdk/online?rule_id="+strHostId, cookie)
+	if err != nil {
+		return nil, err
+	}
+	res, err := service.parser.ParseSDKOnlineHTMLTable(string(respBody))
+	if err != nil {
+		return nil, err
+	}
+	if len(res) == 0 {
+		return nil, fmt.Errorf("暂无数据")
+	}
+	return res, nil
+}

+ 57 - 0
internal/service/parser.go

@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"github.com/PuerkitoBio/goquery"
 	"github.com/PuerkitoBio/goquery"
+	v1 "github.com/go-nunu/nunu-layout-advanced/api/v1"
 	"strings"
 	"strings"
 )
 )
 
 
@@ -13,6 +14,7 @@ type ParserService interface {
 	GetMessage(ctx context.Context, req []byte) (string, error)
 	GetMessage(ctx context.Context, req []byte) (string, error)
 	ParseAlert(html string) (message string, err error)
 	ParseAlert(html string) (message string, err error)
 	GetRuleId(ctx context.Context, htmlBytes []byte) (string, error)
 	GetRuleId(ctx context.Context, htmlBytes []byte) (string, error)
+	ParseSDKOnlineHTMLTable(htmlContent string) ([]v1.SDKInfo, error)
 }
 }
 
 
 func NewParserService(
 func NewParserService(
@@ -80,3 +82,58 @@ func (s *parserService) GetRuleId(ctx context.Context, htmlBytes []byte) (string
 		Find("td").Eq(1).Text()             // 第 2 个 td
 		Find("td").Eq(1).Text()             // 第 2 个 td
 	return strings.TrimSpace(id), nil
 	return strings.TrimSpace(id), nil
 }
 }
+
+// 解析 Sdk在线情况 表格
+func (s *parserService) ParseSDKOnlineHTMLTable(htmlContent string) ([]v1.SDKInfo, error) {
+	// 创建goquery文档
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
+	if err != nil {
+		return nil, fmt.Errorf("解析HTML失败: %v", err)
+	}
+
+	var sdkInfos []v1.SDKInfo
+
+	// 查找表格并解析数据行
+	doc.Find("table.table.table-hover tbody tr").Each(func(i int, s *goquery.Selection) {
+		// 跳过表头行(如果有的话)
+		if s.Find("th").Length() > 0 {
+			return
+		}
+
+		var info v1.SDKInfo
+
+		// 解析每一列的数据
+		s.Find("td").Each(func(j int, td *goquery.Selection) {
+			text := strings.TrimSpace(td.Text())
+
+			// 根据列的位置分配到对应字段(跳过第一列的复选框)
+			switch j {
+			case 1: // 规则ID
+				info.RuleID = text
+			case 2: // 客户端IP
+				info.ClientIP = text
+			case 3: // 网关IP
+				info.GatewayIP = text
+			case 4: // SDK-UUID
+				info.SDKUUID = text
+			case 5: // 会话ID
+				info.SessionID = text
+			case 6: // SDK类型
+				info.SDKType = text
+			case 7: // SDK版本
+				info.SDKVersion = text
+			case 8: // 系统
+				info.System = text
+			case 9: // 附加信息
+				info.ExtraInfo = text
+			}
+		})
+
+		// 只有当规则ID不为空时才添加记录
+		if info.RuleID != "" {
+			sdkInfos = append(sdkInfos, info)
+		}
+	})
+
+	return sdkInfos, nil
+}