Back to Pansou

SuSu 搜索插件设计文档

plugin/susu/susu插件设计文档.md

2.043.8 KB
Original Source

SuSu 搜索插件设计文档

目录

  1. 概述
  2. 设计背景
  3. 总体架构
  4. 核心组件
  5. 关键算法
  6. 性能优化
  7. 错误处理与容错
  8. 测试策略
  9. 部署与集成
  10. 未来扩展
  11. 附录

概述

1.1 文档目的

本文档详细描述了 SuSu 搜索插件的设计与实现,旨在为开发者提供完整的技术参考。文档涵盖了插件的架构设计、核心组件、关键算法、性能优化策略以及错误处理机制等方面,以确保插件能够高效、稳定地运行。

1.2 插件简介

SuSu 搜索插件是 PanSou 网盘搜索系统的一个重要组成部分,专门用于从 SuSu 网站(susuifa.com)搜索并提取网盘资源链接。该插件实现了高效的并发处理、智能缓存机制和可靠的错误处理,能够快速响应用户的搜索请求,并提供准确的搜索结果。

1.3 主要特性

  • 异步搜索:基于 BaseAsyncPlugin 实现异步搜索,支持"尽快响应,持续处理"的模式
  • 多级缓存:实现了多层次的缓存系统,减少重复计算和网络请求
  • 并发处理:采用 goroutine 和信号量控制,实现高效的并发搜索
  • 智能过滤:在早期阶段就过滤掉不相关的结果,提高处理效率
  • 重试机制:实现了指数退避的重试策略,提高请求成功率
  • 容错设计:即使部分请求失败,仍能返回有用的结果

设计背景

2.1 需求分析

SuSu 网站是一个包含大量网盘资源的平台,用户需要能够快速搜索并获取这些资源的链接。主要需求包括:

  1. 高效搜索:能够快速从 SuSu 网站搜索相关资源
  2. 准确提取:正确提取网盘链接和提取码
  3. 良好体验:响应迅速,支持异步处理
  4. 资源节约:减少不必要的网络请求和计算资源消耗
  5. 稳定可靠:具备错误处理和重试机制,确保服务稳定性

2.2 技术挑战

在实现 SuSu 插件过程中,面临以下技术挑战:

  1. 多步骤请求:获取一个完整的网盘链接需要多次 API 请求
  2. JWT 解析:网盘链接被 JWT 加密,需要解析才能获取真实链接
  3. 并发控制:需要在高效与资源消耗间取得平衡
  4. 反爬虫机制:需要模拟真实用户行为,避免被网站封禁
  5. 缓存一致性:确保缓存数据的准确性和时效性

2.3 设计目标

基于需求和挑战,制定了以下设计目标:

  1. 响应时间:平均搜索响应时间控制在 3 秒以内
  2. 成功率:API 请求成功率达到 95% 以上
  3. 资源效率:减少 50% 以上的不必要网络请求
  4. 可扩展性:设计良好的接口,便于未来功能扩展
  5. 可维护性:代码结构清晰,文档完善,便于维护

总体架构

3.1 架构概览

SuSu 插件采用分层架构设计,主要包含以下几个层次:

┌─────────────────────────┐
│     SusuAsyncPlugin     │
└───────────┬─────────────┘
            │
┌───────────▼─────────────┐
│     搜索处理层           │
│  (doSearch, extractPostID) │
└───────────┬─────────────┘
            │
┌───────────▼─────────────┐
│     链接获取层           │
│  (getLinks, getButtonDetail) │
└───────────┬─────────────┘
            │
┌───────────▼─────────────┐
│     工具支持层           │
│  (decodeJWTURL, determineLinkType) │
└───────────┬─────────────┘
            │
┌───────────▼─────────────┐
│     基础设施层           │
│  (缓存系统, HTTP客户端)   │
└─────────────────────────┘

图 3.1 SuSu 插件架构图

3.2 工作流程

SuSu 插件的完整工作流程如下图所示:

┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
│  接收   │     │ 搜索页面 │     │ 提取帖子 │     │ 获取网盘 │     │ 返回结果 │
│  请求   ├────►│  HTML   ├────►│   信息   ├────►│   链接   ├────►│         │
└─────────┘     └─────────┘     └─────────┘     └─────────┘     └─────────┘
     │              │               │               │               │
     ▼              ▼               ▼               ▼               ▼
┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
│ 参数处理 │     │ 关键词过滤│     │ 并发处理 │     │ JWT解析  │     │ 缓存结果 │
└─────────┘     └─────────┘     └─────────┘     └─────────┘     └─────────┘

图 3.2 SuSu 插件工作流程图

3.3 组件关系

插件各组件之间的关系和数据流向如下图所示:

                          ┌───────────────┐
                          │  Search()     │
                          └───────┬───────┘
                                  │
                                  ▼
                          ┌───────────────┐
                          │  doSearch()   │
                          └───────┬───────┘
                                  │
                 ┌────────────────┴────────────────┐
                 │                                 │
                 ▼                                 ▼
         ┌───────────────┐                 ┌───────────────┐
         │ extractPostID()│                 │  getLinks()   │
         └───────────────┘                 └───────┬───────┘
                                                   │
                                                   ▼
                                           ┌───────────────┐
                                           │getButtonDetail()│
                                           └───────┬───────┘
                                                   │
                                  ┌────────────────┴────────────────┐
                                  │                                 │
                                  ▼                                 ▼
                          ┌───────────────┐                 ┌───────────────┐
                          │ decodeJWTURL() │                 │determineLinkType()│
                          └───────────────┘                 └───────────────┘

图 3.3 SuSu 插件组件关系图

3.4 关键接口

插件实现了 SearchPlugin 接口,并基于 BaseAsyncPlugin 进行扩展:

go
// SearchPlugin 接口
type SearchPlugin interface {
    Name() string
    Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
    Priority() int
}

// SusuAsyncPlugin 结构体
type SusuAsyncPlugin struct {
    *plugin.BaseAsyncPlugin
}

主要方法包括:

  • Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error):对外提供的搜索接口
  • doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error):实际的搜索实现
  • getLinks(client *http.Client, postID string) ([]model.Link, error):获取网盘链接
  • getButtonDetail(client *http.Client, postID string, index int) (model.Link, error):获取按钮详情
  • decodeJWTURL(jwtToken string) (string, error):解析JWT获取真实链接

核心组件

4.1 缓存系统

缓存系统是 SuSu 插件性能优化的关键部分,采用多级缓存设计,针对不同类型的操作设置独立缓存。

4.1.1 缓存类型

go
// 缓存相关变量
var (
    // 帖子ID缓存
    postIDCache = sync.Map{}
    
    // 按钮列表缓存
    buttonListCache = sync.Map{}
    
    // 按钮详情缓存
    buttonDetailCache = sync.Map{}
    
    // JWT解析结果缓存
    jwtDecodeCache = sync.Map{}
    
    // 链接类型判断缓存
    linkTypeCache = sync.Map{}
)

每个缓存的作用:

  • postIDCache:缓存从 HTML 元素中提取的帖子 ID,避免重复提取
  • buttonListCache:缓存帖子的网盘按钮列表,减少 API 请求
  • buttonDetailCache:缓存按钮详情信息,减少 API 请求
  • jwtDecodeCache:缓存 JWT 解码结果,避免重复解码
  • linkTypeCache:缓存链接类型判断结果,避免重复判断

4.1.2 缓存管理

缓存系统包含定期清理机制,避免内存泄漏:

go
// startCacheCleaner 定期清理缓存
func startCacheCleaner() {
    ticker := time.NewTicker(1 * time.Hour)
    defer ticker.Stop()
    
    for range ticker.C {
        // 清空所有缓存
        postIDCache = sync.Map{}
        buttonListCache = sync.Map{}
        buttonDetailCache = sync.Map{}
        jwtDecodeCache = sync.Map{}
        linkTypeCache = sync.Map{}
    }
}

缓存系统的工作流程如下图所示:

┌───────────┐     ┌───────────┐     ┌───────────┐
│  操作请求  │     │ 检查缓存  │     │ 返回缓存  │
│           ├────►│           ├────►│   结果    │
└───────────┘     └─────┬─────┘     └───────────┘
                        │ 缓存未命中
                        ▼
                  ┌───────────┐     ┌───────────┐
                  │ 执行实际  │     │ 缓存结果  │
                  │   操作    ├────►│           │
                  └─────┬─────┘     └───────────┘
                        │
                        ▼
                  ┌───────────┐
                  │ 返回操作  │
                  │   结果    │
                  └───────────┘

图 4.1 缓存系统工作流程图

4.2 HTTP 客户端

HTTP 客户端负责与 SuSu 网站的 API 进行通信,包括请求构建、发送和响应处理。

4.2.1 请求头管理

为了模拟真实用户行为,避免被反爬虫机制识别,插件实现了随机 User-Agent 功能:

go
// 常用UA列表
var userAgents = []string{
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
}

// getRandomUA 获取随机UA
func getRandomUA() string {
    return userAgents[rand.Intn(len(userAgents))]
}

请求头设置示例:

go
// 设置请求头
req.Header.Set("User-Agent", getRandomUA())
req.Header.Set("Referer", "https://susuifa.com/")

4.2.2 重试机制

为了处理临时网络问题,插件实现了带有指数退避算法的重试机制:

go
// doRequestWithRetry 发送HTTP请求并支持重试
func (p *SusuAsyncPlugin) doRequestWithRetry(client *http.Client, req *http.Request, maxRetries int) (*http.Response, error) {
    var resp *http.Response
    var err error
    
    for i := 0; i <= maxRetries; i++ {
        // 如果不是第一次尝试,等待一段时间
        if i > 0 {
            // 指数退避算法
            backoff := time.Duration(1<<uint(i-1)) * 500 * time.Millisecond
            if backoff > 5*time.Second {
                backoff = 5 * time.Second
            }
            time.Sleep(backoff)
        }
        
        // 克隆请求,避免重用同一个请求对象
        reqClone := req.Clone(req.Context())
        
        // 发送请求
        resp, err = client.Do(reqClone)
        
        // 如果请求成功或者是不可重试的错误,则退出循环
        if err == nil || !isRetriableError(err) {
            break
        }
    }
    
    return resp, err
}

重试机制的工作流程如下图所示:

┌───────────┐     ┌───────────┐     ┌───────────┐
│  发送请求  │     │ 请求成功? │ 是  │ 返回响应  │
│           ├────►│           ├────►│           │
└───────────┘     └─────┬─────┘     └───────────┘
                        │ 否
                        ▼
                  ┌───────────┐     ┌───────────┐
                  │ 是可重试  │ 否  │ 返回错误  │
                  │   错误?   ├────►│           │
                  └─────┬─────┘     └───────────┘
                        │ 是
                        ▼
                  ┌───────────┐     ┌───────────┐
                  │ 重试次数  │ 是  │ 返回错误  │
                  │  已满?    ├────►│           │
                  └─────┬─────┘     └───────────┘
                        │ 否
                        ▼
                  ┌───────────┐
                  │ 等待退避  │
                  │   时间    │
                  └─────┬─────┘
                        │
                        ▼
                  ┌───────────┐
                  │ 重新发送  │
                  │   请求    │
                  └───────────┘

图 4.2 HTTP 重试机制工作流程图

4.3 并发控制

SuSu 插件使用 goroutine 和信号量实现并发控制,在提高处理效率的同时避免资源过度消耗。

4.3.1 信号量控制

使用带缓冲的通道作为信号量,限制并发数量:

go
// 创建信号量控制并发数
semaphore := make(chan struct{}, MaxConcurrency)

// 获取信号量
semaphore <- struct{}{}
defer func() { <-semaphore }()

4.3.2 并发处理模型

使用 goroutine、WaitGroup 和通道实现并发处理:

go
// 提取搜索结果
var wg sync.WaitGroup
resultChan := make(chan model.SearchResult, 20)
errorChan := make(chan error, 20)

// 并发处理每个搜索结果项
for i, s := range items {
    wg.Add(1)
    
    go func(index int, s *goquery.Selection) {
        defer wg.Done()
        
        // 获取信号量
        semaphore <- struct{}{}
        defer func() { <-semaphore }()
        
        // 处理单个搜索结果
        // ...
        
        resultChan <- result
    }(i, s)
}

// 等待所有goroutine完成
go func() {
    wg.Wait()
    close(resultChan)
    close(errorChan)
}()

// 收集结果
var results []model.SearchResult
for result := range resultChan {
    results = append(results, result)
}

并发处理模型的工作流程如下图所示:

┌───────────┐     ┌───────────┐     ┌───────────┐
│ 搜索结果  │     │ 创建多个  │     │ 等待所有  │
│   列表    ├────►│ goroutine ├────►│任务完成   │
└───────────┘     └───────────┘     └─────┬─────┘
                        │                 │
                        ▼                 ▼
                  ┌───────────┐     ┌───────────┐
                  │ 信号量    │     │ 收集处理  │
                  │ 控制并发  │     │   结果    │
                  └───────────┘     └───────────┘

图 4.3 并发处理模型工作流程图

4.4 早期过滤机制

为了减少不必要的网络请求,SuSu 插件在早期阶段就过滤掉不包含关键词的帖子:

go
// 将关键词转为小写,用于不区分大小写的比较
lowerKeyword := strings.ToLower(keyword)

// 将关键词按空格分割,用于支持多关键词搜索
keywords := strings.Fields(lowerKeyword)

// 预先过滤不包含关键词的帖子
doc.Find(".post-list-item").Each(func(i int, s *goquery.Selection) {
    // 提取标题
    title := s.Find(".post-info h2 a").Text()
    title = strings.TrimSpace(title)
    lowerTitle := strings.ToLower(title)
    
    // 检查每个关键词是否在标题中
    matched := true
    for _, kw := range keywords {
        // 对于所有关键词,检查是否在标题中
        if !strings.Contains(lowerTitle, kw) {
            matched = false
            break
        }
    }
    
    // 只添加匹配的帖子
    if matched {
        items = append(items, s)
    }
})

早期过滤机制的工作流程如下图所示:

┌───────────┐     ┌───────────┐     ┌───────────┐
│ 搜索结果  │     │ 提取标题  │     │ 关键词    │
│   HTML    ├────►│ 和内容    ├────►│ 匹配检查  │
└───────────┘     └───────────┘     └─────┬─────┘
                                          │
                  ┌───────────┐           │
                  │ 丢弃不    │ 否        │
                  │ 匹配项    │◄──────────┘
                  └───────────┘           │
                                          │ 是
                                          ▼
                                    ┌───────────┐
                                    │ 添加到    │
                                    │ 处理列表  │
                                    └───────────┘

图 4.4 早期过滤机制工作流程图

关键算法

5.1 JWT 解析算法

SuSu 网站使用 JWT(JSON Web Token)加密网盘链接,插件需要解析 JWT 才能获取真实链接。

5.1.1 算法描述

JWT 解析算法的步骤如下:

  1. 将 JWT 按点(.)分割成三部分:header、payload、signature
  2. 使用 Base64URL 解码 payload 部分
  3. 将解码后的 payload 解析为 JSON 对象
  4. 从 JSON 对象中提取 data.url 字段,即为真实链接

5.1.2 代码实现

go
// decodeJWTURL 解析JWT token获取真实链接
func (p *SusuAsyncPlugin) decodeJWTURL(jwtToken string) (string, error) {
    // 检查缓存
    if cachedURL, ok := jwtDecodeCache.Load(jwtToken); ok {
        return cachedURL.(string), nil
    }
    
    // 分割JWT
    parts := strings.Split(jwtToken, ".")
    if len(parts) != 3 {
        return "", fmt.Errorf("无效的JWT格式")
    }
    
    // 解码Payload
    payload, err := base64.RawURLEncoding.DecodeString(parts[1])
    if err != nil {
        return "", fmt.Errorf("解码Payload失败: %w", err)
    }
    
    // 解析JSON
    var payloadData struct {
        Data struct {
            URL string `json:"url"`
        } `json:"data"`
    }
    if err := json.Unmarshal(payload, &payloadData); err != nil {
        return "", fmt.Errorf("解析Payload JSON失败: %w", err)
    }
    
    // 缓存结果
    jwtDecodeCache.Store(jwtToken, payloadData.Data.URL)
    
    return payloadData.Data.URL, nil
}

5.1.3 算法复杂度

  • 时间复杂度:O(n),其中 n 是 JWT token 的长度
  • 空间复杂度:O(n),需要存储解码后的 payload

5.1.4 优化措施

  • 使用缓存存储已解析的结果,避免重复解析
  • 使用 RawURLEncoding 而不是 URLEncoding,避免额外的填充处理

5.2 链接类型判断算法

网盘链接类型判断是将提取的 URL 映射到标准网盘类型的过程。

5.2.1 算法描述

链接类型判断算法的步骤如下:

  1. 将 URL 和名称转为小写,便于比较
  2. 首先根据 URL 中的域名特征判断链接类型
  3. 如果无法从 URL 判断,则根据名称中的关键词判断
  4. 如果都无法判断,则返回默认类型 "others"

5.2.2 代码实现

go
// determineLinkType 根据URL和名称确定链接类型
func (p *SusuAsyncPlugin) determineLinkType(url, name string) string {
    // 生成缓存键
    cacheKey := fmt.Sprintf("%s:%s", url, name)
    
    // 检查缓存
    if cachedType, ok := linkTypeCache.Load(cacheKey); ok {
        return cachedType.(string)
    }
    
    lowerURL := strings.ToLower(url)
    lowerName := strings.ToLower(name)
    
    var linkType string
    
    // 根据URL判断
    switch {
    case strings.Contains(lowerURL, "pan.baidu.com"):
        linkType = "baidu"
    case strings.Contains(lowerURL, "alipan.com") || strings.Contains(lowerURL, "aliyundrive.com"):
        linkType = "aliyun"
    case strings.Contains(lowerURL, "pan.xunlei.com"):
        linkType = "xunlei"
    // ... 其他网盘类型判断
    default:
        // 根据名称判断
        switch {
        case strings.Contains(lowerName, "百度"):
            linkType = "baidu"
        case strings.Contains(lowerName, "阿里"):
            linkType = "aliyun"
        // ... 其他名称判断
        default:
            linkType = "others"
        }
    }
    
    // 缓存结果
    linkTypeCache.Store(cacheKey, linkType)
    
    return linkType
}

5.2.3 算法复杂度

  • 时间复杂度:O(1),使用 switch-case 和字符串包含判断,复杂度与输入长度无关
  • 空间复杂度:O(1),只需存储少量变量

5.2.4 优化措施

  • 使用缓存存储已判断的结果,避免重复判断
  • 先根据 URL 判断,再根据名称判断,符合大多数情况下的判断顺序
  • 使用 strings.Contains 而不是正则表达式,提高性能

5.3 帖子 ID 提取算法

从搜索结果 HTML 中提取帖子 ID 是获取网盘链接的第一步。

5.3.1 算法描述

帖子 ID 提取算法的步骤如下:

  1. 首先尝试从列表项的 ID 属性中提取,格式为 "item-{ID}"
  2. 如果无法从 ID 属性提取,则尝试从详情页链接中提取,格式为 "/{ID}.html"
  3. 如果都无法提取,则返回空字符串

5.3.2 代码实现

go
// extractPostID 从搜索结果项中提取帖子ID
func (p *SusuAsyncPlugin) extractPostID(s *goquery.Selection) string {
    // 生成缓存键
    html, _ := s.Html()
    cacheKey := fmt.Sprintf("postid:%x", md5sum(html))
    
    // 检查缓存
    if cachedID, ok := postIDCache.Load(cacheKey); ok {
        return cachedID.(string)
    }
    
    // 方法1:从列表项ID属性提取
    itemID, exists := s.Attr("id")
    if exists && strings.HasPrefix(itemID, "item-") {
        postID := strings.TrimPrefix(itemID, "item-")
        postIDCache.Store(cacheKey, postID)
        return postID
    }
    
    // 方法2:从详情页链接提取
    href, exists := s.Find(".post-info h2 a").Attr("href")
    if exists {
        re := regexp.MustCompile(`/(\d+)\.html`)
        matches := re.FindStringSubmatch(href)
        if len(matches) > 1 {
            postID := matches[1]
            postIDCache.Store(cacheKey, postID)
            return postID
        }
    }
    
    return ""
}

5.3.3 算法复杂度

  • 时间复杂度:O(n),其中 n 是 HTML 内容的长度
  • 空间复杂度:O(n),需要存储 HTML 内容和正则表达式匹配结果

5.3.4 优化措施

  • 使用缓存存储已提取的结果,避免重复提取
  • 使用简化版的 md5sum 函数生成缓存键,避免完整 HTML 内容的存储
  • 优先从 ID 属性提取,再从链接提取,符合大多数情况下的提取顺序

5.4 指数退避算法

指数退避算法是一种重试策略,随着重试次数的增加,等待时间呈指数增长,避免对服务器造成压力。

5.4.1 算法描述

指数退避算法的步骤如下:

  1. 初始等待时间为 500 毫秒
  2. 每次重试后,等待时间翻倍
  3. 设置最大等待时间为 5 秒,避免等待时间过长

5.4.2 代码实现

go
// 指数退避算法
backoff := time.Duration(1<<uint(i-1)) * 500 * time.Millisecond
if backoff > 5*time.Second {
    backoff = 5 * time.Second
}
time.Sleep(backoff)

5.4.3 算法复杂度

  • 时间复杂度:O(1),计算等待时间的复杂度与输入无关
  • 空间复杂度:O(1),只需存储少量变量

5.4.4 优化措施

  • 设置最大等待时间,避免等待时间过长
  • 使用位移操作(1<<uint(i-1))代替幂运算,提高性能

性能优化

6.1 缓存优化

缓存是提高插件性能的关键技术,SuSu 插件采用了多级缓存策略,针对不同操作设计了专用缓存。

6.1.1 缓存设计原则

  1. 针对性:根据不同操作类型设计专用缓存
  2. 轻量级:缓存键设计简洁,避免存储大量数据
  3. 线程安全:使用 sync.Map 确保并发安全
  4. 生命周期管理:定期清理缓存,避免内存泄漏

6.1.2 缓存效果分析

下表展示了各类缓存的预期效果:

缓存类型缓存命中率性能提升内存占用
帖子ID缓存
按钮列表缓存
按钮详情缓存
JWT解析缓存
链接类型缓存

表 6.1 缓存效果分析表

6.1.3 缓存键设计

缓存键设计是缓存系统的重要部分,良好的缓存键设计可以提高缓存命中率,减少内存占用。

  • postIDCache:使用 HTML 内容的哈希值作为键,避免存储完整 HTML
  • buttonListCache:使用帖子 ID 作为键,简单直接
  • buttonDetailCache:使用 "帖子ID:按钮索引" 作为键,确保唯一性
  • jwtDecodeCache:使用完整 JWT token 作为键,确保准确性
  • linkTypeCache:使用 "URL:名称" 作为键,涵盖判断所需的全部信息

6.2 并发优化

并发处理是提高插件吞吐量的重要手段,SuSu 插件在多个环节采用了并发处理。

6.2.1 并发模型选择

插件采用 goroutine + 信号量的并发模型,具有以下优势:

  1. 轻量级:goroutine 比传统线程更轻量,可以创建大量并发任务
  2. 可控性:通过信号量控制并发数量,避免资源过度消耗
  3. 简洁性:基于 CSP(通信顺序进程)模型,代码结构清晰

6.2.2 并发点设计

插件在两个关键环节实现了并发处理:

  1. 搜索结果处理:并发处理多个搜索结果项,每个结果项在单独的 goroutine 中处理
  2. 网盘链接获取:并发获取多个按钮的详情,每个按钮在单独的 goroutine 中处理

6.2.3 并发控制

为了避免并发过度,插件实现了严格的并发控制:

go
// 创建信号量控制并发数
semaphore := make(chan struct{}, MaxConcurrency)

// 获取信号量
semaphore <- struct{}{}
defer func() { <-semaphore }()

并发数量通过常量 MaxConcurrency 控制,可以根据实际情况进行调整。

6.3 网络优化

网络请求是插件性能的主要瓶颈,SuSu 插件采用了多种技术优化网络性能。

6.3.1 请求头优化

为了模拟真实用户行为,减少被反爬机制拦截的可能性,插件实现了随机 User-Agent 和合理的 Referer 设置:

