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 { // 如果关键字匹配,就从固定位置(第二个,索引为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本身被隐藏在一个
 标签内,我们直接定位它。
		keyCell := row.Find("td:nth-of-type(7) pre")
		fullKeyText := keyCell.Text() // 获取 
 标签内的所有文本内容。

		// 原始文本的格式是固定的,我们需要从中提取出真正的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
}