123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- package service
- import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "github.com/PuerkitoBio/goquery"
- v1 "github.com/go-nunu/nunu-layout-advanced/api/v1"
- "strings"
- )
- type ParserService interface {
- GetMessage(ctx context.Context, req []byte) (string, error)
- ParseAlert(html string) (message string, err error)
- GetRuleId(ctx context.Context, htmlBytes []byte) (string, error)
- ParseSDKOnlineHTMLTable(htmlContent string) ([]v1.SDKInfo, error)
- CheckSDKKeyStatus(htmlData string, sdkKeyToFind string) error
- GetRuleIdByColumnName(ctx context.Context, htmlBytes []byte, columnName string) (string, error)
- }
- func NewParserService(
- service *Service,
- ) ParserService {
- return &parserService{
- Service: service,
- }
- }
- type parserService struct {
- *Service
- }
- // 解析 alert 消息
- func (s *parserService) ParseAlert(html string) (message string, err error) {
- doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
- if err != nil {
- return "", err
- }
- sel := doc.Find(".alert")
- if sel.Length() == 0 {
- // 没有 .alert 元素
- return "", nil
- }
- // 找到 .alert,继续提取
- t := strings.TrimSpace(sel.Find("h4").Text())
- full := strings.TrimSpace(sel.Text())
- full = strings.TrimPrefix(full, "×")
- full = strings.TrimSpace(full)
- m := strings.TrimSpace(strings.TrimPrefix(full, t))
- return m, nil
- }
- func (s *parserService) GetMessage(ctx context.Context, req []byte) (string, error) {
- type msg struct {
- Message string `json:"msg"` // 如果字段叫 msg,用 `json:"msg"`
- }
- var m msg
- if err := json.Unmarshal(req, &m); err != nil {
- return "", fmt.Errorf("解析 message 失败: %v", err)
- }
- if m.Message == "no affect row" {
- return "", fmt.Errorf("没有该条数据")
- }
- return m.Message, nil
- }
- func (s *parserService) GetRuleId(ctx context.Context, htmlBytes []byte) (string, error) {
- // 1. 把 []byte 包成 io.Reader
- reader := bytes.NewReader(htmlBytes)
- // 2. 用 goquery 解析
- doc, err := goquery.NewDocumentFromReader(reader)
- if err != nil {
- return "", err
- }
- // 方法一:按位置拿(第 2 个 tr、第 2 个 td)
- id := doc.
- Find("table.table tbody tr").Eq(1). // 跳过表头行,拿第一条数据
- Find("td").Eq(1).Text() // 第 2 个 td
- return strings.TrimSpace(id), nil
- }
- func (s *parserService) GetRuleIdByColumnName(ctx context.Context, htmlBytes []byte, name string) (string, error) {
- // 1. 定义我们用来搜索的列所有可能的名称。
- possibleKeyNames := []string{"标签", "网关组名称"}
- // 2. 解析HTML。
- reader := bytes.NewReader(htmlBytes)
- doc, err := goquery.NewDocumentFromReader(reader)
- if err != nil {
- return "", fmt.Errorf("failed to parse html: %w", err)
- }
- // 3. 动态查找第一个匹配的“关键字列”的索引。
- headerRow := doc.Find("table.table tbody tr:first-child")
- if headerRow.Length() == 0 {
- return "", nil
- }
- keyColumnIndex := -1
- // 我们使用 EachWithBreak,一旦找到有效的列就立即停止搜索,提高效率。
- headerRow.Find("th").EachWithBreak(func(index int, th *goquery.Selection) bool {
- headerText := strings.TrimSpace(th.Text())
- // 检查当前的表头文本是否匹配我们预设的任何一个可能名称。
- for _, possibleName := range possibleKeyNames {
- if headerText == possibleName {
- keyColumnIndex = index
- return false // 找到了,立即停止遍历表头(th)。
- }
- }
- return true // 没找到,继续遍历下一个表头(th)。
- })
- // 4. 检查我们是否找到了任何一个可能的关键字列。
- if keyColumnIndex == -1 {
- return "", fmt.Errorf("找不到任何一个指定的关键字列: %v", possibleKeyNames)
- }
- // 5. 遍历数据行以查找匹配的行。
- var foundID string
- var found bool
- doc.Find("table.table tbody tr").Slice(1, goquery.ToEnd).EachWithBreak(func(i int, row *goquery.Selection) bool {
- // 使用动态找到的索引来获取正确的单元格。
- keyCell := row.Find("td").Eq(keyColumnIndex)
- keyText := strings.TrimSpace(keyCell.Text())
- if keyText == name {
- // 如果关键字匹配,就从固定位置(第二个<td>,索引为1)获取值。
- idCell := row.Find("td").Eq(1)
- foundID = strings.TrimSpace(idCell.Text())
- found = true
- return false // 找到了目标行,停止循环。
- }
- return true // 不是目标行,继续。
- })
- // 6. 返回结果,如果未找到则返回错误。
- if !found {
- return "", fmt.Errorf("找不到关键字列值为 '%s' 的行", name)
- }
- return foundID, 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: // 附加信息
- // 对于附加信息列,提取JSON内容
- info.ExtraInfo = extractJSONFromExtraInfo(text)
- }
- })
- // 只有当规则ID不为空时才添加记录
- if info.RuleID != "" {
- sdkInfos = append(sdkInfos, info)
- }
- })
- return sdkInfos, nil
- }
- // extractJSONFromExtraInfo 从附加信息字符串中提取JSON内容
- func extractJSONFromExtraInfo(text string) string {
- text = strings.TrimSpace(text)
- // 尝试直接解析
- if result := tryParseJSON(text); result != "" {
- return result
- }
- // 尝试解析JSON字符串(去掉外层引号)
- if strings.HasPrefix(text, `"`) && strings.HasSuffix(text, `"`) {
- var jsonContent string
- if json.Unmarshal([]byte(text), &jsonContent) == nil {
- if result := tryParseJSON(jsonContent); result != "" {
- return result
- }
- }
- }
- // 从复杂文本中提取JSON
- return extractFromComplexText(text)
- }
- // 统一的JSON解析和格式化函数
- func tryParseJSON(text string) string {
- var temp interface{}
- if json.Unmarshal([]byte(text), &temp) == nil {
- if formatted, err := json.Marshal(temp); err == nil {
- return string(formatted)
- }
- return text
- }
- return ""
- }
- // 简化的复杂文本JSON提取
- func extractFromComplexText(text string) string {
- // 找到最后一个完整的JSON对象
- for end := strings.LastIndex(text, "}"); end != -1; end = strings.LastIndex(text[:end], "}") {
- // 向前查找匹配的开始大括号
- braceCount := 1
- for start := end - 1; start >= 0; start-- {
- switch text[start] {
- case '}':
- braceCount++
- case '{':
- braceCount--
- if braceCount == 0 {
- candidate := text[start : end+1]
- if result := tryParseJSON(candidate); result != "" {
- return result
- }
- break // 跳出内层循环,继续寻找下一个'}'
- }
- }
- }
- }
- return "查看"
- }
- // CheckSDKKeyStatus 检查SDKKey是否存在且过期
- func (s *parserService) CheckSDKKeyStatus(htmlData string, sdkKeyToFind string) error {
- // 使用 strings.NewReader 将字符串转换为一个 io.Reader,这是 go-query 所需的输入格式。
- // go-query 会加载并解析这个HTML,返回一个可供查询的文档对象(doc)。
- doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlData))
- if err != nil {
- // 如果go-query无法加载或解析HTML,则返回错误。
- return fmt.Errorf("无法解析HTML: %w", err)
- }
- // 定义两个变量用于在循环结束后判断状态
- var keyFound bool = false // 标记是否找到了Key
- var resultErr error = nil // 用于存储找到Key后的最终错误状态(如果是过期的话)
- // 使用go-query的选择器找到class为 "table" 的表格(table.table)的主体(tbody)中的所有行(tr)。
- // .EachWithBreak 方法会遍历每一行,并允许我们在满足特定条件时提前中断循环。
- doc.Find("table.table tbody tr").EachWithBreak(func(i int, row *goquery.Selection) bool {
- // 在当前行(row)中,查找第7个单元格(td:nth-of-type(7)),这是“SDK启动KEY”所在的列。
- // Key本身被隐藏在一个 <pre> 标签内,我们直接定位它。
- keyCell := row.Find("td:nth-of-type(7) pre")
- fullKeyText := keyCell.Text() // 获取 <pre> 标签内的所有文本内容。
- // 原始文本的格式是固定的,我们需要从中提取出真正的KEY。
- // 格式: "原始内容:... >>> SDK启动KEY如下,复制后启动SDK使用 <<< [THE_ACTUAL_KEY]"
- parts := strings.Split(fullKeyText, ">>> SDK启动KEY如下,复制后启动SDK使用 <<<")
- if len(parts) < 2 {
- // 如果当前行不符合这个格式,跳到下一行处理。
- return true // `true` 在 EachWithBreak 中表示继续循环
- }
- // 提取并清理KEY字符串,去掉它前后的所有空格和换行符。
- extractedKey := strings.TrimSpace(parts[1])
- // 检查从页面提取出的KEY是否与我们要找的KEY相匹配。
- if extractedKey == sdkKeyToFind {
- keyFound = true // 首先,标记我们已经找到了Key
- // 接着,在同一行中查找过期状态。
- // 第11个单元格(td:nth-of-type(11))包含“过期时间”信息。
- expirationCell := row.Find("td:nth-of-type(11)")
- expirationText := expirationCell.Text() // 获取该单元格的文本内容。
- // 检查过期时间文本中是否包含`(已过期)`
- if strings.Contains(expirationText, "(已过期)") {
- // 如果包含,我们将错误信息赋值给外部的 resultErr 变量
- resultErr = fmt.Errorf("该KEY已过期")
- }
- // 注意:即使未过期,resultErr 仍然是 nil,这正是我们想要的结果。
- // 我们已经找到了目标并处理完毕,没有必要再检查剩下的行了。
- return false // `false` 在 EachWithBreak 中表示中断循环
- }
- // 如果当前行的KEY不匹配,继续下一行的查找。
- return true
- })
- // --- 循环结束后的最终判断 ---
- // 如果 keyFound 标志位仍然是 false,说明遍历了所有行都没有找到匹配的Key。
- if !keyFound {
- return fmt.Errorf("未找到指定的Key")
- }
- // 如果 keyFound 是 true,说明找到了Key。
- // 此时,我们返回在循环中确定的 resultErr。
- // - 如果Key未过期,resultErr 就是它初始的 nil 值。
- // - 如果Key已过期,resultErr 就是我们在循环里设置的那个 error。
- return resultErr
- }
|