go
// 设置请求头
req.Header.Set("User-Agent", getRandomUA())
req.Header.Set("Referer", fmt.Sprintf("https://susuifa.com/%s.html", postID))

6.3.2 请求重试

为了处理临时网络问题,插件实现了请求重试机制,提高请求成功率:

go
// doRequestWithRetry 发送HTTP请求并支持重试
func (p *SusuAsyncPlugin) doRequestWithRetry(client *http.Client, req *http.Request, maxRetries int) (*http.Response, error) {
    // ... 重试逻辑 ...
}

6.3.3 早期过滤

为了减少不必要的网络请求,插件在早期阶段就过滤掉不相关的搜索结果:

go
// 预先过滤不包含关键词的帖子
doc.Find(".post-list-item").Each(func(i int, s *goquery.Selection) {
    // ... 过滤逻辑 ...
})

6.4 算法优化

算法优化是提高插件性能的重要手段,SuSu 插件在多个算法环节进行了优化。

6.4.1 字符串处理优化

字符串处理是插件中的常见操作,插件采用了高效的字符串处理方法:

  1. 使用 strings.Contains 代替正则表达式:在简单的字符串包含判断中,strings.Contains 比正则表达式更高效
  2. 使用 strings.ToLower 进行不区分大小写的比较:先转换为小写,再进行比较,避免复杂的正则表达式
  3. 使用 strings.TrimSpace 去除空白字符:简单高效,避免复杂的正则表达式

6.4.2 哈希函数优化

插件使用简化版的哈希函数生成缓存键,避免完整 MD5 或 SHA 算法的开销:

go
// md5sum 计算字符串的MD5值的简化版本
func md5sum(s string) uint32 {
    h := uint32(0)
    for i := 0; i < len(s); i++ {
        h = h*31 + uint32(s[i])
    }
    return h
}

这个简化版哈希函数在性能和冲突概率之间取得了良好的平衡。

错误处理与容错

7.1 错误处理策略

SuSu 插件采用了全面的错误处理策略,确保即使在出现错误的情况下,插件仍能提供有用的结果。

7.1.1 错误类型分类

插件将错误分为以下几类:

  1. 网络错误:如连接超时、连接重置等
  2. 解析错误:如 HTML 解析失败、JSON 解析失败等
  3. 业务错误:如未找到帖子 ID、未找到网盘按钮等
  4. 系统错误:如内存不足、权限不足等

7.1.2 错误处理原则

插件遵循以下错误处理原则:

  1. 早期检查:在操作开始前检查参数和条件,避免后续错误
  2. 优雅降级:在出现错误时,尽量返回部分结果,而不是完全失败
  3. 详细日志:记录错误的详细信息,便于调试和问题排查
  4. 错误包装:使用 fmt.Errorf 包装错误,保留错误上下文

7.1.3 错误处理示例

go
// 获取网盘链接
links, err := p.getLinks(client, postID)
if err != nil || len(links) == 0 {
    // 如果获取链接失败,仍然返回结果,但没有链接
    links = []model.Link{}
}

7.2 重试机制

为了处理临时网络问题,插件实现了重试机制,提高请求成功率。

7.2.1 可重试错误判断

插件实现了 isRetriableError 函数,判断错误是否可以重试:

go
// isRetriableError 判断错误是否可以重试
func isRetriableError(err error) bool {
    if err == nil {
        return false
    }
    
    // 判断是否是网络错误或超时错误
    if netErr, ok := err.(net.Error); ok {
        return netErr.Timeout() || netErr.Temporary()
    }
    
    // 其他可能需要重试的错误类型
    errStr := err.Error()
    return strings.Contains(errStr, "connection refused") ||
           strings.Contains(errStr, "connection reset") ||
           strings.Contains(errStr, "EOF")
}

7.2.2 指数退避

为了避免对服务器造成压力,插件使用指数退避算法控制重试间隔:

go
// 指数退避算法
backoff := time.Duration(1<<uint(i-1)) * 500 * time.Millisecond
if backoff > 5*time.Second {
    backoff = 5 * time.Second
}
time.Sleep(backoff)

7.3 资源管理

良好的资源管理是确保插件稳定运行的关键,SuSu 插件实现了严格的资源管理。

7.3.1 HTTP 连接管理

插件使用 defer 语句确保 HTTP 响应体被正确关闭,避免资源泄漏:

