plugin/susu/susu插件设计文档.md
本文档详细描述了 SuSu 搜索插件的设计与实现,旨在为开发者提供完整的技术参考。文档涵盖了插件的架构设计、核心组件、关键算法、性能优化策略以及错误处理机制等方面,以确保插件能够高效、稳定地运行。
SuSu 搜索插件是 PanSou 网盘搜索系统的一个重要组成部分,专门用于从 SuSu 网站(susuifa.com)搜索并提取网盘资源链接。该插件实现了高效的并发处理、智能缓存机制和可靠的错误处理,能够快速响应用户的搜索请求,并提供准确的搜索结果。
SuSu 网站是一个包含大量网盘资源的平台,用户需要能够快速搜索并获取这些资源的链接。主要需求包括:
在实现 SuSu 插件过程中,面临以下技术挑战:
基于需求和挑战,制定了以下设计目标:
SuSu 插件采用分层架构设计,主要包含以下几个层次:
┌─────────────────────────┐
│ SusuAsyncPlugin │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ 搜索处理层 │
│ (doSearch, extractPostID) │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ 链接获取层 │
│ (getLinks, getButtonDetail) │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ 工具支持层 │
│ (decodeJWTURL, determineLinkType) │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ 基础设施层 │
│ (缓存系统, HTTP客户端) │
└─────────────────────────┘
图 3.1 SuSu 插件架构图
SuSu 插件的完整工作流程如下图所示:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 接收 │ │ 搜索页面 │ │ 提取帖子 │ │ 获取网盘 │ │ 返回结果 │
│ 请求 ├────►│ HTML ├────►│ 信息 ├────►│ 链接 ├────►│ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 参数处理 │ │ 关键词过滤│ │ 并发处理 │ │ JWT解析 │ │ 缓存结果 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
图 3.2 SuSu 插件工作流程图
插件各组件之间的关系和数据流向如下图所示:
┌───────────────┐
│ Search() │
└───────┬───────┘
│
▼
┌───────────────┐
│ doSearch() │
└───────┬───────┘
│
┌────────────────┴────────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ extractPostID()│ │ getLinks() │
└───────────────┘ └───────┬───────┘
│
▼
┌───────────────┐
│getButtonDetail()│
└───────┬───────┘
│
┌────────────────┴────────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ decodeJWTURL() │ │determineLinkType()│
└───────────────┘ └───────────────┘
图 3.3 SuSu 插件组件关系图
插件实现了 SearchPlugin 接口,并基于 BaseAsyncPlugin 进行扩展:
// 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获取真实链接缓存系统是 SuSu 插件性能优化的关键部分,采用多级缓存设计,针对不同类型的操作设置独立缓存。
// 缓存相关变量
var (
// 帖子ID缓存
postIDCache = sync.Map{}
// 按钮列表缓存
buttonListCache = sync.Map{}
// 按钮详情缓存
buttonDetailCache = sync.Map{}
// JWT解析结果缓存
jwtDecodeCache = sync.Map{}
// 链接类型判断缓存
linkTypeCache = sync.Map{}
)
每个缓存的作用:
缓存系统包含定期清理机制,避免内存泄漏:
// 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 缓存系统工作流程图
HTTP 客户端负责与 SuSu 网站的 API 进行通信,包括请求构建、发送和响应处理。
为了模拟真实用户行为,避免被反爬虫机制识别,插件实现了随机 User-Agent 功能:
// 常用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))]
}
请求头设置示例:
// 设置请求头
req.Header.Set("User-Agent", getRandomUA())
req.Header.Set("Referer", "https://susuifa.com/")
为了处理临时网络问题,插件实现了带有指数退避算法的重试机制:
// 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 重试机制工作流程图
SuSu 插件使用 goroutine 和信号量实现并发控制,在提高处理效率的同时避免资源过度消耗。
使用带缓冲的通道作为信号量,限制并发数量:
// 创建信号量控制并发数
semaphore := make(chan struct{}, MaxConcurrency)
// 获取信号量
semaphore <- struct{}{}
defer func() { <-semaphore }()
使用 goroutine、WaitGroup 和通道实现并发处理:
// 提取搜索结果
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 并发处理模型工作流程图
为了减少不必要的网络请求,SuSu 插件在早期阶段就过滤掉不包含关键词的帖子:
// 将关键词转为小写,用于不区分大小写的比较
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 早期过滤机制工作流程图
SuSu 网站使用 JWT(JSON Web Token)加密网盘链接,插件需要解析 JWT 才能获取真实链接。
JWT 解析算法的步骤如下:
// 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
}
网盘链接类型判断是将提取的 URL 映射到标准网盘类型的过程。
链接类型判断算法的步骤如下:
// 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
}
从搜索结果 HTML 中提取帖子 ID 是获取网盘链接的第一步。
帖子 ID 提取算法的步骤如下:
// 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 ""
}
指数退避算法是一种重试策略,随着重试次数的增加,等待时间呈指数增长,避免对服务器造成压力。
指数退避算法的步骤如下:
// 指数退避算法
backoff := time.Duration(1<<uint(i-1)) * 500 * time.Millisecond
if backoff > 5*time.Second {
backoff = 5 * time.Second
}
time.Sleep(backoff)
缓存是提高插件性能的关键技术,SuSu 插件采用了多级缓存策略,针对不同操作设计了专用缓存。
下表展示了各类缓存的预期效果:
| 缓存类型 | 缓存命中率 | 性能提升 | 内存占用 |
|---|---|---|---|
| 帖子ID缓存 | 高 | 中 | 低 |
| 按钮列表缓存 | 中 | 高 | 中 |
| 按钮详情缓存 | 中 | 高 | 中 |
| JWT解析缓存 | 高 | 中 | 低 |
| 链接类型缓存 | 高 | 低 | 低 |
表 6.1 缓存效果分析表
缓存键设计是缓存系统的重要部分,良好的缓存键设计可以提高缓存命中率,减少内存占用。
并发处理是提高插件吞吐量的重要手段,SuSu 插件在多个环节采用了并发处理。
插件采用 goroutine + 信号量的并发模型,具有以下优势:
插件在两个关键环节实现了并发处理:
为了避免并发过度,插件实现了严格的并发控制:
// 创建信号量控制并发数
semaphore := make(chan struct{}, MaxConcurrency)
// 获取信号量
semaphore <- struct{}{}
defer func() { <-semaphore }()
并发数量通过常量 MaxConcurrency 控制,可以根据实际情况进行调整。
网络请求是插件性能的主要瓶颈,SuSu 插件采用了多种技术优化网络性能。
为了模拟真实用户行为,减少被反爬机制拦截的可能性,插件实现了随机 User-Agent 和合理的 Referer 设置:
// 设置请求头
req.Header.Set("User-Agent", getRandomUA())
req.Header.Set("Referer", fmt.Sprintf("https://susuifa.com/%s.html", postID))
为了处理临时网络问题,插件实现了请求重试机制,提高请求成功率:
// doRequestWithRetry 发送HTTP请求并支持重试
func (p *SusuAsyncPlugin) doRequestWithRetry(client *http.Client, req *http.Request, maxRetries int) (*http.Response, error) {
// ... 重试逻辑 ...
}
为了减少不必要的网络请求,插件在早期阶段就过滤掉不相关的搜索结果:
// 预先过滤不包含关键词的帖子
doc.Find(".post-list-item").Each(func(i int, s *goquery.Selection) {
// ... 过滤逻辑 ...
})
算法优化是提高插件性能的重要手段,SuSu 插件在多个算法环节进行了优化。
字符串处理是插件中的常见操作,插件采用了高效的字符串处理方法:
插件使用简化版的哈希函数生成缓存键,避免完整 MD5 或 SHA 算法的开销:
// 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
}
这个简化版哈希函数在性能和冲突概率之间取得了良好的平衡。
SuSu 插件采用了全面的错误处理策略,确保即使在出现错误的情况下,插件仍能提供有用的结果。
插件将错误分为以下几类:
插件遵循以下错误处理原则:
// 获取网盘链接
links, err := p.getLinks(client, postID)
if err != nil || len(links) == 0 {
// 如果获取链接失败,仍然返回结果,但没有链接
links = []model.Link{}
}
为了处理临时网络问题,插件实现了重试机制,提高请求成功率。
插件实现了 isRetriableError 函数,判断错误是否可以重试:
// 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")
}
为了避免对服务器造成压力,插件使用指数退避算法控制重试间隔:
// 指数退避算法
backoff := time.Duration(1<<uint(i-1)) * 500 * time.Millisecond
if backoff > 5*time.Second {
backoff = 5 * time.Second
}
time.Sleep(backoff)
良好的资源管理是确保插件稳定运行的关键,SuSu 插件实现了严格的资源管理。
插件使用 defer 语句确保 HTTP 响应体被正确关闭,避免资源泄漏:
resp, err := p.doRequestWithRetry(client, req, MaxRetries)
if err != nil {
return nil, fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()
插件使用 WaitGroup 确保所有 goroutine 都能正确退出,避免 goroutine 泄漏:
var wg sync.WaitGroup
// ...
wg.Add(1)
go func() {
defer wg.Done()
// ...
}()
// ...
wg.Wait()
插件通过定期清理缓存,避免内存泄漏:
// startCacheCleaner 定期清理缓存
func startCacheCleaner() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
// 清空所有缓存
// ...
}
}
容错设计是确保插件在面对各种异常情况时仍能正常工作的关键。
即使在某些操作失败的情况下,插件仍然会返回部分结果:
// 获取网盘链接
links, err := p.getLinks(client, postID)
if err != nil || len(links) == 0 {
// 如果获取链接失败,仍然返回结果,但没有链接
links = []model.Link{}
}
插件实现了多种提取方法,当一种方法失败时,可以尝试其他方法:
// 方法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
}
}
为了确保 SuSu 插件的质量和稳定性,需要进行多种类型的测试。
单元测试主要测试插件的各个组件和函数的功能正确性,包括:
集成测试主要测试插件的各个组件之间的交互是否正确,包括:
性能测试主要测试插件的性能指标是否满足要求,包括:
以 decodeJWTURL 函数为例,设计以下测试用例:
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)
}
}
以搜索流程为例,设计以下测试用例:
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)
}
// 特殊字符关键词可能没有结果,所以不检查结果数量
}
以响应时间测试为例,设计以下测试用例:
func BenchmarkSearch(b *testing.B) {
plugin := NewSusuAsyncPlugin()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = plugin.Search("测试关键词", nil)
}
}
单元测试环境应该是隔离的,不依赖外部服务,可以使用 mock 对象模拟外部依赖。
集成测试环境应该尽可能接近生产环境,但可以使用测试数据和测试服务。
性能测试环境应该与生产环境配置相同,以获得准确的性能数据。
SuSu 插件作为 PanSou 系统的一部分,其部署流程与整个系统紧密相关。
插件随 PanSou 系统一起编译和打包,不需要单独处理:
# 在项目根目录下执行
go build -o pansou main.go
插件的配置项可以通过环境变量或配置文件设置,主要配置项包括:
插件依赖以下外部库:
这些依赖通过 Go 的模块系统管理,确保版本一致性。
SuSu 插件通过插件系统与 PanSou 系统集成,实现了松耦合的设计。
插件在 init 函数中注册到全局插件管理器:
func init() {
// 注册插件
plugin.RegisterGlobalPlugin(NewSusuAsyncPlugin())
// 启动缓存清理
go startCacheCleaner()
// 初始化随机数种子
rand.Seed(time.Now().UnixNano())
}
插件实现了 SearchPlugin 接口,与系统其他部分进行交互:
// SearchPlugin 接口
type SearchPlugin interface {
Name() string
Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
Priority() int
}
插件与系统之间的数据流转如下图所示:
┌───────────┐ ┌───────────┐ ┌───────────┐
│ 用户请求 │ │ API 层 │ │ 服务层 │
│ ├────►│ ├────►│ │
└───────────┘ └───────────┘ └─────┬─────┘
│
▼
┌───────────┐
│ 插件管理器 │
│ │
└─────┬─────┘
│
┌───────────┐ │
│ 其他插件 │◄──────────┘
│ │ │
└───────────┘ ▼
┌───────────┐
│ SuSu 插件 │
│ │
└───────────┘
图 9.1 系统集成数据流图
为了确保插件的稳定运行,需要进行监控和运维。
插件使用系统的日志框架记录关键信息,包括:
可以通过以下指标监控插件的性能:
当插件出现问题时,可以通过以下步骤进行排查: