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 { // 如果关键字匹配,就从固定位置(第二个
标签内,我们直接定位它。 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 }