go
resp, err := p.doRequestWithRetry(client, req, MaxRetries)
if err != nil {
    return nil, fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()

7.3.2 goroutine 管理

插件使用 WaitGroup 确保所有 goroutine 都能正确退出,避免 goroutine 泄漏:

go
var wg sync.WaitGroup
// ...
wg.Add(1)
go func() {
    defer wg.Done()
    // ...
}()
// ...
wg.Wait()

7.3.3 内存管理

插件通过定期清理缓存,避免内存泄漏:

go
// startCacheCleaner 定期清理缓存
func startCacheCleaner() {
    ticker := time.NewTicker(1 * time.Hour)
    defer ticker.Stop()
    
    for range ticker.C {
        // 清空所有缓存
        // ...
    }
}

7.4 容错设计

容错设计是确保插件在面对各种异常情况时仍能正常工作的关键。

7.4.1 部分结果返回

即使在某些操作失败的情况下,插件仍然会返回部分结果:

go
// 获取网盘链接
links, err := p.getLinks(client, postID)
if err != nil || len(links) == 0 {
    // 如果获取链接失败,仍然返回结果,但没有链接
    links = []model.Link{}
}

7.4.2 多种提取方法

插件实现了多种提取方法,当一种方法失败时,可以尝试其他方法:

go
// 方法1:从列表项ID属性提取
itemID, exists := s.Attr("id")
if exists && strings.HasPrefix(itemID, "item-") {
    postID := strings.TrimPrefix(itemID, "item-")
    postIDCache.Store(cacheKey, postID)
    return postID
}

// 方法2:从详情页链接提取
href, exists := s.Find(".post-info h2 a").Attr("href")
if exists {
    re := regexp.MustCompile(`/(\d+)\.html`)
    matches := re.FindStringSubmatch(href)
    if len(matches) > 1 {
        postID := matches[1]
        postIDCache.Store(cacheKey, postID)
        return postID
    }
}

测试策略

8.1 测试类型

为了确保 SuSu 插件的质量和稳定性,需要进行多种类型的测试。

8.1.1 单元测试

单元测试主要测试插件的各个组件和函数的功能正确性,包括:

  1. JWT 解析测试:测试 decodeJWTURL 函数能否正确解析 JWT token
  2. 链接类型判断测试:测试 determineLinkType 函数能否正确判断链接类型
  3. 帖子 ID 提取测试:测试 extractPostID 函数能否正确提取帖子 ID
  4. 错误处理测试:测试各种错误情况下的处理是否正确

8.1.2 集成测试

集成测试主要测试插件的各个组件之间的交互是否正确,包括:

  1. 搜索流程测试:测试完整的搜索流程是否正常工作
  2. 缓存系统测试:测试缓存系统是否正常工作
  3. 并发处理测试:测试并发处理是否正常工作
  4. 错误传播测试:测试错误是否能够正确传播

8.1.3 性能测试

性能测试主要测试插件的性能指标是否满足要求,包括:

  1. 响应时间测试:测试插件的响应时间是否在可接受范围内
  2. 并发性能测试:测试插件在高并发情况下的性能
  3. 内存占用测试:测试插件的内存占用是否在可接受范围内
  4. CPU 占用测试:测试插件的 CPU 占用是否在可接受范围内

8.2 测试用例设计

8.2.1 单元测试用例

以 decodeJWTURL 函数为例,设计以下测试用例:

go
func TestDecodeJWTURL(t *testing.T) {
    plugin := NewSusuAsyncPlugin()
    
    // 测试用例1:正常的JWT token
    token1 := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvc3VzdWlmYS5jb20iLCJpYXQiOjE3NTMxNzgyMTYsIm5iZiI6MTc1MzE3ODIxNiwiZXhwIjoxNzUzMTc4NTE2LCJkYXRhIjp7InVybCI6Imh0dHBzOlwvXC9jYWl5dW4uMTM5LmNvbVwvbVwvaT8yalFYbXNmc01mRHUzIiwidXNlcl9pZCI6MCwicG9zdF9pZCI6IjE4ODkyIiwiaW5kZXgiOiIwIiwiaSI6IjAifX0.x14hn2sCcNC4WMZ9UAG8a89ldA8eHZ2Qw-dJsWqefog"
    expectedURL1 := "https://caiyun.139.com/m/i?2jQXmsfsMfDu3"
    
    url1, err1 := plugin.decodeJWTURL(token1)
    if err1 != nil {
        t.Errorf("解析正常JWT token失败: %v", err1)
    }
    if url1 != expectedURL1 {
        t.Errorf("解析结果不匹配,期望: %s, 实际: %s", expectedURL1, url1)
    }
    
    // 测试用例2:无效的JWT token
    token2 := "invalid-token"
    _, err2 := plugin.decodeJWTURL(token2)
    if err2 == nil {
        t.Error("解析无效JWT token应该返回错误,但没有")
    }
    
    // 测试用例3:缓存命中
    url3, err3 := plugin.decodeJWTURL(token1)
    if err3 != nil {
        t.Errorf("缓存命中解析失败: %v", err3)
    }
    if url3 != expectedURL1 {
        t.Errorf("缓存命中解析结果不匹配,期望: %s, 实际: %s", expectedURL1, url3)
    }
}

8.2.2 集成测试用例

以搜索流程为例,设计以下测试用例:

go
func TestSearch(t *testing.T) {
    plugin := NewSusuAsyncPlugin()
    
    // 测试用例1:正常搜索
    results1, err1 := plugin.Search("测试关键词", nil)
    if err1 != nil {
        t.Errorf("正常搜索失败: %v", err1)
    }
    if len(results1) == 0 {
        t.Error("正常搜索应该返回结果,但没有")
    }
    
    // 测试用例2:空关键词
    _, err2 := plugin.Search("", nil)
    if err2 == nil {
        t.Error("空关键词搜索应该返回错误,但没有")
    }
    
    // 测试用例3:特殊字符关键词
    results3, err3 := plugin.Search("测试!@#$%^&*()", nil)
    if err3 != nil {
        t.Errorf("特殊字符关键词搜索失败: %v", err3)
    }
    // 特殊字符关键词可能没有结果,所以不检查结果数量
}

8.2.3 性能测试用例

以响应时间测试为例,设计以下测试用例:

go
func BenchmarkSearch(b *testing.B) {
    plugin := NewSusuAsyncPlugin()
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _ = plugin.Search("测试关键词", nil)
    }
}

8.3 测试环境

8.3.1 单元测试环境

单元测试环境应该是隔离的,不依赖外部服务,可以使用 mock 对象模拟外部依赖。

8.3.2 集成测试环境

集成测试环境应该尽可能接近生产环境,但可以使用测试数据和测试服务。

8.3.3 性能测试环境

性能测试环境应该与生产环境配置相同,以获得准确的性能数据。

8.4 测试工具

8.4.1 单元测试工具

  • testing 包:Go 标准库中的测试框架
  • testify:提供断言和 mock 功能的测试库

8.4.2 集成测试工具

  • httptest:Go 标准库中的 HTTP 测试工具
  • gock:HTTP 请求 mock 工具

8.4.3 性能测试工具

  • testing.B:Go 标准库中的基准测试工具
  • pprof:Go 标准库中的性能分析工具

部署与集成

9.1 部署流程

SuSu 插件作为 PanSou 系统的一部分,其部署流程与整个系统紧密相关。

9.1.1 编译与打包

插件随 PanSou 系统一起编译和打包,不需要单独处理:

bash
# 在项目根目录下执行
go build -o pansou main.go

9.1.2 配置管理

插件的配置项可以通过环境变量或配置文件设置,主要配置项包括:

  • MaxRetries:最大重试次数,默认为 2
  • MaxConcurrency:最大并发数,默认为 50
  • CacheCleanInterval:缓存清理间隔,默认为 1 小时

9.1.3 依赖管理

插件依赖以下外部库:

  • github.com/PuerkitoBio/goquery:用于 HTML 解析
  • pansou/model:系统内部模型定义
  • pansou/plugin:插件系统基础库
  • pansou/util/json:JSON 处理工具

这些依赖通过 Go 的模块系统管理,确保版本一致性。

9.2 系统集成

SuSu 插件通过插件系统与 PanSou 系统集成,实现了松耦合的设计。

9.2.1 插件注册

插件在 init 函数中注册到全局插件管理器:

go
func init() {
    // 注册插件
    plugin.RegisterGlobalPlugin(NewSusuAsyncPlugin())
    
    // 启动缓存清理
    go startCacheCleaner()
    
    // 初始化随机数种子
    rand.Seed(time.Now().UnixNano())
}

9.2.2 接口实现

插件实现了 SearchPlugin 接口,与系统其他部分进行交互:

go
// SearchPlugin 接口
type SearchPlugin interface {
    Name() string
    Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
    Priority() int
}

9.2.3 数据流转

插件与系统之间的数据流转如下图所示:

┌───────────┐     ┌───────────┐     ┌───────────┐
│  用户请求  │     │  API 层   │     │ 服务层    │
│           ├────►│           ├────►│           │
└───────────┘     └───────────┘     └─────┬─────┘
                                          │
                                          ▼
                                    ┌───────────┐
                                    │ 插件管理器 │
                                    │           │
                                    └─────┬─────┘
                                          │
                  ┌───────────┐           │
                  │ 其他插件  │◄──────────┘
                  │           │           │
                  └───────────┘           ▼
                                    ┌───────────┐
                                    │ SuSu 插件  │
                                    │           │
                                    └───────────┘

图 9.1 系统集成数据流图

9.3 监控与运维

为了确保插件的稳定运行,需要进行监控和运维。

9.3.1 日志管理

插件使用系统的日志框架记录关键信息,包括:

  • 错误日志:记录各种错误情况
  • 警告日志:记录可能影响性能的情况
  • 信息日志:记录关键操作和状态变化
  • 调试日志:记录详细的调试信息

9.3.2 性能监控

可以通过以下指标监控插件的性能:

  • 响应时间:插件的搜索响应时间
  • 成功率:API 请求的成功率
  • 缓存命中率:各类缓存的命中率
  • 内存占用:插件的内存占用情况
  • CPU 占用:插件的 CPU 占用情况

9.3.3 故障排查

当插件出现问题时,可以通过以下步骤进行排查:

  1. 检查日志:查看错误日志,定位问题
  2. 检查网络:确认与 SuSu 网站的连接是否正常
  3. 检查资源:确认系统资源是否充足
  4. 检查配置:确认配置是否正确
  5. 检查代码:如果以上都正常,可能是代码问题,需要进行调试