From 53b83a089924c50206ee05f96a2537363a730666 Mon Sep 17 00:00:00 2001 From: "www.xueximeng.com" Date: Wed, 30 Jul 2025 20:07:49 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=BC=93=E5=AD=98=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0,=E5=A4=A7=E5=B9=85=E4=BC=98=E5=8C=96=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 71 +- api/middleware.go | 17 +- config/config.go | 15 + docs/1-项目总体架构设计.md | 174 --- docs/2-API层设计.md | 482 ------- docs/3-服务层设计.md | 587 --------- docs/4-插件系统设计.md | 605 --------- docs/5-缓存系统设计.md | 1393 -------------------- docs/PanSou系统开发设计文档.md | 584 +++++++++ docs/插件开发指南.md | 1679 ++++-------------------- main.go | 3 +- model/plugin_result.go | 32 + plugin/baseasyncplugin.go | 702 +++++----- plugin/hdr4k/hdr4k.go | 13 +- plugin/hunhepan/hunhepan.go | 14 +- plugin/jikepan/jikepan.go | 14 +- plugin/pan666/pan666.go | 14 +- plugin/pansearch/pansearch.go | 123 +- plugin/panta/panta.go | 105 +- plugin/panyq/panyq.go | 13 +- plugin/plugin.go | 59 +- plugin/qupansou/qupansou.go | 69 +- plugin/susu/susu.go | 14 +- plugin/xuexizhinan/xuexizhinan.go | 14 +- service/search_service.go | 326 ++++- util/cache/enhanced_two_level_cache.go | 37 + util/cache/sharded_memory_cache.go | 38 +- 27 files changed, 1846 insertions(+), 5351 deletions(-) delete mode 100644 docs/1-项目总体架构设计.md delete mode 100644 docs/2-API层设计.md delete mode 100644 docs/3-服务层设计.md delete mode 100644 docs/4-插件系统设计.md delete mode 100644 docs/5-缓存系统设计.md create mode 100644 docs/PanSou系统开发设计文档.md create mode 100644 model/plugin_result.go diff --git a/README.md b/README.md index 497738e..ee1bae7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ PanSou是一个高性能的网盘资源搜索API服务,支持TG搜索和自定义插件搜索。系统设计以性能和可扩展性为核心,支持并发搜索、结果智能排序和网盘类型分类。 +## 🚀 性能表现 + +实测 PanSou 在 8GB MacBook Pro 上的性能表现: + +- ✅ **500用户瞬时并发**: 100%成功率,平均响应167ms +- ✅ **200用户持续并发**: 30秒内处理4725请求,QPS=148 +- ✅ **缓存命中**: 99.8%请求<100ms响应时间 +- ✅ **高可用性**: 长时间运行无故障 + + ## 特性 - **高性能搜索**:并发搜索多个Telegram频道,显著提升搜索速度;工作池设计,高效管理并发任务 @@ -120,36 +130,45 @@ cd pansou | HTTP_READ_TIMEOUT | HTTP读取超时时间(秒) | 自动计算,最小30 | | HTTP_WRITE_TIMEOUT | HTTP写入超时时间(秒) | 自动计算,最小60 | | HTTP_IDLE_TIMEOUT | HTTP空闲连接超时时间(秒) | 120 | -| HTTP_MAX_CONNS | HTTP最大并发连接数 | CPU核心数×200,最小1000 | +| HTTP_MAX_CONNS | HTTP最大并发连接数 | CPU核心数×25,最小100,最大500 | +| ASYNC_LOG_ENABLED | 异步插件详细日志开关 | true | +### 🔧 性能优化配置 + +为不同环境提供的优化配置方案: + +#### macOS/笔记本电脑配置 (推荐) ```bash -# 默认频道 -export CHANNELS="tgsearchers2,xxx" - -# 缓存配置 -export CACHE_ENABLED=true -export CACHE_PATH="./cache" -export CACHE_MAX_SIZE=100 # MB -export CACHE_TTL=60 # 分钟 -export SHARD_COUNT=8 # 分片数量 - -# 异步插件配置 -export ASYNC_PLUGIN_ENABLED=true -export ASYNC_RESPONSE_TIMEOUT=4 # 响应超时时间(秒) -export ASYNC_MAX_BACKGROUND_WORKERS=40 # 最大后台工作者数量 -export ASYNC_MAX_BACKGROUND_TASKS=200 # 最大后台任务数量 -export ASYNC_CACHE_TTL_HOURS=1 # 异步缓存有效期(小时) - -# HTTP服务器配置 -export HTTP_READ_TIMEOUT=30 # 读取超时时间(秒) -export HTTP_WRITE_TIMEOUT=60 # 写入超时时间(秒) -export HTTP_IDLE_TIMEOUT=120 # 空闲连接超时时间(秒) -export HTTP_MAX_CONNS=1600 # 最大并发连接数(8核CPU示例) - -# 代理配置(如需) -export PROXY="socks5://127.0.0.1:7890" +# 针对macOS系统线程限制优化的配置 +export HTTP_MAX_CONNS=200 # 降低连接数 +export ASYNC_MAX_BACKGROUND_WORKERS=15 # 减少工作者数量 +export ASYNC_MAX_BACKGROUND_TASKS=75 # 减少任务队列 +export CONCURRENCY=30 # 适中的并发数 +export ASYNC_LOG_ENABLED=false # 关闭详细日志 ``` +#### 服务器/云环境配置 +```bash +# 高性能服务器配置 +export HTTP_MAX_CONNS=500 # 更高的连接数 +export ASYNC_MAX_BACKGROUND_WORKERS=40 # 更多工作者 +export ASYNC_MAX_BACKGROUND_TASKS=200 # 更大任务队列 +export CONCURRENCY=50 # 高并发数 +export CACHE_MAX_SIZE=500 # 更大缓存 +export ASYNC_LOG_ENABLED=false # 关闭详细日志 +``` + +#### 资源受限环境配置 +```bash +# 低配置服务器或容器环境 +export HTTP_MAX_CONNS=100 # 最低连接数 +export ASYNC_MAX_BACKGROUND_WORKERS=8 # 最少工作者 +export ASYNC_MAX_BACKGROUND_TASKS=40 # 最小任务队列 +export CONCURRENCY=15 # 低并发数 +export CACHE_MAX_SIZE=50 # 较小缓存 +``` + + 3. 构建 ```linux diff --git a/api/middleware.go b/api/middleware.go index 61f3c69..165adf7 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -2,6 +2,8 @@ package api import ( "fmt" + "net/url" + "strings" "time" "github.com/gin-gonic/gin" @@ -44,6 +46,19 @@ func LoggerMiddleware() gin.HandlerFunc { // 请求路由 reqURI := c.Request.RequestURI + // 对于搜索API,尝试解码关键词以便更好地显示 + displayURI := reqURI + if strings.Contains(reqURI, "/api/search") && strings.Contains(reqURI, "kw=") { + if parsedURL, err := url.Parse(reqURI); err == nil { + if keyword := parsedURL.Query().Get("kw"); keyword != "" { + if decodedKeyword, err := url.QueryUnescape(keyword); err == nil { + // 替换原始URI中的编码关键词为解码后的关键词 + displayURI = strings.Replace(reqURI, "kw="+keyword, "kw="+decodedKeyword, 1) + } + } + } + } + // 状态码 statusCode := c.Writer.Status() @@ -53,6 +68,6 @@ func LoggerMiddleware() gin.HandlerFunc { // 日志格式 gin.DefaultWriter.Write([]byte( fmt.Sprintf("| %s | %s | %s | %d | %s\n", - clientIP, reqMethod, reqURI, statusCode, latencyTime.String()))) + clientIP, reqMethod, displayURI, statusCode, latencyTime.String()))) } } \ No newline at end of file diff --git a/config/config.go b/config/config.go index 22768db..600c06a 100644 --- a/config/config.go +++ b/config/config.go @@ -38,6 +38,7 @@ type Config struct { AsyncMaxBackgroundWorkers int // 最大后台工作者数量 AsyncMaxBackgroundTasks int // 最大后台任务数量 AsyncCacheTTLHours int // 异步缓存有效期(小时) + AsyncLogEnabled bool // 是否启用异步插件详细日志 // HTTP服务器配置 HTTPReadTimeout time.Duration // 读取超时 HTTPWriteTimeout time.Duration // 写入超时 @@ -81,6 +82,7 @@ func Init() { AsyncMaxBackgroundWorkers: getAsyncMaxBackgroundWorkers(), AsyncMaxBackgroundTasks: getAsyncMaxBackgroundTasks(), AsyncCacheTTLHours: getAsyncCacheTTLHours(), + AsyncLogEnabled: getAsyncLogEnabled(), // HTTP服务器配置 HTTPReadTimeout: getHTTPReadTimeout(), HTTPWriteTimeout: getHTTPWriteTimeout(), @@ -453,6 +455,19 @@ func getHTTPMaxConns() int { return maxConns } +// 从环境变量获取异步插件日志开关,如果未设置则使用默认值 +func getAsyncLogEnabled() bool { + logEnv := os.Getenv("ASYNC_LOG_ENABLED") + if logEnv == "" { + return true // 默认启用日志 + } + enabled, err := strconv.ParseBool(logEnv) + if err != nil { + return true // 解析失败时默认启用 + } + return enabled +} + // 应用GC设置 func applyGCSettings() { // 设置GC百分比 diff --git a/docs/1-项目总体架构设计.md b/docs/1-项目总体架构设计.md deleted file mode 100644 index 3110bb8..0000000 --- a/docs/1-项目总体架构设计.md +++ /dev/null @@ -1,174 +0,0 @@ -# PanSou 项目总体架构设计 - -## 1. 项目概述 - -PanSou是一个高性能的网盘资源搜索API服务,支持Telegram搜索和多种网盘搜索引擎。系统设计以性能和可扩展性为核心,支持多频道并发搜索、结果智能排序和网盘类型分类。 - -### 1.1 核心功能 - -- **多源搜索**:支持Telegram频道和多种网盘搜索引擎 -- **高性能并发**:通过工作池实现高效并发搜索 -- **智能排序**:基于时间和关键词权重的多级排序策略 -- **网盘类型分类**:自动识别多种网盘链接,按类型归类展示 -- **增强版两级缓存**:分片内存+分片磁盘缓存机制,大幅提升重复查询速度和并发性能 -- **插件系统**:支持通过插件扩展搜索来源 -- **异步插件缓存更新**:支持在不强制刷新的情况下获取异步插件的最新缓存数据 - -### 1.2 技术栈 - -- **编程语言**:Go -- **Web框架**:Gin -- **缓存**:自定义增强版两级缓存(分片内存+优化分片磁盘) -- **JSON处理**:sonic(高性能JSON库) -- **并发控制**:工作池模式 - -## 2. 系统架构 - -### 2.1 整体架构 - -PanSou采用模块化的分层架构设计,主要包括以下几个层次: - -``` -┌─────────────────────────┐ -│ API 层 │ -│ (路由、处理器、中间件) │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 服务层 │ -│ (搜索服务、缓存) │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ ┌─────────────────┐ -│ 插件系统 │◄───┤ 插件注册表 │ -│ (搜索插件、插件管理器) │ └─────────────────┘ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 工具层 │ -│ (缓存、HTTP客户端、工作池) │ -└─────────────────────────┘ -``` - -### 2.2 模块职责 - -#### 2.2.1 API层 - -- **路由管理**:定义API端点和路由规则 -- **请求处理**:处理HTTP请求,参数解析和验证 -- **响应生成**:生成标准化的JSON响应 -- **中间件**:跨域处理、日志记录、压缩等 -- **参数规范化**:统一处理不同形式但语义相同的参数 -- **扩展参数处理**:支持通过`ext`参数向插件传递自定义搜索参数 - -#### 2.2.2 服务层 - -- **搜索服务**:整合插件和Telegram搜索结果 -- **结果处理**:过滤、排序和分类搜索结果 -- **缓存管理**:管理搜索结果的缓存策略,支持TG和插件搜索的独立缓存 -- **缓存键生成**:基于所有影响结果的参数生成一致的缓存键 -- **主缓存注入**:将主缓存系统注入到异步插件中,实现统一的缓存更新 -- **参数传递**:将自定义参数从API层传递到插件层 - -#### 2.2.3 插件系统 - -- **插件接口**:定义统一的搜索插件接口 -- **插件管理**:管理插件的注册、获取和调用 -- **自动注册**:通过init函数实现插件自动注册 -- **高性能JSON处理**:使用sonic库优化JSON序列化/反序列化 -- **异步插件**:支持"尽快响应,持续处理"的异步搜索模式 -- **扩展参数支持**:通过`ext`参数支持插件自定义搜索参数 - -#### 2.2.4 工具层 - -- **缓存工具**:增强版两级缓存实现(内存+分片磁盘) -- **序列化器**:支持Gob和JSON双序列化方式 -- **HTTP客户端**:优化的HTTP客户端,支持代理 -- **工作池**:并发任务执行的工作池 -- **JSON工具**:高性能JSON处理工具 - -### 2.3 数据流 - -1. **请求接收**:API层接收搜索请求 -2. **参数处理**:解析、验证和规范化请求参数 -3. **缓存键生成**:分别为TG搜索和插件搜索生成独立的缓存键 -4. **缓存检查**:检查是否有缓存结果 -5. **缓存读取优化**:优先从内存读取数据 -6. **并发搜索**:如无缓存,并发执行搜索任务 -7. **结果处理**:过滤、排序和分类搜索结果 -8. **缓存存储**:将结果存入缓存 -9. **响应返回**:返回处理后的结果 - -## 3. 核心设计思想 - -### 3.1 高性能设计 - -- **并发搜索**:使用工作池模式实现高效并发 -- **两级缓存**:分片内存缓存提供快速访问,优化分片磁盘缓存提供持久存储和高并发支持 -- **异步操作**:非关键路径使用异步处理 -- **内存优化**:预分配策略、对象池化、GC参数优化 -- **高效序列化**:使用Gob序列化提高性能 -- **高效JSON处理**:使用sonic库替代标准库,提升序列化性能 - -### 3.2 可扩展性设计 - -- **插件系统**:通过统一接口和自动注册机制实现可扩展 -- **模块化**:清晰的模块边界和职责划分 -- **配置驱动**:通过环境变量实现灵活配置 -- **序列化器接口**:通过统一接口支持多种序列化方式 - -### 3.3 可靠性设计 - -- **超时控制**:搜索操作有严格的超时限制 -- **错误处理**:全面的错误捕获和处理 -- **优雅降级**:单个搜索源失败不影响整体结果 -- **缓存一致性**:确保相同语义的查询使用相同的缓存键 - -## 4. 代码组织结构 - -``` -pansou/ -├── api/ # API层 -│ ├── handler.go # 请求处理器 -│ ├── middleware.go # 中间件 -│ └── router.go # 路由定义 -├── config/ # 配置管理 -│ └── config.go # 配置定义和加载 -├── docs/ # 文档 -├── model/ # 数据模型 -│ ├── request.go # 请求模型 -│ └── response.go # 响应模型 -├── plugin/ # 插件系统 -│ ├── plugin.go # 插件接口和管理 -│ ├── baseasyncplugin.go # 异步插件基类实现 -│ ├── jikepan/ # 即刻盘插件 -│ ├── hunhepan/ # 混合盘插件 -│ ├── pansearch/ # 盘搜插件 -│ ├── qupansou/ # 趣盘搜插件 -│ ├── pan666/ # 盘666插件 -│ └── panta/ # PanTa插件 -├── service/ # 服务层 -│ └── search_service.go # 搜索服务 -├── util/ # 工具层 -│ ├── cache/ # 缓存工具 -│ │ ├── cache_key.go # 优化的缓存键生成 -│ │ ├── disk_cache.go # 磁盘缓存 -│ │ ├── sharded_disk_cache.go # 分片磁盘缓存 -│ │ ├── enhanced_two_level_cache.go # 增强版两级缓存 -│ │ ├── serializer.go # 序列化器接口 -│ │ └── utils.go # 缓存工具函数 -│ ├── compression.go # 压缩工具 -│ ├── convert.go # 类型转换工具 -│ ├── http_util.go # HTTP工具函数 -│ ├── json/ # 高性能JSON工具 -│ │ └── json.go # 基于sonic的JSON处理封装 -│ ├── parser_util.go # 解析工具 -│ ├── pool/ # 工作池 -│ │ ├── object_pool.go # 对象池 -│ │ └── worker_pool.go # 工作池实现 -│ └── regex_util.go # 正则表达式工具 -├── go.mod # Go模块定义 -├── go.sum # 依赖版本锁定 -├── main.go # 程序入口 -└── README.md # 项目说明 -``` diff --git a/docs/2-API层设计.md b/docs/2-API层设计.md deleted file mode 100644 index 8efd5b3..0000000 --- a/docs/2-API层设计.md +++ /dev/null @@ -1,482 +0,0 @@ -# PanSou API层设计详解 - -## 1. API层概述 - -API层是PanSou系统的外部接口层,负责处理来自客户端的HTTP请求,并返回适当的响应。该层采用Gin框架实现,主要包含路由定义、请求处理和中间件三个核心部分。 - -## 2. 目录结构 - -``` -pansou/api/ -├── handler.go # 请求处理器 -├── middleware.go # 中间件 -└── router.go # 路由定义 -``` - -## 3. 路由设计 - -### 3.1 路由定义(router.go) - -路由模块负责定义API端点和路由规则,将请求映射到相应的处理函数。 - -```go -// SetupRouter 设置路由 -func SetupRouter(searchService *service.SearchService) *gin.Engine { - // 设置搜索服务 - SetSearchService(searchService) - - // 设置为生产模式 - gin.SetMode(gin.ReleaseMode) - - // 创建默认路由 - r := gin.Default() - - // 添加中间件 - r.Use(CORSMiddleware()) - r.Use(LoggerMiddleware()) - r.Use(util.GzipMiddleware()) // 添加压缩中间件 - - // 定义API路由组 - api := r.Group("/api") - { - // 搜索接口 - 支持POST和GET两种方式 - api.POST("/search", SearchHandler) - api.GET("/search", SearchHandler) // 添加GET方式支持 - - // 健康检查接口 - api.GET("/health", func(c *gin.Context) { - pluginCount := 0 - if searchService != nil && searchService.GetPluginManager() != nil { - pluginCount = len(searchService.GetPluginManager().GetPlugins()) - } - - c.JSON(200, gin.H{ - "status": "ok", - "plugins_enabled": true, - "plugin_count": pluginCount, - }) - }) - } - - return r -} -``` - -### 3.2 路由设计思想 - -1. **RESTful API设计**:采用RESTful风格设计API,使用适当的HTTP方法和路径 -2. **路由分组**:使用路由组对API进行分类管理 -3. **灵活的请求方式**:搜索接口同时支持GET和POST请求,满足不同场景需求 -4. **健康检查**:提供健康检查接口,便于监控系统状态 - -## 4. 请求处理器 - -### 4.1 处理器实现(handler.go) - -处理器模块负责处理具体的业务逻辑,包括参数解析、验证、调用服务层和返回响应。 - -```go -// SearchHandler 搜索处理函数 -func SearchHandler(c *gin.Context) { - var req model.SearchRequest - var err error - - // 根据请求方法不同处理参数 - if c.Request.Method == http.MethodGet { - // GET方式:从URL参数获取 - // 获取keyword,必填参数 - keyword := c.Query("kw") - - // 处理channels参数,支持逗号分隔 - channelsStr := c.Query("channels") - var channels []string - // 只有当参数非空时才处理 - if channelsStr != "" && channelsStr != " " { - parts := strings.Split(channelsStr, ",") - for _, part := range parts { - trimmed := strings.TrimSpace(part) - if trimmed != "" { - channels = append(channels, trimmed) - } - } - } - - // 处理并发数 - concurrency := 0 - concStr := c.Query("conc") - if concStr != "" && concStr != " " { - concurrency = util.StringToInt(concStr) - } - - // 处理强制刷新 - forceRefresh := false - refreshStr := c.Query("refresh") - if refreshStr != "" && refreshStr != " " && refreshStr == "true" { - forceRefresh = true - } - - // 处理结果类型和来源类型 - resultType := c.Query("res") - if resultType == "" || resultType == " " { - resultType = "" // 使用默认值 - } - - sourceType := c.Query("src") - if sourceType == "" || sourceType == " " { - sourceType = "" // 使用默认值 - } - - // 处理plugins参数,支持逗号分隔 - pluginsStr := c.Query("plugins") - var plugins []string - // 只有当参数非空时才处理 - if pluginsStr != "" && pluginsStr != " " { - parts := strings.Split(pluginsStr, ",") - for _, part := range parts { - trimmed := strings.TrimSpace(part) - if trimmed != "" { - plugins = append(plugins, trimmed) - } - } - } - - // 处理ext参数,JSON格式 - var ext map[string]interface{} - extStr := c.Query("ext") - if extStr != "" && extStr != " " { - // 处理特殊情况:ext={} - if extStr == "{}" { - ext = make(map[string]interface{}) - } else { - if err := jsonutil.Unmarshal([]byte(extStr), &ext); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的ext参数格式: "+err.Error())) - return - } - } - } - // 确保ext不为nil - if ext == nil { - ext = make(map[string]interface{}) - } - - req = model.SearchRequest{ - Keyword: keyword, - Channels: channels, - Concurrency: concurrency, - ForceRefresh: forceRefresh, - ResultType: resultType, - SourceType: sourceType, - Plugins: plugins, - Ext: ext, - } - } else { - // POST方式:从请求体获取 - data, err := c.GetRawData() - if err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "读取请求数据失败: "+err.Error())) - return - } - - if err := jsonutil.Unmarshal(data, &req); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的请求参数: "+err.Error())) - return - } - } - - // 检查并设置默认值 - if len(req.Channels) == 0 { - req.Channels = config.AppConfig.DefaultChannels - } - - // 如果未指定结果类型,默认返回merge - if req.ResultType == "" { - req.ResultType = "merge" - } else if req.ResultType == "merge" { - // 将merge转换为merged_by_type,以兼容内部处理 - req.ResultType = "merged_by_type" - } - - // 如果未指定数据来源类型,默认为全部 - if req.SourceType == "" { - req.SourceType = "all" - } - - // 参数互斥逻辑:当src=tg时忽略plugins参数,当src=plugin时忽略channels参数 - if req.SourceType == "tg" { - req.Plugins = nil // 忽略plugins参数 - } else if req.SourceType == "plugin" { - req.Channels = nil // 忽略channels参数 - } - - // 执行搜索 - result, err := searchService.Search(req.Keyword, req.Channels, req.Concurrency, req.ForceRefresh, req.ResultType, req.SourceType, req.Plugins, req.Ext) - - if err != nil { - response := model.NewErrorResponse(500, "搜索失败: "+err.Error()) - jsonData, _ := jsonutil.Marshal(response) - c.Data(http.StatusInternalServerError, "application/json", jsonData) - return - } - - // 返回结果 - response := model.NewSuccessResponse(result) - jsonData, _ := jsonutil.Marshal(response) - c.Data(http.StatusOK, "application/json", jsonData) -} -``` - -### 4.2 处理器设计思想 - -1. **多种请求方式支持**:同时支持GET和POST请求,并针对不同请求方式采用不同的参数解析策略 -2. **参数规范化**:对输入参数进行清理和规范化处理,确保不同形式但语义相同的参数能够生成一致的缓存键 -3. **默认值处理**:为未提供的参数设置合理的默认值 -4. **参数互斥逻辑**:实现参数间的互斥关系,避免冲突 -5. **统一响应格式**:使用标准化的响应格式,包括成功和错误响应 -6. **高性能JSON处理**:使用优化的JSON库处理请求和响应 -7. **缓存一致性支持**:通过参数处理确保相同语义的查询能够命中相同的缓存 - -## 5. 中间件设计 - -### 5.1 中间件实现(middleware.go) - -中间件模块提供了跨域处理、日志记录等功能,用于处理请求前后的通用逻辑。 - -```go -// CORSMiddleware 跨域中间件 -func CORSMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(204) - return - } - - c.Next() - } -} - -// LoggerMiddleware 日志中间件 -func LoggerMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - // 开始时间 - startTime := time.Now() - - // 处理请求 - c.Next() - - // 结束时间 - endTime := time.Now() - - // 执行时间 - latencyTime := endTime.Sub(startTime) - - // 请求方式 - reqMethod := c.Request.Method - - // 请求路由 - reqURI := c.Request.RequestURI - - // 状态码 - statusCode := c.Writer.Status() - - // 请求IP - clientIP := c.ClientIP() - - // 日志格式 - gin.DefaultWriter.Write([]byte( - fmt.Sprintf("| %s | %s | %s | %d | %s\n", - clientIP, reqMethod, reqURI, statusCode, latencyTime.String()))) - } -} -``` - -### 5.2 中间件设计思想 - -1. **关注点分离**:将通用功能抽象为中间件,与业务逻辑分离 -2. **链式处理**:中间件可以按顺序组合,形成处理管道 -3. **前置/后置处理**:支持在请求处理前后执行逻辑 -4. **性能监控**:通过日志中间件记录请求处理时间,便于性能分析 - -## 6. API接口规范 - -### 6.1 搜索API - -**接口地址**:`/api/search` -**请求方法**:`POST` 或 `GET` -**Content-Type**:`application/json`(POST方法) - -#### POST请求参数 - -| 参数名 | 类型 | 必填 | 描述 | -|--------|------|------|------| -| kw | string | 是 | 搜索关键词 | -| channels | string[] | 否 | 搜索的频道列表,不提供则使用默认配置 | -| conc | number | 否 | 并发搜索数量,不提供则自动设置为频道数+插件数+10 | -| refresh | boolean | 否 | 强制刷新,不使用缓存,便于调试和获取最新数据 | -| res | string | 否 | 结果类型:all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type),默认为merge | -| src | string | 否 | 数据来源类型:all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件) | -| plugins | string[] | 否 | 指定搜索的插件列表,不指定则搜索全部插件 | -| ext | object | 否 | 扩展参数,用于传递给插件的自定义参数,如{"title_en":"English Title", "is_all":true} | - -#### GET请求参数 - -| 参数名 | 类型 | 必填 | 描述 | -|--------|------|------|------| -| kw | string | 是 | 搜索关键词 | -| channels | string | 否 | 搜索的频道列表,使用英文逗号分隔多个频道,不提供则使用默认配置 | -| conc | number | 否 | 并发搜索数量,不提供则自动设置为频道数+插件数+10 | -| refresh | boolean | 否 | 强制刷新,设置为"true"表示不使用缓存 | -| res | string | 否 | 结果类型:all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type),默认为merge | -| src | string | 否 | 数据来源类型:all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件) | -| plugins | string | 否 | 指定搜索的插件列表,使用英文逗号分隔多个插件名,不指定则搜索全部插件 | -| ext | string | 否 | JSON格式的扩展参数,用于传递给插件的自定义参数,如{"title_en":"English Title", "is_all":true} | - -#### 成功响应 - -```json -{ - "code": 0, - "message": "success", - "data": { - "total": 15, - "results": [ - { - "message_id": "12345", - "unique_id": "channel-12345", - "channel": "tgsearchers2", - "datetime": "2023-06-10T14:23:45Z", - "title": "速度与激情全集1-10", - "content": "速度与激情系列全集,1080P高清...", - "links": [ - { - "type": "baidu", - "url": "https://pan.baidu.com/s/1abcdef", - "password": "1234" - } - ], - "tags": ["电影", "合集"] - }, - // 更多结果... - ], - "merged_by_type": { - "baidu": [ - { - "url": "https://pan.baidu.com/s/1abcdef", - "password": "1234", - "note": "速度与激情全集1-10", - "datetime": "2023-06-10T14:23:45Z" - }, - // 更多百度网盘链接... - ], - "aliyun": [ - // 阿里云盘链接... - ] - // 更多网盘类型... - } - } -} -``` - -#### 错误响应 - -```json -{ - "code": 400, - "message": "关键词不能为空" -} -``` - -### 6.2 健康检查API - -**接口地址**:`/api/health` -**请求方法**:`GET` - -#### 成功响应 - -```json -{ - "status": "ok", - "plugins_enabled": true, - "plugin_count": 6, - "plugins": ["pansearch", "panta", "qupansou", "hunhepan", "jikepan", "pan666"], - "channels": ["tgsearchers2", "SharePanBaidu"] -} -``` - -## 7. 参数处理优化 - -### 7.1 GET请求参数处理 - -```go -// 处理plugins参数,支持逗号分隔 -var plugins []string -// 检查请求中是否存在plugins参数 -if c.Request.URL.Query().Has("plugins") { - pluginsStr := c.Query("plugins") - // 判断参数是否非空 - if pluginsStr != "" && pluginsStr != " " { - parts := strings.Split(pluginsStr, ",") - for _, part := range parts { - trimmed := strings.TrimSpace(part) - if trimmed != "" { - plugins = append(plugins, trimmed) - } - } - } -} else { - // 如果请求中不存在plugins参数,设置为nil - plugins = nil -} -``` - -### 7.2 扩展参数处理 - -```go -// 处理ext参数,JSON格式 -var ext map[string]interface{} -extStr := c.Query("ext") -if extStr != "" && extStr != " " { - // 处理特殊情况:ext={} - if extStr == "{}" { - ext = make(map[string]interface{}) - } else { - if err := jsonutil.Unmarshal([]byte(extStr), &ext); err != nil { - c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的ext参数格式: "+err.Error())) - return - } - } -} -// 确保ext不为nil -if ext == nil { - ext = make(map[string]interface{}) -} -``` - -### 7.3 参数互斥与规范化处理 - -```go -// 参数互斥逻辑:当src=tg时忽略plugins参数,当src=plugin时忽略channels参数 -if req.SourceType == "tg" { - req.Plugins = nil // 忽略plugins参数 -} else if req.SourceType == "plugin" { - req.Channels = nil // 忽略channels参数 -} else if req.SourceType == "all" { - // 对于all类型,如果plugins为空或不存在,统一设为nil - if req.Plugins == nil || len(req.Plugins) == 0 { - req.Plugins = nil - } -} -``` - -## 8. 性能优化措施 - -1. **高效参数处理**:对GET请求参数进行高效处理,避免不必要的字符串操作 -2. **高性能JSON库**:使用sonic高性能JSON库处理请求和响应 -3. **响应压缩**:通过GzipMiddleware实现响应压缩,减少传输数据量 -4. **避免内存分配**:合理使用预分配和对象池,减少内存分配和GC压力 -5. **直接写入响应体**:使用`c.Data`直接写入响应体,避免中间转换 -6. **精确参数检查**:使用`c.Request.URL.Query().Has()`检查参数是否存在,避免不必要的处理 -7. **参数统一处理**:对相同语义的不同形式参数进行统一处理,确保缓存一致性 diff --git a/docs/3-服务层设计.md b/docs/3-服务层设计.md deleted file mode 100644 index 4025df1..0000000 --- a/docs/3-服务层设计.md +++ /dev/null @@ -1,587 +0,0 @@ -# PanSou 服务层设计详解 - -## 1. 服务层概述 - -服务层是PanSou系统的核心业务逻辑层,负责整合不同来源的搜索结果,并进行过滤、排序和分类处理。该层是连接API层和插件系统的桥梁,实现了搜索功能的核心逻辑。 - -## 2. 目录结构 - -``` -pansou/service/ -└── search_service.go # 搜索服务实现 -``` - -## 3. 搜索服务设计 - -### 3.1 搜索服务结构 - -搜索服务是服务层的核心组件,负责协调不同来源的搜索操作,并处理搜索结果。 - -```go -// 全局缓存实例和缓存是否初始化标志 -var ( - twoLevelCache *cache.TwoLevelCache - enhancedTwoLevelCache *cache.EnhancedTwoLevelCache - cacheInitialized bool -) - -// 优先关键词列表 -var priorityKeywords = []string{"全", "合集", "系列", "完", "最新", "附"} - -// 初始化缓存 -func init() { - if config.AppConfig != nil && config.AppConfig.CacheEnabled { - var err error - // 优先使用增强版缓存 - enhancedTwoLevelCache, err = cache.NewEnhancedTwoLevelCache() - if err == nil { - cacheInitialized = true - return - } - - // 如果增强版缓存初始化失败,回退到原始缓存 - twoLevelCache, err = cache.NewTwoLevelCache() - if err == nil { - cacheInitialized = true - } - } -} - -// SearchService 搜索服务 -type SearchService struct{ - pluginManager *plugin.PluginManager -} - -// NewSearchService 创建搜索服务实例并确保缓存可用 -func NewSearchService(pluginManager *plugin.PluginManager) *SearchService { - // 检查缓存是否已初始化,如果未初始化则尝试重新初始化 - if !cacheInitialized && config.AppConfig != nil && config.AppConfig.CacheEnabled { - var err error - // 优先使用增强版缓存 - enhancedTwoLevelCache, err = cache.NewEnhancedTwoLevelCache() - if err == nil { - cacheInitialized = true - } else { - // 如果增强版缓存初始化失败,回退到原始缓存 - twoLevelCache, err = cache.NewTwoLevelCache() - if err == nil { - cacheInitialized = true - } - } - } - - return &SearchService{ - pluginManager: pluginManager, - } -} -``` - -### 3.2 搜索方法实现 - -搜索方法是搜索服务的核心方法,实现了搜索逻辑、缓存管理和结果处理。 - -```go -// Search 执行搜索 -func (s *SearchService) Search(keyword string, channels []string, concurrency int, forceRefresh bool, resultType string, sourceType string, plugins []string, ext map[string]interface{}) (model.SearchResponse, error) { - // 确保ext不为nil - if ext == nil { - ext = make(map[string]interface{}) - } - - // 参数预处理 - // 源类型标准化 - if sourceType == "" { - sourceType = "all" - } - - // 插件参数规范化处理 - if sourceType == "tg" { - // 对于只搜索Telegram的请求,忽略插件参数 - plugins = nil - } else if sourceType == "all" || sourceType == "plugin" { - // 检查是否为空列表或只包含空字符串 - if plugins == nil || len(plugins) == 0 { - plugins = nil - } else { - // 检查是否有非空元素 - hasNonEmpty := false - for _, p := range plugins { - if p != "" { - hasNonEmpty = true - break - } - } - - // 如果全是空字符串,视为未指定 - if !hasNonEmpty { - plugins = nil - } else { - // 检查是否包含所有插件 - allPlugins := s.pluginManager.GetPlugins() - allPluginNames := make([]string, 0, len(allPlugins)) - for _, p := range allPlugins { - allPluginNames = append(allPluginNames, strings.ToLower(p.Name())) - } - - // 创建请求的插件名称集合(忽略空字符串) - requestedPlugins := make([]string, 0, len(plugins)) - for _, p := range plugins { - if p != "" { - requestedPlugins = append(requestedPlugins, strings.ToLower(p)) - } - } - - // 如果请求的插件数量与所有插件数量相同,检查是否包含所有插件 - if len(requestedPlugins) == len(allPluginNames) { - // 创建映射以便快速查找 - pluginMap := make(map[string]bool) - for _, p := range requestedPlugins { - pluginMap[p] = true - } - - // 检查是否包含所有插件 - allIncluded := true - for _, name := range allPluginNames { - if !pluginMap[name] { - allIncluded = false - break - } - } - - // 如果包含所有插件,统一设为nil - if allIncluded { - plugins = nil - } - } - } - } - } - - // 并行获取TG搜索和插件搜索结果 - var tgResults []model.SearchResult - var pluginResults []model.SearchResult - - var wg sync.WaitGroup - var tgErr, pluginErr error - - // 如果需要搜索TG - if sourceType == "all" || sourceType == "tg" { - wg.Add(1) - go func() { - defer wg.Done() - tgResults, tgErr = s.searchTG(keyword, channels, forceRefresh) - }() - } - - // 如果需要搜索插件 - if sourceType == "all" || sourceType == "plugin" { - wg.Add(1) - go func() { - defer wg.Done() - // 对于插件搜索,我们总是希望获取最新的缓存数据 - // 因此,即使forceRefresh=false,我们也需要确保获取到最新的缓存 - pluginResults, pluginErr = s.searchPlugins(keyword, plugins, forceRefresh, concurrency, ext) - }() - } - - // 等待所有搜索完成 - wg.Wait() - - // 检查错误 - if tgErr != nil { - return model.SearchResponse{}, tgErr - } - if pluginErr != nil { - return model.SearchResponse{}, pluginErr - } - - // 合并结果 - allResults := mergeSearchResults(tgResults, pluginResults) - - // 过滤结果,确保标题包含搜索关键词 - filteredResults := filterResultsByKeyword(allResults, keyword) - - // 按照优化后的规则排序结果 - sortResultsByTimeAndKeywords(filteredResults) - - // 过滤结果,只保留有时间的结果或包含优先关键词的结果到Results中 - filteredForResults := make([]model.SearchResult, 0, len(filteredResults)) - for _, result := range filteredResults { - // 有时间的结果或包含优先关键词的结果保留在Results中 - if !result.Datetime.IsZero() || getKeywordPriority(result.Title) > 0 { - filteredForResults = append(filteredForResults, result) - } - } - - // 合并链接按网盘类型分组(使用所有过滤后的结果) - mergedLinks := mergeResultsByType(filteredResults) - - // 构建响应 - var total int - if resultType == "merged_by_type" { - // 计算所有类型链接的总数 - total = 0 - for _, links := range mergedLinks { - total += len(links) - } - } else { - // 只计算filteredForResults的数量 - total = len(filteredForResults) - } - - response := model.SearchResponse{ - Total: total, - Results: filteredForResults, // 使用进一步过滤的结果 - MergedByType: mergedLinks, - } - - // 根据resultType过滤返回结果 - return filterResponseByType(response, resultType), nil -} -``` - -### 3.3 分离的缓存键生成 - -为了支持TG搜索和插件搜索的独立缓存,系统实现了分离的缓存键生成函数: - -```go -// 在util/cache/cache_key.go中 - -// GenerateTGCacheKey 为TG搜索生成缓存键 -func GenerateTGCacheKey(keyword string, channels []string) string { - // 关键词标准化 - normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword)) - - // 获取频道列表哈希 - channelsHash := getChannelsHash(channels) - - // 生成TG搜索特定的缓存键 - keyStr := fmt.Sprintf("tg:%s:%s", normalizedKeyword, channelsHash) - hash := md5.Sum([]byte(keyStr)) - return hex.EncodeToString(hash[:]) -} - -// GeneratePluginCacheKey 为插件搜索生成缓存键 -func GeneratePluginCacheKey(keyword string, plugins []string) string { - // 关键词标准化 - normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword)) - - // 获取插件列表哈希 - pluginsHash := getPluginsHash(plugins) - - // 生成插件搜索特定的缓存键 - keyStr := fmt.Sprintf("plugin:%s:%s", normalizedKeyword, pluginsHash) - hash := md5.Sum([]byte(keyStr)) - return hex.EncodeToString(hash[:]) -} -``` - -### 3.4 TG搜索实现 - -TG搜索方法使用独立的缓存键,实现了TG搜索结果的缓存管理: - -```go -// searchTG 搜索TG频道 -func (s *SearchService) searchTG(keyword string, channels []string, forceRefresh bool) ([]model.SearchResult, error) { - // 生成缓存键 - cacheKey := cache.GenerateTGCacheKey(keyword, channels) - - // 如果未启用强制刷新,尝试从缓存获取结果 - if !forceRefresh && cacheInitialized && config.AppConfig.CacheEnabled { - var data []byte - var hit bool - var err error - - // 优先使用增强版缓存 - if enhancedTwoLevelCache != nil { - data, hit, err = enhancedTwoLevelCache.Get(cacheKey) - - if err == nil && hit { - var results []model.SearchResult - if err := enhancedTwoLevelCache.GetSerializer().Deserialize(data, &results); err == nil { - return results, nil - } - } - } else if twoLevelCache != nil { - data, hit, err = twoLevelCache.Get(cacheKey) - - if err == nil && hit { - var results []model.SearchResult - if err := cache.DeserializeWithPool(data, &results); err == nil { - return results, nil - } - } - } - } - - // 缓存未命中,执行实际搜索 - var results []model.SearchResult - - // 使用工作池并行搜索多个频道 - tasks := make([]pool.Task, 0, len(channels)) - - for _, channel := range channels { - ch := channel // 创建副本,避免闭包问题 - tasks = append(tasks, func() interface{} { - results, err := s.searchChannel(keyword, ch) - if err != nil { - return nil - } - return results - }) - } - - // 执行搜索任务并获取结果 - taskResults := pool.ExecuteBatchWithTimeout(tasks, len(channels), config.AppConfig.PluginTimeout) - - // 合并所有频道的结果 - for _, result := range taskResults { - if result != nil { - channelResults := result.([]model.SearchResult) - results = append(results, channelResults...) - } - } - - // 异步缓存结果 - if cacheInitialized && config.AppConfig.CacheEnabled { - go func(res []model.SearchResult) { - ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute - - // 优先使用增强版缓存 - if enhancedTwoLevelCache != nil { - data, err := enhancedTwoLevelCache.GetSerializer().Serialize(res) - if err != nil { - return - } - enhancedTwoLevelCache.Set(cacheKey, data, ttl) - } else if twoLevelCache != nil { - data, err := cache.SerializeWithPool(res) - if err != nil { - return - } - twoLevelCache.Set(cacheKey, data, ttl) - } - }(results) - } - - return results, nil -} -``` - -### 3.5 插件搜索实现 - -插件搜索方法使用独立的缓存键,并实现了优化的缓存读取策略,确保即使在不强制刷新的情况下也能获取最新的缓存数据: - -```go -// searchPlugins 搜索插件 -func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRefresh bool, concurrency int, ext map[string]interface{}) ([]model.SearchResult, error) { - // 确保ext不为nil - if ext == nil { - ext = make(map[string]interface{}) - } - - // 生成缓存键 - cacheKey := cache.GeneratePluginCacheKey(keyword, plugins) - - // 如果未启用强制刷新,尝试从缓存获取结果 - if !forceRefresh && cacheInitialized && config.AppConfig.CacheEnabled { - var data []byte - var hit bool - var err error - - // 优先使用增强版缓存 - if enhancedTwoLevelCache != nil { - // 使用Get方法,它会优先从磁盘读取数据 - data, hit, err = enhancedTwoLevelCache.Get(cacheKey) - - if err == nil && hit { - var results []model.SearchResult - if err := enhancedTwoLevelCache.GetSerializer().Deserialize(data, &results); err == nil { - // 返回缓存数据 - return results, nil - } - } - } else if twoLevelCache != nil { - data, hit, err = twoLevelCache.Get(cacheKey) - - if err == nil && hit { - var results []model.SearchResult - if err := cache.DeserializeWithPool(data, &results); err == nil { - // 返回缓存数据 - return results, nil - } - } - } - } - - // 缓存未命中或强制刷新,执行实际搜索 - // 获取所有可用插件 - var availablePlugins []plugin.SearchPlugin - if s.pluginManager != nil { - allPlugins := s.pluginManager.GetPlugins() - - // 确保plugins不为nil并且有非空元素 - hasPlugins := plugins != nil && len(plugins) > 0 - hasNonEmptyPlugin := false - - if hasPlugins { - for _, p := range plugins { - if p != "" { - hasNonEmptyPlugin = true - break - } - } - } - - // 只有当plugins数组包含非空元素时才进行过滤 - if hasPlugins && hasNonEmptyPlugin { - pluginMap := make(map[string]bool) - for _, p := range plugins { - if p != "" { // 忽略空字符串 - pluginMap[strings.ToLower(p)] = true - } - } - - for _, p := range allPlugins { - if pluginMap[strings.ToLower(p.Name())] { - availablePlugins = append(availablePlugins, p) - } - } - } else { - // 如果plugins为nil、空数组或只包含空字符串,视为未指定,使用所有插件 - availablePlugins = allPlugins - } - } - - // 控制并发数 - if concurrency <= 0 { - concurrency = len(availablePlugins) + 10 - if concurrency < 1 { - concurrency = 1 - } - } - - // 使用工作池执行并行搜索 - tasks := make([]pool.Task, 0, len(availablePlugins)) - for _, p := range availablePlugins { - plugin := p // 创建副本,避免闭包问题 - tasks = append(tasks, func() interface{} { - results, err := plugin.Search(keyword, ext) - if err != nil { - return nil - } - return results - }) - } - - // 执行搜索任务并获取结果 - results := pool.ExecuteBatchWithTimeout(tasks, concurrency, config.AppConfig.PluginTimeout) - - // 合并所有插件的结果 - var allResults []model.SearchResult - for _, result := range results { - if result != nil { - pluginResults := result.([]model.SearchResult) - allResults = append(allResults, pluginResults...) - } - } - - // 异步缓存结果 - if cacheInitialized && config.AppConfig.CacheEnabled { - go func(res []model.SearchResult) { - ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute - - // 优先使用增强版缓存 - if enhancedTwoLevelCache != nil { - data, err := enhancedTwoLevelCache.GetSerializer().Serialize(res) - if err != nil { - return - } - enhancedTwoLevelCache.Set(cacheKey, data, ttl) - } else if twoLevelCache != nil { - data, err := cache.SerializeWithPool(res) - if err != nil { - return - } - twoLevelCache.Set(cacheKey, data, ttl) - } - }(allResults) - } - - return allResults, nil -} -``` - -## 4. 结果处理 - -### 4.1 过滤和排序 - -搜索服务实现了多种结果处理功能,包括过滤、排序和分类: - -1. **关键词过滤**:确保结果标题包含搜索关键词 -2. **时间和关键词排序**:基于时间和关键词优先级的多级排序策略 -3. **网盘类型分类**:按网盘类型分组展示结果 - -### 4.2 合并结果 - -系统支持合并不同来源的搜索结果,并处理可能的重复: - -```go -// 合并搜索结果 -func mergeSearchResults(tgResults, pluginResults []model.SearchResult) []model.SearchResult { - // 预估合并后的结果数量 - totalSize := len(tgResults) + len(pluginResults) - if totalSize == 0 { - return []model.SearchResult{} - } - - // 创建结果映射,用于去重 - resultMap := make(map[string]model.SearchResult, totalSize) - - // 添加TG搜索结果 - for _, result := range tgResults { - resultMap[result.UniqueID] = result - } - - // 添加或更新插件搜索结果(如果有重复,保留较新的) - for _, result := range pluginResults { - if existing, ok := resultMap[result.UniqueID]; ok { - // 如果已存在,保留较新的 - if result.Datetime.After(existing.Datetime) { - resultMap[result.UniqueID] = result - } - } else { - resultMap[result.UniqueID] = result - } - } - - // 转换回切片 - mergedResults := make([]model.SearchResult, 0, len(resultMap)) - for _, result := range resultMap { - mergedResults = append(mergedResults, result) - } - - return mergedResults -} -``` - -## 5. 服务层优化 - -### 5.1 缓存系统优化 - -服务层通过以下方式优化了缓存系统: - -1. **分离缓存键**:为TG搜索和插件搜索生成独立的缓存键,实现独立更新 -2. **增强版两级缓存**:优先使用增强版两级缓存,失败时回退到原始缓存 -3. **优化的缓存读取策略**:优先从磁盘读取数据,确保获取最新结果 -4. **主缓存注入**:将主缓存系统注入到异步插件中,实现统一的缓存更新 -5. **异步缓存更新**:搜索结果异步写入缓存,不阻塞主流程 - -### 5.2 并发性能优化 - -服务层通过以下方式优化了并发性能: - -1. **并行搜索**:TG搜索和插件搜索并行执行 -2. **工作池**:使用工作池并行执行多个搜索任务 -3. **超时控制**:对搜索任务设置超时限制,避免长时间阻塞 -4. **异步操作**:非关键路径使用异步处理,如缓存写入 diff --git a/docs/4-插件系统设计.md b/docs/4-插件系统设计.md deleted file mode 100644 index 300c0c3..0000000 --- a/docs/4-插件系统设计.md +++ /dev/null @@ -1,605 +0,0 @@ -# PanSou 插件系统设计详解 - -## 1. 插件系统概述 - -插件系统是PanSou的核心特性之一,通过统一的接口和自动注册机制,实现了搜索源的可扩展性。该系统允许轻松添加新的网盘搜索插件,而无需修改主程序代码,使系统能够灵活适应不同的搜索需求。 - -## 2. 目录结构 - -``` -pansou/plugin/ -├── plugin.go # 插件接口和管理器定义 -├── baseasyncplugin.go # 异步插件基类实现 -├── jikepan/ # 即刻盘异步插件 -├── pan666/ # 盘666异步插件 -├── hunhepan/ # 混合盘异步插件 -├── pansearch/ # 盘搜插件 -├── qupansou/ # 趣盘搜插件 -├── xuexizhinan/ # 学习指南插件 -└── panta/ # PanTa插件 -``` - -## 3. 插件接口设计 - -### 3.1 插件接口定义 - -插件接口是所有搜索插件必须实现的接口,定义了插件的基本行为。 - -```go -// SearchPlugin 搜索插件接口 -type SearchPlugin interface { - // Name 返回插件名称 - Name() string - - // Search 执行搜索并返回结果 - // ext参数用于传递额外的搜索参数,插件可以根据需要使用或忽略 - Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) - - // Priority 返回插件优先级(可选,用于控制结果排序) - Priority() int -} -``` - -### 3.2 接口设计思想 - -1. **简单明确**:接口只定义了必要的方法,使插件开发简单明了 -2. **统一返回格式**:所有插件返回相同格式的搜索结果,便于统一处理 -3. **优先级控制**:通过Priority方法支持插件优先级,影响结果排序 -4. **错误处理**:Search方法返回error,便于处理搜索过程中的错误 - -## 4. 插件注册机制 - -### 4.1 全局注册表 - -插件系统使用全局注册表管理所有插件,通过init函数实现自动注册。 - -```go -// 全局插件注册表 -var ( - globalRegistry = make(map[string]SearchPlugin) - globalRegistryLock sync.RWMutex -) - -// RegisterGlobalPlugin 注册插件到全局注册表 -// 这个函数应该在每个插件的init函数中被调用 -func RegisterGlobalPlugin(plugin SearchPlugin) { - if plugin == nil { - return - } - - globalRegistryLock.Lock() - defer globalRegistryLock.Unlock() - - name := plugin.Name() - if name == "" { - return - } - - globalRegistry[name] = plugin -} - -// GetRegisteredPlugins 获取所有已注册的插件 -func GetRegisteredPlugins() []SearchPlugin { - globalRegistryLock.RLock() - defer globalRegistryLock.RUnlock() - - plugins := make([]SearchPlugin, 0, len(globalRegistry)) - for _, plugin := range globalRegistry { - plugins = append(plugins, plugin) - } - - return plugins -} -``` - -### 4.2 自动注册机制 - -每个插件通过init函数在程序启动时自动注册到全局注册表。 - -```go -// 插件实现示例(以jikepan为例) -package jikepan - -import ( - "pansou/model" - "pansou/plugin" - "pansou/util/json" // 使用优化的JSON库 -) - -// 确保JikePanPlugin实现了SearchPlugin接口 -var _ plugin.SearchPlugin = (*JikePanPlugin)(nil) - -// JikePanPlugin 即刻盘搜索插件 -type JikePanPlugin struct{} - -// init函数在包被导入时自动执行,用于注册插件 -func init() { - // 注册插件到全局注册表 - plugin.RegisterGlobalPlugin(&JikePanPlugin{}) -} - -// Name 返回插件名称 -func (p *JikePanPlugin) Name() string { - return "jikepan" -} - -// Search 执行搜索 -func (p *JikePanPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 实现搜索逻辑 - // ... - return results, nil -} - -// Priority 返回插件优先级 -func (p *JikePanPlugin) Priority() int { - return 5 // 优先级为5 -} -``` - -## 5. 异步插件系统 - -### 5.1 异步插件基类 - -为了解决某些插件响应时间长的问题,系统提供了BaseAsyncPlugin基类,实现了"尽快响应,持续处理"的异步模式。 - -```go -// BaseAsyncPlugin 基础异步插件结构 -type BaseAsyncPlugin struct { - name string // 插件名称 - priority int // 优先级 - client *http.Client // 短超时客户端 - backgroundClient *http.Client // 长超时客户端 - cacheTTL time.Duration // 缓存有效期 - mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数 - MainCacheKey string // 主缓存键,导出字段,由主程序设置 -} - -// NewBaseAsyncPlugin 创建基础异步插件 -func NewBaseAsyncPlugin(name string, priority int) *BaseAsyncPlugin { - // 确保异步插件已初始化 - if !initialized { - initAsyncPlugin() - } - - // 初始化配置和客户端 - // ... - - return &BaseAsyncPlugin{ - name: name, - priority: priority, - client: &http.Client{ - Timeout: responseTimeout, - }, - backgroundClient: &http.Client{ - Timeout: processingTimeout, - }, - cacheTTL: cacheTTL, - } -} - -// SetMainCacheUpdater 设置主缓存更新函数 -func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []byte, time.Duration) error) { - p.mainCacheUpdater = updater -} -``` - -### 5.2 异步搜索机制 - -异步插件的核心是AsyncSearch方法,它实现了以下功能: - -1. **缓存检查**:首先检查是否有缓存结果可用 -2. **双通道处理**:同时启动快速响应通道和后台处理通道 -3. **超时控制**:在响应超时时返回当前结果,后台继续处理 -4. **缓存更新**:后台处理完成后更新缓存,供后续查询使用 - -```go -// AsyncSearch 异步搜索基础方法 -func (p *BaseAsyncPlugin) AsyncSearch( - keyword string, - searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), - mainCacheKey string, // 主缓存key参数 - ext map[string]interface{}, // 扩展参数 -) ([]model.SearchResult, error) { - // 确保ext不为nil - if ext == nil { - ext = make(map[string]interface{}) - } - - now := time.Now() - - // 修改缓存键,确保包含插件名称 - pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword) - - // 检查缓存 - if cachedItems, ok := apiResponseCache.Load(pluginSpecificCacheKey); ok { - cachedResult := cachedItems.(cachedResponse) - - // 缓存完全有效(未过期且完整) - if time.Since(cachedResult.Timestamp) < p.cacheTTL && cachedResult.Complete { - recordCacheHit() - recordCacheAccess(pluginSpecificCacheKey) - - // 如果缓存接近过期(已用时间超过TTL的80%),在后台刷新缓存 - if time.Since(cachedResult.Timestamp) > (p.cacheTTL * 4 / 5) { - go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey) - } - - return cachedResult.Results, nil - } - - // 缓存已过期但有结果,启动后台刷新,同时返回旧结果 - if len(cachedResult.Results) > 0 { - recordCacheHit() - recordCacheAccess(pluginSpecificCacheKey) - - // 标记为部分过期 - if time.Since(cachedResult.Timestamp) >= p.cacheTTL { - // 在后台刷新缓存 - go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey) - } - - return cachedResult.Results, nil - } - } - - // 创建通道 - resultChan := make(chan []model.SearchResult, 1) - errorChan := make(chan error, 1) - doneChan := make(chan struct{}) - - // 启动后台处理 - go func() { - // 尝试获取工作槽 - if !acquireWorkerSlot() { - // 工作池已满,使用快速响应客户端直接处理 - results, err := searchFunc(p.client, keyword, ext) - // 处理结果... - - // 更新主缓存系统 - p.updateMainCache(mainCacheKey, results) - - return - } - defer releaseWorkerSlot() - - // 执行搜索 - results, err := searchFunc(p.backgroundClient, keyword, ext) - - // 检查是否已经响应 - select { - case <-doneChan: - // 已经响应,只更新缓存 - // ... - - // 更新主缓存系统 - p.updateMainCache(mainCacheKey, results) - default: - // 尚未响应,发送结果 - // ... - - // 更新主缓存系统 - p.updateMainCache(mainCacheKey, results) - } - }() - - // 等待响应超时或结果 - select { - case results := <-resultChan: - close(doneChan) - return results, nil - case err := <-errorChan: - close(doneChan) - return nil, err - case <-time.After(responseTimeout): - // 响应超时,返回空结果,后台继续处理 - // ... - return []model.SearchResult{}, nil - } -} -``` - -### 5.3 缓存键传递机制 - -为了确保异步插件和主缓存系统使用相同的缓存键,系统实现了缓存键传递机制: - -```go -// BaseAsyncPlugin 基础异步插件结构 -type BaseAsyncPlugin struct { - name string // 插件名称 - priority int // 优先级 - client *http.Client // 短超时客户端 - backgroundClient *http.Client // 长超时客户端 - cacheTTL time.Duration // 缓存有效期 - mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数 - MainCacheKey string // 主缓存键,导出字段,由主程序设置 -} - -// SetMainCacheKey 设置主缓存键 -func (p *BaseAsyncPlugin) SetMainCacheKey(key string) { - p.MainCacheKey = key -} -``` - -关键的优化点是: - -1. **主缓存键传递**:主程序通过`SetMainCacheKey`方法将缓存键传递给插件 -2. **统一缓存键**:插件直接使用传递的缓存键,而不是自己重新生成 -3. **避免缓存不一致**:确保异步插件和主缓存系统使用完全相同的缓存键 - -```go -// AsyncSearch 异步搜索基础方法 -func (p *BaseAsyncPlugin) AsyncSearch( - keyword string, - searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), - mainCacheKey string, // 主缓存key参数 - ext map[string]interface{}, // 扩展参数 -) ([]model.SearchResult, error) { - // ... - - // 更新主缓存系统 - p.updateMainCache(mainCacheKey, results) - - // ... -} -``` - -### 5.4 异步插件缓存机制 - -异步插件系统实现了高级缓存机制: - -1. **持久化存储**:缓存定期保存到磁盘,服务重启时自动加载 -2. **智能缓存管理**:基于访问频率、时间和热度的缓存淘汰策略 -3. **增量更新**:新旧结果智能合并,保留唯一标识符不同的结果 - -```go -// 缓存响应结构 -type cachedResponse struct { - Results []model.SearchResult - Timestamp time.Time - Complete bool - LastAccess time.Time - AccessCount int -} - -// 缓存保存示例 -func saveCacheToDisk() { - // 将内存缓存保存到磁盘 - // ... -} - -// 缓存加载示例 -func loadCacheFromDisk() { - // 从磁盘加载缓存到内存 - // ... -} -``` - -### 5.5 异步插件实现示例 - -```go -// MyAsyncPlugin 自定义异步搜索插件 -type MyAsyncPlugin struct { - *plugin.BaseAsyncPlugin -} - -// NewMyAsyncPlugin 创建自定义插件 -func NewMyAsyncPlugin() *MyAsyncPlugin { - return &MyAsyncPlugin{ - BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myasyncplugin", 5), - } -} - -// Search 实现搜索接口 -func (p *MyAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 使用保存的主缓存键,传递ext参数 - return p.BaseAsyncPlugin.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext) -} - -// doSearch 执行实际搜索 -func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 处理ext参数 - if ext != nil { - // 根据需要使用ext参数 - if customParam, ok := ext["custom_param"].(string); ok && customParam != "" { - // 使用自定义参数 - keyword = fmt.Sprintf("%s %s", keyword, customParam) - } - } - - // 实现搜索逻辑 - // ... - return results, nil -} -``` - -### 5.6 异步插件与主程序缓存协同 - -异步插件系统与主程序的缓存系统协同工作,实现了完整的缓存更新流程: - -1. **主缓存键传递**:主程序在调用异步插件时传递主缓存键 -2. **缓存键保存**:主程序通过`SetMainCacheKey`方法将缓存键保存到插件的`MainCacheKey`字段 -3. **直接使用**:插件在`Search`方法中直接使用`p.MainCacheKey` -4. **缓存更新**:异步插件在后台处理完成后,使用保存的主缓存键更新主缓存 -5. **缓存一致性**:确保异步插件缓存和主缓存保持一致 - -```go -// 主缓存注入示例 -func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCache *cache.EnhancedTwoLevelCache) { - // 如果缓存或插件管理器不可用,直接返回 - if mainCache == nil || pluginManager == nil { - return - } - - // 创建缓存更新函数 - cacheUpdater := func(key string, data []byte, ttl time.Duration) error { - return mainCache.Set(key, data, ttl) - } - - // 获取所有插件 - plugins := pluginManager.GetPlugins() - - // 遍历所有插件,找出异步插件 - for _, p := range plugins { - // 检查插件是否实现了SetMainCacheUpdater方法 - if asyncPlugin, ok := p.(interface{ SetMainCacheUpdater(func(string, []byte, time.Duration) error) }); ok { - // 注入缓存更新函数 - asyncPlugin.SetMainCacheUpdater(cacheUpdater) - } - } -} -``` - -这种协同机制确保了用户总是能获取到最新的搜索结果,同时保持系统的高性能和响应速度。 - -## 6. 插件管理器 - -### 6.1 插件管理器设计 - -插件管理器负责管理所有已注册的插件,提供统一的接口获取和使用插件。 - -```go -// PluginManager 插件管理器 -type PluginManager struct { - plugins []SearchPlugin - mutex sync.RWMutex -} - -// NewPluginManager 创建插件管理器 -func NewPluginManager() *PluginManager { - return &PluginManager{ - plugins: make([]SearchPlugin, 0), - } -} - -// LoadPlugins 加载所有已注册的插件 -func (m *PluginManager) LoadPlugins() { - m.mutex.Lock() - defer m.mutex.Unlock() - - // 获取所有已注册的插件 - m.plugins = GetRegisteredPlugins() - - // 按优先级排序 - sort.Slice(m.plugins, func(i, j int) bool { - return m.plugins[i].Priority() > m.plugins[j].Priority() - }) -} - -// GetPlugins 获取所有插件 -func (m *PluginManager) GetPlugins() []SearchPlugin { - m.mutex.RLock() - defer m.mutex.RUnlock() - - return m.plugins -} -``` - -### 6.2 插件管理器使用 - -```go -// 在main.go中初始化插件管理器 -pluginManager := plugin.NewPluginManager() -pluginManager.LoadPlugins() - -// 创建搜索服务,传入插件管理器 -searchService := service.NewSearchService(pluginManager) - -// 设置路由 -router := api.SetupRouter(searchService) -``` - -## 7. 插件系统优势 - -1. **可扩展性**:通过统一接口和自动注册机制,轻松添加新的搜索源 -2. **高性能**:异步插件模式提供快速响应和后台处理,提高用户体验 -3. **缓存优化**:完善的缓存机制,提高重复查询的响应速度 -4. **错误隔离**:单个插件的错误不会影响其他插件和整体系统 -5. **优先级控制**:通过插件优先级控制搜索结果的排序 - -## 8. 插件开发指南 - -### 8.1 同步插件开发 - -```go -package myplugin - -import ( - "pansou/model" - "pansou/plugin" -) - -// MyPlugin 自定义插件 -type MyPlugin struct{} - -// 注册插件 -func init() { - plugin.RegisterGlobalPlugin(&MyPlugin{}) -} - -// Name 返回插件名称 -func (p *MyPlugin) Name() string { - return "myplugin" -} - -// Search 执行搜索 -func (p *MyPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 处理ext参数 - if ext != nil { - // 根据需要使用ext参数 - if customParam, ok := ext["custom_param"].(string); ok && customParam != "" { - // 使用自定义参数 - keyword = fmt.Sprintf("%s %s", keyword, customParam) - } - } - - // 实现搜索逻辑 - // ... - return results, nil -} - -// Priority 返回插件优先级 -func (p *MyPlugin) Priority() int { - return 3 -} -``` - -### 8.2 异步插件开发 - -```go -package myasyncplugin - -import ( - "pansou/model" - "pansou/plugin" -) - -// MyAsyncPlugin 自定义异步插件 -type MyAsyncPlugin struct { - *plugin.BaseAsyncPlugin -} - -// 创建并注册插件 -func init() { - plugin.RegisterGlobalPlugin(NewMyAsyncPlugin()) -} - -// NewMyAsyncPlugin 创建异步插件 -func NewMyAsyncPlugin() *MyAsyncPlugin { - return &MyAsyncPlugin{ - BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myasyncplugin", 4), - } -} - -// Search 实现搜索接口 -func (p *MyAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 使用保存的主缓存键 - return p.BaseAsyncPlugin.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext) -} - -// doSearch 执行实际搜索 -func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 实现搜索逻辑 - // ... - return results, nil -} -``` \ No newline at end of file diff --git a/docs/5-缓存系统设计.md b/docs/5-缓存系统设计.md deleted file mode 100644 index eb9c50e..0000000 --- a/docs/5-缓存系统设计.md +++ /dev/null @@ -1,1393 +0,0 @@ -# PanSou 缓存系统设计详解 - -## 1. 缓存系统概述 - -缓存系统是PanSou性能优化的核心组件,通过增强版两级缓存(内存+分片磁盘)机制,大幅提升重复查询的响应速度。该系统采用分层设计,实现了高效的缓存存取和智能的缓存策略。 - -PanSou的缓存系统包括两个主要部分: -1. **通用缓存系统**:用于API响应和常规搜索结果缓存 -2. **异步插件缓存系统**:专为异步插件设计的高级缓存机制 - -### 1.1 设计思想概述 - -PanSou缓存系统的设计基于以下核心理念: - -1. **性能与新鲜度平衡**:在高性能和数据新鲜度之间寻求最佳平衡点 -2. **分层缓存策略**:采用多层次缓存架构,满足不同场景的需求 -3. **协同工作机制**:主缓存系统与异步插件缓存系统协同工作 -4. **自适应优化**:根据访问模式和数据特性自动调整缓存策略 -5. **资源高效利用**:智能管理内存和磁盘资源,避免浪费 - -### 1.2 目录结构 - -``` -pansou/util/cache/ -├── cache_key.go # 优化的缓存键生成 -├── memory_cache.go # 原始内存缓存实现 -├── sharded_memory_cache.go # 分片内存缓存 -├── disk_cache.go # 磁盘缓存实现 -├── sharded_disk_cache.go # 分片磁盘缓存 -├── enhanced_two_level_cache.go # 增强版两级缓存实现 -├── serializer.go # 序列化器接口 -└── utils.go # 缓存工具函数 - -pansou/plugin/ -├── baseasyncplugin.go # 异步插件缓存实现 - -pansou/util/json/ -└── json.go # 基于sonic的高性能JSON处理封装 -``` - -## 2. 缓存架构设计 - -### 2.1 增强版两级缓存架构 - -PanSou采用增强版两级缓存架构,包括分片内存缓存和分片磁盘缓存: - -``` -┌─────────────────────────┐ -│ 搜索请求 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 缓存键生成 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 分片内存缓存查询 │ -│ (基于哈希分片分布) │ -└───────────┬─────────────┘ - │ (未命中) -┌───────────▼─────────────┐ -│ 分片磁盘缓存查询 │ -│ (动态分片数,位运算) │ -└───────────┬─────────────┘ - │ (未命中) -┌───────────▼─────────────┐ -│ 执行搜索 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 更新分片内存缓存 │ -│ (原子操作优化) │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 异步更新分片磁盘缓存 │ -│ (并行分片写入) │ -└─────────────────────────┘ -``` - -### 2.2 缓存层次职责 - -1. **分片内存缓存 (ShardedMemoryCache)**: - - 基于CPU核心数动态分片(2×核心数,最少4,最多64) - - 每个分片独立的sync.RWMutex,避免锁竞争 - - 原子操作优化lastUsed时间戳更新 - - 位运算哈希分片分布,提供快速访问 - - 存储热点数据,减少磁盘I/O - - 并行分片清理机制 - -2. **分片磁盘缓存 (OptimizedShardedDiskCache)**: - - 动态分片数量(基于CPU核心数,最多32个分片) - - 位运算哈希算法优化分片路由性能 - - 分片数为2的幂,确保位运算掩码高效 - - 提供持久存储,在服务重启后保留缓存 - - 并行分片I/O操作,减少磁盘访问瓶颈 - - 独立分片锁机制,提高并发写入性能 - -3. **缓存协同机制**: - - 内存缓存同步更新,磁盘缓存异步更新 - - 缓存注入机制:主缓存的Set方法注入到异步插件 - - 分片策略一致性:内存和磁盘使用相同的哈希算法 - - 智能缓存回写:内存命中时更新磁盘缓存时间戳 - -### 2.3 为什么选择两级缓存架构? - -#### 2.3.1 传统单级缓存的局限性 - -传统的单级缓存(如纯内存缓存或纯磁盘缓存)存在以下局限性: - -1. **纯内存缓存**: - - 容量受限于可用内存 - - 服务重启后缓存丢失 - - 内存资源昂贵 - -2. **纯磁盘缓存**: - - 访问速度较慢 - - I/O操作可能成为瓶颈 - - 高并发场景下性能下降 - -#### 2.3.2 两级缓存的优势 - -PanSou采用的增强版两级缓存架构结合了内存缓存和磁盘缓存的优势: - -1. **内存缓存层**: - - 提供极速访问 - - 减少磁盘I/O操作 - - 缓存热点数据 - -2. **磁盘缓存层**: - - 提供持久存储 - - 支持更大的缓存容量 - - 服务重启后缓存依然有效 - -3. **协同工作**: - - 内存缓存作为磁盘缓存的"加速层" - - 磁盘缓存作为内存缓存的"持久层" - - 两层缓存互相补充,形成完整的缓存体系 - -### 2.4 异步插件缓存系统架构 - -异步插件缓存系统实现了"尽快响应,持续处理"的异步模式: - -``` -┌─────────────────────────┐ -│ 搜索请求 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 缓存键生成 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 缓存检查 │ -└───────────┬─────────────┘ - │ (未命中) -┌───────────┬─────────────┐ -│ 快速响应通道│ 后台处理通道 │ -└───────┬───┴─────────┬───┘ - │ │ -┌───────▼───────┐ ┌───▼───────────┐ -│ 响应超时返回 │ │ 完整处理 │ -└───────┬───────┘ └───┬───────────┘ - │ │ -┌───────▼───────┐ ┌───▼───────────┐ -│ 返回部分结果 │ │ 更新插件缓存 │ -└───────────────┘ └───┬───────────┘ - │ - ┌───▼───────────┐ - │ 更新主缓存 │ - └───────────────┘ -``` - -## 3. 核心组件与数据结构 - -### 3.1 核心组件概览 - -PanSou缓存系统由以下核心组件组成: - -1. **EnhancedTwoLevelCache**:增强版两级缓存,整合内存缓存和分片磁盘缓存 -2. **MemoryCache**:内存缓存,提供快速访问 -3. **ShardedDiskCache**:分片磁盘缓存,提供持久存储 -4. **DiskCache**:单个磁盘缓存分片 -5. **Serializer**:序列化器接口及其实现 -6. **BaseAsyncPlugin**:异步插件基类,实现异步搜索和缓存更新 - -这些组件通过精心设计的接口和协作机制,共同构成了PanSou的高性能缓存系统。 - -### 3.2 数据结构设计 - -#### 3.2.1 增强版两级缓存 - -```go -// EnhancedTwoLevelCache 增强版两级缓存 -type EnhancedTwoLevelCache struct { - memory *ShardedMemoryCache // 分片内存缓存 - disk *ShardedDiskCache // 分片磁盘缓存 - mutex sync.RWMutex // 读写锁 - serializer Serializer // 序列化器 -} -``` - -增强版两级缓存是整个缓存系统的核心,它整合了内存缓存和分片磁盘缓存,提供了统一的缓存接口。 - -#### 3.2.2 分片内存缓存 - -```go -// 分片内存缓存 -type ShardedMemoryCache struct { - shards []*memoryCacheShard // 分片数组 - shardMask uint32 // 分片掩码(用于快速取模) - maxItems int // 最大项目数 - maxSize int64 // 最大内存使用量 - itemsPerShard int // 每个分片的最大项目数 - sizePerShard int64 // 每个分片的最大内存使用量 -} - -// 单个分片 -type memoryCacheShard struct { - items map[string]*shardedMemoryCacheItem - mutex sync.RWMutex // 分片级读写锁 - currSize int64 // 当前分片内存使用量 -} - -// 分片内存缓存项 -type shardedMemoryCacheItem struct { - data []byte // 缓存数据 - expiry time.Time // 过期时间 - lastUsed int64 // 最后使用时间(原子操作) - lastModified time.Time // 最后修改时间 - size int // 数据大小 -} -``` - -分片内存缓存使用动态分片数量(基于CPU核心数×2),每个分片独立使用sync.RWMutex,避免全局锁竞争。关键优化包括: -- **原子操作**:lastUsed时间戳使用atomic.StoreInt64避免锁开销 -- **位运算哈希**:使用hash & shardMask快速确定分片索引 -- **并行清理**:每个分片独立进行过期清理 -- **LRU淘汰**:基于原子操作的lastUsed实现高效LRU淘汰策略 - -#### 3.2.3 分片磁盘缓存 - -```go -// ShardedDiskCache 分片磁盘缓存(优化版) -type ShardedDiskCache struct { - baseDir string // 基础目录 - shardCount int // 动态分片数量 - shardMask uint32 // 分片掩码(用于快速取模) - shards []*DiskCache // 分片数组 - maxSizeMB int // 最大磁盘使用量 - mutex sync.RWMutex // 读写锁 -} -``` - -分片磁盘缓存通过动态分片数量和位运算哈希算法优化并发性能: -- **动态分片数**:基于CPU核心数×2计算,最多32个分片,确保分片数为2的幂 -- **位运算哈希**:使用`hash & shardMask`替代`hash % shardCount`,提升路由性能 -- **并行I/O**:每个分片独立进行磁盘读写操作,减少I/O阻塞 -- **均匀分布**:通过FNV-1a哈希算法确保缓存键在各分片间均匀分布 -- **独立清理**:各分片并行执行过期数据清理,提高清理效率 - -#### 3.2.4 磁盘缓存 - -```go -// 磁盘缓存项元数据 -type diskCacheMetadata struct { - Key string `json:"key"` // 缓存键 - Expiry time.Time `json:"expiry"` // 过期时间 - LastUsed time.Time `json:"last_used"` // 最后使用时间 - Size int `json:"size"` // 数据大小 - LastModified time.Time `json:"last_modified"` // 最后修改时间 -} - -// DiskCache 磁盘缓存 -type DiskCache struct { - path string // 缓存路径 - maxSizeMB int // 最大磁盘使用量 - metadata map[string]*diskCacheMetadata // 元数据映射 - mutex sync.RWMutex // 读写锁 - currSize int64 // 当前磁盘使用量 -} -``` - -磁盘缓存负责将缓存数据持久化到磁盘,并维护缓存项的元数据。它实现了基于访问时间的淘汰策略,当磁盘使用量超过限制时,会淘汰最久未使用的项。 - -#### 3.2.5 序列化器 - -```go -// Serializer 序列化接口 -type Serializer interface { - Serialize(v interface{}) ([]byte, error) - Deserialize(data []byte, v interface{}) error -} - -// GobSerializer 使用gob进行序列化/反序列化 -type GobSerializer struct { - bufferPool sync.Pool -} - -// JSONSerializer 使用JSON进行序列化/反序列化 -type JSONSerializer struct { - // 使用sonic库优化性能 -} -``` - -序列化器负责将数据结构转换为字节数组,以便存储到缓存中。PanSou实现了两种序列化器:GobSerializer和JSONSerializer,前者性能更高,后者兼容性更好。 - -#### 3.2.6 异步插件缓存 - -```go -// 缓存相关变量 -var ( - // API响应缓存,键为关键词,值为缓存的响应 - apiResponseCache = sync.Map{} - - // 最后一次清理缓存的时间 - lastCacheCleanTime = time.Now() - - // 最后一次保存缓存的时间 - lastCacheSaveTime = time.Now() - - // 缓存保存锁,防止并发保存导致的竞态条件 - saveCacheLock sync.Mutex - - // 工作池相关变量 - backgroundWorkerPool chan struct{} - backgroundTasksCount int32 = 0 -) - -// 缓存响应结构 -type cachedResponse struct { - Results []model.SearchResult `json:"results"` // 搜索结果 - Timestamp time.Time `json:"timestamp"` // 缓存时间 - Complete bool `json:"complete"` // 是否完整 - LastAccess time.Time `json:"last_access"` // 最后访问时间 - AccessCount int `json:"access_count"`// 访问计数 -} - -// BaseAsyncPlugin 基础异步插件结构 -type BaseAsyncPlugin struct { - name string // 插件名称 - priority int // 优先级 - client *http.Client // 短超时客户端 - backgroundClient *http.Client // 长超时客户端 - cacheTTL time.Duration // 缓存有效期 - mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数 - MainCacheKey string // 主缓存键,导出字段,由主程序设置 -} -``` - -异步插件缓存是为异步插件设计的专用缓存系统,它实现了"尽快响应,持续处理"的异步模式。通过`mainCacheUpdater`函数,异步插件可以直接更新主缓存系统,确保缓存一致性。主程序通过`SetMainCacheKey`方法将缓存键传递给插件并存储在`MainCacheKey`字段中,插件直接使用这个字段而不再重新生成缓存键,避免了缓存键不一致的问题。 - -### 3.3 分片策略设计 - -#### 3.3.1 为什么需要分片? - -在高并发场景下,传统的单一磁盘缓存存在以下问题: - -1. **锁竞争**:多线程并发访问同一资源导致严重的锁竞争 -2. **I/O瓶颈**:单一文件系统目录下大量小文件导致I/O性能下降 -3. **扩展性限制**:难以横向扩展以支持更高的并发访问 - -#### 3.3.2 分片实现原理 - -分片磁盘缓存的核心是通过哈希算法将缓存键映射到不同的分片: - -```go -// 获取键对应的分片 -func (c *ShardedDiskCache) getShard(key string) *DiskCache { - // 计算哈希值决定分片 - h := fnv.New32a() - h.Write([]byte(key)) - shardIndex := int(h.Sum32()) % c.shardCount - return c.shards[shardIndex] -} -``` - -这种设计确保: - -1. **确定性映射**:相同的键总是映射到相同的分片 -2. **均匀分布**:缓存键均匀分布到各个分片 -3. **隔离故障**:单个分片的问题不会影响其他分片 - -### 3.4 并发控制与竞态条件处理 - -#### 3.4.1 互斥锁保护缓存保存 - -为了防止多个goroutine同时保存缓存导致的竞态条件,系统使用互斥锁保护`saveCacheToDisk`函数: - -```go -// 缓存保存锁,防止并发保存导致的竞态条件 -saveCacheLock sync.Mutex - -// saveCacheToDisk 将缓存保存到磁盘 -func saveCacheToDisk() { - // 使用互斥锁确保同一时间只有一个goroutine可以执行 - saveCacheLock.Lock() - defer saveCacheLock.Unlock() - - // ... 缓存保存逻辑 ... -} -``` - -这种设计确保: -1. 同一时间只有一个goroutine可以执行缓存保存操作 -2. 避免多个goroutine同时创建和重命名临时文件导致的冲突 -3. 防止缓存文件损坏或不一致 - -#### 3.4.2 随机延迟减少冲突 - -在后台刷新缓存时,系统添加了随机延迟,进一步减少并发冲突的可能性: - -```go -// refreshCacheInBackground 在后台刷新缓存 -func (p *BaseAsyncPlugin) refreshCacheInBackground(...) { - // ... 缓存刷新逻辑 ... - - // 添加随机延迟,避免多个goroutine同时调用saveCacheToDisk - time.Sleep(time.Duration(100+rand.Intn(500)) * time.Millisecond) - - // 更新缓存后立即触发保存 - go saveCacheToDisk() -} -``` - -这种随机延迟机制可以: -1. 错开多个goroutine的缓存保存时间 -2. 减少对互斥锁的竞争 -3. 提高系统在高并发场景下的稳定性 - -#### 3.4.3 sync.Map的无锁设计 - -异步插件缓存使用`sync.Map`存储缓存项,而不是普通的map加锁: - -```go -// API响应缓存,键为关键词,值为缓存的响应 -apiResponseCache = sync.Map{} -``` - -`sync.Map`的优势: -1. 针对读多写少的场景优化 -2. 无需显式加锁,减少锁竞争 -3. 支持并发读取和更新操作 - -### 3.5 关键数据流转 - -#### 3.5.1 缓存写入流程 - -``` -┌─────────────────────────┐ -│ 搜索结果生成 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 序列化数据 │ -└───────────┬─────────────┘ - │ -┌───────────┬──────────────┐ -│ 更新内存缓存│异步更新磁盘缓存│ -└───────────┴──────────────┘ -``` - -1. **搜索结果生成**:系统生成搜索结果 -2. **序列化数据**:使用序列化器将结果转换为字节数组 -3. **更新内存缓存**:立即更新内存缓存 -4. **异步更新磁盘缓存**:在后台异步更新磁盘缓存,不阻塞主流程 - -#### 3.5.2 缓存读取流程 - -``` -┌─────────────────────────┐ -│ 缓存键生成 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 检查内存缓存 │ -└───────────┬─────────────┘ - │ (未命中) -┌───────────▼─────────────┐ -│ 使用磁盘数据更新内存 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 反序列化数据 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 返回缓存结果 │ -└─────────────────────────┘ -``` - -1. **缓存键生成**:根据搜索参数生成缓存键 -2. **检查内存缓存**:查询内存缓存是否命中,命中则直接返回缓存结果 -3. **检查磁盘缓存**:查询磁盘缓存是否命中 -4. **更新内存缓存**:使用磁盘数据更新内存缓存 -5. **反序列化数据**:将字节数组转换回原始数据结构 -6. **返回结果**:返回缓存结果 - -这种优化的读取流程确保了系统总是能获取到最新的缓存数据,特别适合异步插件场景。 - -## 4. 缓存键设计 - -### 4.1 缓存键设计的挑战 - -缓存键设计是缓存系统的基础,但面临以下挑战: - -1. **语义等价性**:不同形式但语义相同的查询应使用相同的缓存键 -2. **参数敏感性**:缓存键需要包含所有影响结果的参数 -3. **长度与性能**:过长的缓存键会影响性能 -4. **一致性保证**:确保不同组件生成相同格式的缓存键 - -### 4.2 分离的缓存键生成 - -PanSou实现了分离的缓存键生成策略,为TG搜索和插件搜索生成独立的缓存键。 - -```go -// GenerateTGCacheKey 为TG搜索生成缓存键 -func GenerateTGCacheKey(keyword string, channels []string) string { - // 关键词标准化 - normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword)) - - // 获取频道列表哈希 - channelsHash := getChannelsHash(channels) - - // 生成TG搜索特定的缓存键 - keyStr := fmt.Sprintf("tg:%s:%s", normalizedKeyword, channelsHash) - hash := md5.Sum([]byte(keyStr)) - return hex.EncodeToString(hash[:]) -} - -// GeneratePluginCacheKey 为插件搜索生成缓存键 -func GeneratePluginCacheKey(keyword string, plugins []string) string { - // 关键词标准化 - normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword)) - - // 获取插件列表哈希 - pluginsHash := getPluginsHash(plugins) - - // 生成插件搜索特定的缓存键 - keyStr := fmt.Sprintf("plugin:%s:%s", normalizedKeyword, pluginsHash) - hash := md5.Sum([]byte(keyStr)) - return hex.EncodeToString(hash[:]) -} -``` - -### 4.3 缓存键设计策略 - -PanSou采用了以下缓存键设计策略: - -1. **参数标准化**: - - 关键词转小写并去除前后空格 - - 数组参数排序,确保顺序不同但内容相同的参数生成相同的键 - - 空参数和未指定参数的统一处理 - -2. **前缀区分**: - - TG搜索使用"tg:"前缀 - - 插件搜索使用"plugin:"前缀 - - 确保不同类型的搜索使用不同的缓存空间 - -3. **哈希处理**: - - 对完整的键字符串进行MD5哈希 - - 减小缓存键长度 - - 提高查找效率 - -4. **统一格式**: - - 异步插件和主缓存系统使用相同格式的缓存键 - - 确保缓存一致性 - -5. **扩展参数处理**: - - 扩展参数(ext)不参与缓存键生成,避免缓存爆炸问题 - -### 4.4 列表参数处理 - -```go -// 获取或计算插件哈希 -func getPluginsHash(plugins []string) string { - // 检查是否为空列表 - if plugins == nil || len(plugins) == 0 { - // 使用预计算的所有插件哈希 - if hash, ok := precomputedHashes.Load("all_plugins"); ok { - return hash.(string) - } - return allPluginsHash - } - - // 检查是否有空字符串元素 - hasNonEmptyPlugin := false - for _, p := range plugins { - if p != "" { - hasNonEmptyPlugin = true - break - } - } - - // 如果全是空字符串,也视为空列表 - if !hasNonEmptyPlugin { - if hash, ok := precomputedHashes.Load("all_plugins"); ok { - return hash.(string) - } - return allPluginsHash - } - - // 对于小型列表,直接使用字符串连接 - if len(plugins) < 5 { - pluginsCopy := make([]string, 0, len(plugins)) - for _, p := range plugins { - if p != "" { // 忽略空字符串 - pluginsCopy = append(pluginsCopy, p) - } - } - sort.Strings(pluginsCopy) - - // 检查是否有预计算的哈希 - key := strings.Join(pluginsCopy, ",") - if hash, ok := precomputedHashes.Load("plugins:"+key); ok { - return hash.(string) - } - - return strings.Join(pluginsCopy, ",") - } - - // 生成排序后的字符串用作键,忽略空字符串 - pluginsCopy := make([]string, 0, len(plugins)) - for _, p := range plugins { - if p != "" { // 忽略空字符串 - pluginsCopy = append(pluginsCopy, p) - } - } - sort.Strings(pluginsCopy) - key := strings.Join(pluginsCopy, ",") - - // 尝试从缓存获取 - if hash, ok := pluginHashCache.Load(key); ok { - return hash.(string) - } - - // 计算哈希 - hash := calculateListHash(pluginsCopy) - - // 存入缓存 - pluginHashCache.Store(key, hash) - return hash -} -``` - -### 4.5 预计算哈希优化 - -```go -// 初始化预计算的哈希值 -func init() { - // 预计算空列表的哈希值 - precomputedHashes.Store("empty_channels", "all") - - // 预计算常用的频道组合哈希值 - commonChannels := [][]string{ - {"dongman", "anime"}, - {"movie", "film"}, - {"music", "audio"}, - {"book", "ebook"}, - } - - for _, channels := range commonChannels { - key := strings.Join(channels, ",") - hash := calculateListHash(channels) - precomputedHashes.Store("channels:"+key, hash) - } - - // 预计算常用的插件组合哈希值 - commonPlugins := [][]string{ - {"pan666", "panta"}, - {"aliyun", "baidu"}, - } - - for _, plugins := range commonPlugins { - key := strings.Join(plugins, ",") - hash := calculateListHash(plugins) - precomputedHashes.Store("plugins:"+key, hash) - } - - // 预计算所有插件的哈希值 - allPlugins := plugin.GetRegisteredPlugins() - allPluginNames := make([]string, 0, len(allPlugins)) - for _, p := range allPlugins { - allPluginNames = append(allPluginNames, p.Name()) - } - sort.Strings(allPluginNames) - allPluginsHash = calculateListHash(allPluginNames) - precomputedHashes.Store("all_plugins", allPluginsHash) -} -``` - -## 5. 关键方法实现 - -### 5.1 增强版两级缓存实现 - -#### 5.1.1 创建增强版两级缓存 - -```go -// NewEnhancedTwoLevelCache 创建新的增强版两级缓存 -func NewEnhancedTwoLevelCache() (*EnhancedTwoLevelCache, error) { - // 内存缓存大小为磁盘缓存的60% - memCacheMaxItems := 5000 - memCacheSizeMB := config.AppConfig.CacheMaxSizeMB * 3 / 5 - - memCache := NewShardedMemoryCache(memCacheMaxItems, memCacheSizeMB) - memCache.StartCleanupTask() - - // 创建优化的分片磁盘缓存,使用动态分片数量 - diskCache, err := NewOptimizedShardedDiskCache(config.AppConfig.CachePath, config.AppConfig.CacheMaxSizeMB) - if err != nil { - return nil, err - } - - // 创建序列化器 - serializer := NewGobSerializer() - - return &EnhancedTwoLevelCache{ - memory: memCache, - disk: diskCache, - serializer: serializer, - }, nil -} -``` - -#### 5.1.2 优化的缓存读取策略 - -```go -// Get 获取缓存 -func (c *EnhancedTwoLevelCache) Get(key string) ([]byte, bool, error) { - - // 检查内存缓存 - data, _, memHit := c.memory.GetWithTimestamp(key) - if memHit { - return data, true, nil - } - - // 尝试从磁盘读取数据 - diskData, diskHit, diskErr := c.disk.Get(key) - if diskErr == nil && diskHit { - // 磁盘缓存命中,更新内存缓存 - diskLastModified, _ := c.disk.GetLastModified(key) - ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute - c.memory.SetWithTimestamp(key, diskData, ttl, diskLastModified) - return diskData, true, nil - } - - return nil, false, nil -} -``` - -#### 5.1.3 异步缓存写入 - -```go -// Set 设置缓存 -func (c *EnhancedTwoLevelCache) Set(key string, data []byte, ttl time.Duration) error { - // 获取当前时间作为最后修改时间 - now := time.Now() - - // 先设置内存缓存(这是快速操作,直接在当前goroutine中执行) - c.memory.SetWithTimestamp(key, data, ttl, now) - - // 异步设置磁盘缓存(这是IO操作,可能较慢) - go func(k string, d []byte, t time.Duration) { - // 使用独立的goroutine写入磁盘,避免阻塞调用者 - _ = c.disk.Set(k, d, t) - }(key, data, ttl) - - return nil -} -``` - -#### 5.1.4 缓存删除 - -```go -// Delete 删除缓存 -func (c *EnhancedTwoLevelCache) Delete(key string) error { - // 从内存缓存删除 - c.memory.mutex.Lock() - if item, exists := c.memory.items[key]; exists { - c.memory.currSize -= int64(item.size) - delete(c.memory.items, key) - } - c.memory.mutex.Unlock() - - // 从磁盘缓存删除 - return c.disk.Delete(key) -} -``` - -### 5.2 内存缓存实现 - -#### 5.2.1 LRU淘汰策略 - -```go -// 驱逐策略 - LRU -func (c *MemoryCache) evict() { - // 找出最久未使用的项 - var oldestKey string - var oldestTime time.Time - - // 初始化为当前时间 - oldestTime = time.Now() - - for k, v := range c.items { - if v.lastUsed.Before(oldestTime) { - oldestKey = k - oldestTime = v.lastUsed - } - } - - // 如果找到了最久未使用的项,删除它 - if oldestKey != "" { - item := c.items[oldestKey] - c.currSize -= int64(item.size) - delete(c.items, oldestKey) - } -} -``` - -#### 5.2.2 带时间戳的缓存设置 - -```go -// SetWithTimestamp 设置缓存,并指定最后修改时间 -func (c *MemoryCache) SetWithTimestamp(key string, data []byte, ttl time.Duration, lastModified time.Time) { - c.mutex.Lock() - defer c.mutex.Unlock() - - // 如果已存在,先减去旧项的大小 - if item, exists := c.items[key]; exists { - c.currSize -= int64(item.size) - } - - // 创建新的缓存项 - now := time.Now() - item := &memoryCacheItem{ - data: data, - expiry: now.Add(ttl), - lastUsed: now, - lastModified: lastModified, - size: len(data), - } - - // 检查是否需要清理空间 - if len(c.items) >= c.maxItems || c.currSize+int64(len(data)) > c.maxSize { - c.evict() - } - - // 存储新项 - c.items[key] = item - c.currSize += int64(len(data)) -} -``` - -#### 5.2.3 定期清理过期项 - -```go -// 清理过期项 -func (c *MemoryCache) CleanExpired() { - c.mutex.Lock() - defer c.mutex.Unlock() - - now := time.Now() - for k, v := range c.items { - if now.After(v.expiry) { - c.currSize -= int64(v.size) - delete(c.items, k) - } - } -} - -// 启动定期清理 -func (c *MemoryCache) StartCleanupTask() { - ticker := time.NewTicker(5 * time.Minute) - go func() { - for range ticker.C { - c.CleanExpired() - } - }() -} -``` - -### 5.3 分片磁盘缓存实现 - -#### 5.3.1 分片选择 - -```go -// 获取键对应的分片 -func (c *ShardedDiskCache) getShard(key string) *DiskCache { - // 计算哈希值决定分片 - h := fnv.New32a() - h.Write([]byte(key)) - shardIndex := int(h.Sum32()) % c.shardCount - return c.shards[shardIndex] -} -``` - -#### 5.3.2 分片操作代理 - -```go -// Set 设置缓存 -func (c *ShardedDiskCache) Set(key string, data []byte, ttl time.Duration) error { - shard := c.getShard(key) - return shard.Set(key, data, ttl) -} - -// Get 获取缓存 -func (c *ShardedDiskCache) Get(key string) ([]byte, bool, error) { - shard := c.getShard(key) - return shard.Get(key) -} - -// Delete 删除缓存 -func (c *ShardedDiskCache) Delete(key string) error { - shard := c.getShard(key) - return shard.Delete(key) -} -``` - -## 6. 性能优化 - -### 6.1 内存优化 - -#### 6.1.1 对象池 - -对象池是一种重用对象的技术,可以减少内存分配和垃圾回收的压力。PanSou在序列化操作中使用对象池优化性能: - -```go -// GobSerializer 使用gob进行序列化/反序列化 -type GobSerializer struct { - bufferPool sync.Pool -} - -// NewGobSerializer 创建新的Gob序列化器 -func NewGobSerializer() *GobSerializer { - return &GobSerializer{ - bufferPool: sync.Pool{ - New: func() interface{} { - return new(bytes.Buffer) - }, - }, - } -} - -// Serialize 序列化 -func (s *GobSerializer) Serialize(v interface{}) ([]byte, error) { - buf := s.bufferPool.Get().(*bytes.Buffer) - buf.Reset() - defer s.bufferPool.Put(buf) - - // 使用缓冲区进行序列化 - enc := gob.NewEncoder(buf) - if err := enc.Encode(v); err != nil { - return nil, err - } - - // 复制缓冲区数据,因为buf会被重用 - data := make([]byte, buf.Len()) - copy(data, buf.Bytes()) - - return data, nil -} -``` - -这种优化减少了每次序列化操作时创建新缓冲区的开销,特别是在高并发场景下,可以显著降低GC压力。 - -#### 6.1.2 预分配容量 - -PanSou在创建map、slice等数据结构时,会预先分配合适的容量,避免动态扩容带来的性能损失: - -```go -// 预估合并后的结果数量 -totalSize := len(tgResults) + len(pluginResults) -if totalSize == 0 { - return []model.SearchResult{} -} - -// 创建结果映射,用于去重,预分配容量 -resultMap := make(map[string]model.SearchResult, totalSize) -``` - -#### 6.1.3 零拷贝技术 - -在某些场景下,PanSou使用零拷贝技术避免不必要的内存拷贝: - -```go -// 使用指针传递大型数据结构 -func (c *EnhancedTwoLevelCache) GetSerializer() Serializer { - c.mutex.RLock() - defer c.mutex.RUnlock() - return c.serializer -} -``` - -### 6.2 并发优化 - -#### 6.2.1 分片锁 - -PanSou使用分片锁技术减少锁竞争,提高并发性能。每个分片有自己的锁,不同分片的操作可以并行执行,减少了锁竞争。 - -#### 6.2.2 读写锁 - -PanSou在缓存操作中使用读写锁,允许多个读操作并发执行: - -```go -// Get 获取缓存 -func (c *MemoryCache) Get(key string) ([]byte, bool) { - c.mutex.RLock() // 使用读锁 - item, exists := c.items[key] - c.mutex.RUnlock() - - if !exists { - return nil, false - } - - // 检查是否过期 - if time.Now().After(item.expiry) { - c.mutex.Lock() // 使用写锁 - delete(c.items, key) - c.currSize -= int64(item.size) - c.mutex.Unlock() - return nil, false - } - - // 更新最后使用时间 - c.mutex.Lock() - item.lastUsed = time.Now() - c.mutex.Unlock() - - return item.data, true -} -``` - -#### 6.2.3 异步写入 - -PanSou使用异步写入策略,避免磁盘I/O操作阻塞主流程,提高了系统的响应速度。 - -### 6.3 I/O优化 - -#### 6.3.1 批量操作 - -PanSou在适当的场景下使用批量操作,减少系统调用的次数: - -```go -// 批量执行搜索任务 -results := pool.ExecuteBatchWithTimeout(tasks, concurrency, config.AppConfig.PluginTimeout) -``` - -#### 6.3.2 延迟写入 - -PanSou使用延迟写入策略,减少磁盘写入次数: - -```go -// 启动定期保存缓存到磁盘的goroutine -func startCachePersistence() { - // 每2分钟保存一次缓存 - ticker := time.NewTicker(cacheSaveInterval) - defer ticker.Stop() - - for range ticker.C { - // 检查是否有缓存项需要保存 - if hasCacheItems() { - saveCacheToDisk() - } - } -} -``` - -#### 6.3.3 文件系统优化 - -PanSou通过分片存储和合理的文件命名策略,优化了文件系统的性能: - -```go -// 获取文件名 -func (c *DiskCache) getFilename(key string) string { - hash := md5.Sum([]byte(key)) - return hex.EncodeToString(hash[:]) -} -``` - -### 6.4 性能指标 - -在实际应用中,PanSou缓存系统展现出了卓越的性能: - -| 指标 | 数值 | 说明 | -|------|------|------| -| 缓存命中率 | >95% | 大部分请求可以直接从缓存获取结果 | -| 内存缓存访问时间 | <1ms | 内存缓存提供极速访问 | -| 磁盘缓存访问时间 | <10ms | 分片磁盘缓存提供快速访问 | -| 缓存写入时间 | <5ms | 异步写入策略提高写入性能 | -| 并发处理能力 | >1000 QPS | 系统可以处理高并发请求 | - -## 7. 多次搜索流程 - -### 7.1 首次搜索(缓存未命中) - -当用户首次执行搜索时,系统会经历以下步骤: - -``` -┌─────────────────────────┐ -│ 用户搜索请求 │ -│ (关键词:"电影") │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 生成缓存键 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 检查内存缓存(未命中) │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 检查磁盘缓存(未命中) │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 并行执行搜索 │ -│ (TG搜索 + 插件搜索) │ -└───────────┬─────────────┘ - │ -┌───────────┬─────────────┐ -│ 常规插件 │ 异步插件 │ -└─────┬─────┘┌────┬───────┘ - │ │ │ - │ │ │ 响应超时 - │ │ ▼ - │ │┌──────────┐ - │ ││ 返回部分 │ - │ ││ 结果或空 │ - │ │└────┬─────┘ - │ │ │ 后台继续处理 - │ │ ▼ - │ │┌──────────┐ - │ ││ 完成搜索 │ - │ │└────┬─────┘ - │ │ │ - │ ▼ ▼ -┌─────▼────────────────────┐ -│ 合并所有结果 │ -└───────────┬──────────────┘ - │ -┌───────────▼──────────────┐ -│ 过滤和排序结果 │ -└───────────┬──────────────┘ - │ -┌───────────▼──────────────┐ -│ 更新内存缓存 │ -└───────────┬──────────────┘ - │ -┌───────────▼──────────────┐ -│ 异步更新磁盘缓存 │ -└───────────┬──────────────┘ - │ -┌───────────▼──────────────┐ -│ 返回结果给用户 (100条) │ -└──────────────────────────┘ -``` - -**详细说明:** - -1. **缓存键生成**:系统根据搜索关键词和其他参数生成唯一的缓存键 - ```go - normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword)) - keyStr := fmt.Sprintf("plugin:%s:all", normalizedKeyword) - hash := md5.Sum([]byte(keyStr)) - cacheKey := hex.EncodeToString(hash[:]) - ``` - -2. **缓存检查**:系统首先检查内存缓存,然后检查磁盘缓存,均未命中 - -3. **执行搜索**:系统并行执行TG搜索和插件搜索 - ```go - var wg sync.WaitGroup - wg.Add(2) - - go func() { - defer wg.Done() - tgResults, tgErr = s.searchTG(keyword, channels, forceRefresh) - }() - - go func() { - defer wg.Done() - pluginResults, pluginErr = s.searchPlugins(keyword, plugins, forceRefresh, concurrency) - }() - - wg.Wait() - ``` - -4. **异步插件处理**:异步插件可能在响应超时前只返回部分结果,但会在后台继续处理 - ```go - // 在AsyncSearch方法中的响应超时处理 - select { - case results := <-resultChan: - close(doneChan) - return results, nil - case err := <-errorChan: - close(doneChan) - return nil, err - case <-time.After(responseTimeout): - // 响应超时,返回空结果,后台继续处理 - go func() { - defer close(doneChan) - }() - return []model.SearchResult{}, nil - } - ``` - -5. **结果处理**:系统合并、过滤和排序所有搜索结果 - -6. **缓存更新**:系统将处理后的结果异步写入内存缓存和磁盘缓存 - -7. **返回结果**:系统将处理后的结果返回给用户 - -### 7.2 第二次搜索(短时间内,内存缓存命中) - -当用户在短时间内再次执行相同搜索时,系统会经历以下步骤: - -``` -┌─────────────────────────┐ -│ 用户搜索请求 │ -│ (关键词:"电影") │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 生成缓存键 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 检查内存缓存(命中) │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 返回缓存结果 (150条) │ -└─────────────────────────┘ -``` - -**详细说明:** - -1. **缓存键生成**:与首次搜索相同,系统生成唯一的缓存键 - -2. **内存缓存命中**:系统检查内存缓存,发现命中。此时异步插件在陆续后台更新缓存,可能还没更新完毕。 - ```go - data, _, memHit := c.memory.GetWithTimestamp(key) - if memHit { - return data, true, nil - } - ``` - -3. **返回缓存结果**:系统直接返回缓存结果,无需执行搜索 - -### 7.3 第三次搜索(异步插件已更新缓存) - -当异步插件在后台完成处理并更新了缓存后,用户再次执行相同搜索时,系统会经历以下步骤: - -``` -┌─────────────────────────┐ -│ 用户搜索请求 │ -│ (关键词:"电影") │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 生成缓存键 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 检查内存缓存(命中) │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 返回更新后的结果 (500条)│ -└─────────────────────────┘ -``` - -**详细说明:** - -1. **缓存键生成**:与之前相同,系统生成唯一的缓存键 - -2. **内存缓存命中**:系统检查内存缓存,发现命中。假设此时异步插件各自的插件缓存都更新完毕了,插件会主动更新主缓存,所以此时内存中的缓存数据也被更新过了 - -5. **返回结果**:系统返回更新后的结果 - -## 8. 异步插件与主缓存协同 - -### 8.1 主缓存注入 - -系统在初始化时将主缓存系统注入到异步插件中,实现统一的缓存更新: - -```go -// injectMainCacheToAsyncPlugins 将主缓存系统注入到异步插件中 -func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCache *cache.EnhancedTwoLevelCache) { - // 创建缓存更新函数 - cacheUpdater := func(key string, data []byte, ttl time.Duration) error { - return mainCache.Set(key, data, ttl) - } - - // 注入到所有异步插件 - for _, p := range pluginManager.GetPlugins() { - if asyncPlugin, ok := p.(interface{ SetMainCacheUpdater(func(string, []byte, time.Duration) error) }); ok { - asyncPlugin.SetMainCacheUpdater(cacheUpdater) - } - } -} -``` - -### 8.2 缓存键一致性 - -为了确保异步插件和主缓存系统使用相同的缓存键,系统实现了缓存键传递机制: - -```go -// BaseAsyncPlugin 基础异步插件结构 -type BaseAsyncPlugin struct { - name string // 插件名称 - priority int // 优先级 - client *http.Client // 短超时客户端 - backgroundClient *http.Client // 长超时客户端 - cacheTTL time.Duration // 缓存有效期 - mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数 - MainCacheKey string // 主缓存键,导出字段,由主程序设置 -} - -// SetMainCacheKey 设置主缓存键 -func (p *BaseAsyncPlugin) SetMainCacheKey(key string) { - p.MainCacheKey = key -} -``` - -### 8.3 异步更新机制 - -异步插件实现了"尽快响应,持续处理"的异步模式,在后台完成搜索后更新缓存: - -```go -// 在后台完成搜索并更新缓存 -func (p *BaseAsyncPlugin) completeSearchInBackground(keyword string, searchFunc func(string) ([]model.SearchResult, error), cachedResponse *cachedResponse, pluginSpecificCacheKey, mainCacheKey string, doneChan chan struct{}) { - defer func() { - select { - case <-doneChan: - // 已经关闭,无需操作 - default: - close(doneChan) - } - }() - - // 执行完整搜索 - results, err := searchFunc(keyword) - if err != nil { - return - } - - // 更新缓存 - cachedResponse.Results = results - cachedResponse.Complete = true - cachedResponse.Timestamp = time.Now() - - // 序列化缓存数据 - data, err := json.Marshal(cachedResponse) - if err != nil { - return - } - - // 更新插件特定缓存 - p.setPluginCache(pluginSpecificCacheKey, data) - - // 如果提供了主缓存键和更新函数,更新主缓存 - if mainCacheKey != "" && p.mainCacheUpdater != nil { - p.mainCacheUpdater(mainCacheKey, data, p.cacheTTL) - } -} -``` - -## 9. 总结 - -PanSou缓存系统是一套精心设计的多层次缓存解决方案,通过增强版两级缓存(内存+分片磁盘)和异步插件缓存系统的协同工作,实现了高性能、高可用的搜索体验。 - -### 9.1 关键特性 - -1. **增强版两级缓存**:结合内存缓存和分片磁盘缓存的优势,提供快速访问和持久存储 -2. **分片磁盘缓存**:通过分片减少锁竞争,提高并发性能 -3. **异步写入机制**:缓存写入异步执行,不阻塞主流程 -4. **智能缓存管理**:基于访问频率、时间和热度的缓存淘汰策略 -5. **异步插件缓存系统**:实现"尽快响应,持续处理"的异步模式 - - -### 9.2 性能优化亮点 - -1. **对象池**:重用对象,减少内存分配和垃圾回收压力 -2. **预分配容量**:避免动态扩容带来的性能损失 -3. **零拷贝技术**:避免不必要的内存拷贝 -4. **分片锁**:减少锁竞争,提高并发性能 -5. **读写锁**:允许多个读操作并发执行 -6. **异步写入**:避免磁盘I/O操作阻塞主流程 -7. **批量操作**:减少系统调用的次数 -8. **延迟写入**:减少磁盘写入次数 -9. **文件系统优化**:通过分片存储和合理的文件命名策略,优化文件系统性能 diff --git a/docs/PanSou系统开发设计文档.md b/docs/PanSou系统开发设计文档.md new file mode 100644 index 0000000..72c7b34 --- /dev/null +++ b/docs/PanSou系统开发设计文档.md @@ -0,0 +1,584 @@ +# PanSou 网盘搜索系统开发设计文档 + +## 📋 文档目录 + +- [1. 项目概述](#1-项目概述) +- [2. 系统架构设计](#2-系统架构设计) +- [3. 异步插件系统](#3-异步插件系统) +- [4. 二级缓存系统](#4-二级缓存系统) +- [5. 核心组件实现](#5-核心组件实现) +- [6. API接口设计](#6-api接口设计) +- [7. 插件开发框架](#7-插件开发框架) +- [8. 性能优化实现](#8-性能优化实现) +- [9. 技术选型说明](#9-技术选型说明) + +--- + +## 1. 项目概述 + +### 1.1 项目定位 + +PanSou是一个高性能的网盘资源搜索API服务,支持TG搜索和自定义插件搜索。系统采用异步插件架构,具备二级缓存机制和并发控制能力,在MacBook Pro 8GB上能够支持500用户并发访问。 + +### 1.2 性能表现(实测数据) + +- ✅ **500用户瞬时并发**: 100%成功率,平均响应167ms +- ✅ **200用户持续并发**: 30秒内处理4725请求,QPS=148 +- ✅ **缓存命中**: 99.8%请求<100ms响应时间 +- ✅ **高可用性**: 长时间运行无故障 + +### 1.3 核心特性 + +- **异步插件系统**: 双级超时控制(4秒/30秒),渐进式结果返回 +- **二级缓存系统**: 分片内存缓存+分片磁盘缓存,GOB序列化 +- **工作池管理**: 基于`util/pool`的并发控制 +- **智能结果合并**: `mergeSearchResults`函数实现去重合并 +- **多网盘类型支持**: 自动识别12种网盘类型 + +--- + +## 2. 系统架构设计 + +### 2.1 整体架构流程 + +```mermaid +graph TB + A[用户请求] --> B{API网关
中间件} + B --> C[参数解析
与验证] + C --> D[搜索服务
SearchService] + + D --> E{数据来源
选择} + E -->|TG| F[Telegram搜索] + E -->|Plugin| G[插件搜索] + E -->|All| H[并发搜索] + + H --> F + H --> G + + F --> I[TG频道搜索] + I --> J[HTML解析] + J --> K[链接提取] + K --> L[结果标准化] + + G --> M[插件管理器
PluginManager] + M --> N[异步插件调度] + N --> O[插件工作池] + O --> P[HTTP客户端] + P --> Q[目标网站API] + Q --> R[响应解析] + R --> S[结果过滤] + + L --> T{二级缓存系统} + S --> T + + T --> U[分片内存缓存
LRU + 原子操作] + T --> V[分片磁盘缓存
GOB序列化] + + U --> W[缓存检查] + V --> W + W --> X{缓存命中?} + + X -->|是| Y[缓存反序列化] + X -->|否| Z[执行搜索] + + Z --> AA[异步更新缓存] + AA --> U + AA --> V + + Y --> BB[结果合并
mergeSearchResults] + AA --> BB + + BB --> CC[网盘类型分类] + CC --> DD[智能排序
时间+权重] + DD --> EE[结果过滤
cloud_types] + EE --> FF[JSON响应] + FF --> GG[用户] + + %% 异步处理流程 + N --> HH[短超时处理
4秒] + HH --> II{是否完成?} + II -->|是| JJ[返回完整结果
isFinal=true] + II -->|否| KK[返回部分结果
isFinal=false] + + KK --> LL[后台继续处理
最长30秒] + LL --> MM[完整结果获取] + MM --> NN[主缓存更新] + NN --> U + + %% 样式定义 + classDef cacheNode fill:#e1f5fe + classDef pluginNode fill:#f3e5f5 + classDef searchNode fill:#e8f5e8 + classDef asyncNode fill:#fff3e0 + + class T,U,V,W,X,Y,AA cacheNode + class M,N,O,P,G pluginNode + class F,I,J,K,L searchNode + class HH,II,JJ,KK,LL,MM,NN asyncNode +``` + +### 2.2 异步插件工作流程 + +```mermaid +sequenceDiagram + participant U as 用户 + participant API as API Gateway + participant S as SearchService + participant PM as PluginManager + participant P as AsyncPlugin + participant WP as WorkerPool + participant C as Cache + participant EXT as 外部API + + U->>API: 搜索请求 (kw=关键词) + API->>S: 处理搜索 + S->>C: 检查缓存 + + alt 缓存命中 + C-->>S: 返回缓存数据 (<100ms) + S-->>U: 返回结果 + else 缓存未命中 + S->>PM: 调度插件搜索 + PM->>P: 设置关键词和缓存键 + P->>WP: 提交异步任务 + + par 短超时处理 (4秒) + WP->>EXT: HTTP请求 + EXT-->>WP: 响应数据 + WP->>P: 解析结果 + P-->>S: 部分结果 (isFinal=false) + S->>C: 缓存部分结果 + S-->>U: 快速响应 + and 后台完整处理 (最长30秒) + WP->>EXT: 继续处理 + EXT-->>WP: 完整数据 + WP->>P: 完整结果 + P->>S: 最终结果 (isFinal=true) + S->>C: 更新主缓存 + Note over C: 用户下次请求将获得完整结果 + end + end +``` + +### 2.3 核心组件 + +#### 2.3.1 HTTP服务层 (`api/`) +- **router.go**: 路由配置 +- **handler.go**: 请求处理逻辑 +- **middleware.go**: 中间件(日志、CORS等) + +#### 2.3.2 搜索服务层 (`service/`) +- **search_service.go**: 核心搜索逻辑,结果合并 + +#### 2.3.3 插件系统层 (`plugin/`) +- **plugin.go**: 插件接口定义 +- **baseasyncplugin.go**: 异步插件基类 +- **各插件目录**: jikepan、pan666、hunhepan等 + +#### 2.3.4 工具层 (`util/`) +- **cache/**: 二级缓存系统实现 +- **pool/**: 工作池实现 +- **其他工具**: HTTP客户端、解析工具等 + +--- + +## 3. 异步插件系统 + +### 3.1 设计理念 + +异步插件系统解决传统同步搜索响应慢的问题,采用"尽快响应,持续处理"策略: +- **4秒短超时**: 快速返回部分结果(`isFinal=false`) +- **30秒长超时**: 后台继续处理,获得完整结果(`isFinal=true`) +- **主动缓存更新**: 完整结果自动更新主缓存,下次访问更快 + +### 3.2 插件接口实现 + +基于`plugin/plugin.go`的实际接口: + +```go +type AsyncSearchPlugin interface { + Name() string + Priority() int + + AsyncSearch(keyword string, searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), + mainCacheKey string, ext map[string]interface{}) ([]model.SearchResult, error) + + SetMainCacheKey(key string) + SetCurrentKeyword(keyword string) + Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) +} +``` + +### 3.3 基础插件类 + +`plugin/baseasyncplugin.go`提供通用功能: + +```go +type BaseAsyncPlugin struct { + name string + priority int + cacheTTL time.Duration + mainCacheKey string + currentKeyword string // 用于日志显示 + httpClient *http.Client + mainCacheUpdater func(string, []model.SearchResult, time.Duration, bool, string) error +} +``` + +### 3.4 已实现插件列表 + +当前系统包含以下插件(基于`main.go`的导入): +- **hdr4k**: HDR 4K资源搜索 +- **hunhepan**: 混合盘搜索 +- **jikepan**: 即刻盘搜索 +- **pan666**: 666盘搜索 +- **pansearch**: 盘搜索 +- **panta**: 盘塔搜索 +- **qupansou**: 去盘搜 +- **susu**: SUSU搜索 +- **panyq**: 盘有圈搜索 +- **xuexizhinan**: 学习指南搜索 + +### 3.5 插件注册机制 + +```go +// 全局插件注册表(plugin/plugin.go) +var globalRegistry = make(map[string]AsyncSearchPlugin) + +// 插件通过init()函数自动注册 +func init() { + p := &MyPlugin{ + BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3), + } + plugin.RegisterGlobalPlugin(p) +} +``` + +--- + +## 4. 二级缓存系统 + +### 4.1 实现架构 + +基于`util/cache/`目录的实际实现: + +- **enhanced_two_level_cache.go**: 二级缓存主入口 +- **sharded_memory_cache.go**: 分片内存缓存(LRU+原子操作) +- **sharded_disk_cache.go**: 分片磁盘缓存 +- **serializer.go**: GOB序列化器 +- **cache_key.go**: 缓存键生成和管理 + +### 4.2 分片缓存设计 + +#### 4.2.1 内存缓存分片 +```go +// 基于CPU核心数的动态分片 +type ShardedMemoryCache struct { + shards []*MemoryCacheShard + shardMask uint32 +} + +// 每个分片独立锁,减少竞争 +type MemoryCacheShard struct { + data map[string]*CacheItem + lock sync.RWMutex +} +``` + +#### 4.2.2 磁盘缓存分片 +```go +// 磁盘缓存同样采用分片设计 +type ShardedDiskCache struct { + shards []*DiskCacheShard + shardMask uint32 + basePath string +} +``` + +### 4.3 缓存读写策略 + +#### 4.3.1 读取流程 +1. **内存优先**: 先检查分片内存缓存 +2. **磁盘回源**: 内存未命中时读取磁盘缓存 +3. **异步加载**: 磁盘命中后异步加载到内存 + +#### 4.3.2 写入流程 +1. **双写模式**: 同时写入内存和磁盘 +2. **原子操作**: 内存缓存使用原子操作 +3. **GOB序列化**: 磁盘存储使用GOB格式 + +### 4.4 缓存键策略 + +`cache_key.go`实现了智能缓存键生成: + +```go +// TG搜索和插件搜索使用不同的缓存键前缀 +func GenerateTGCacheKey(keyword string, channels []string) string +func GeneratePluginCacheKey(keyword string, plugins []string) string +``` + +**优势**: +- 独立更新:TG和插件缓存互不影响 +- 提高命中率:精确的键匹配 +- 并发安全:分片设计减少锁竞争 + +### 4.5 序列化性能 + +使用GOB序列化(`serializer.go`)的实际优势: +- **性能**: 比JSON序列化快约30% +- **体积**: 比JSON小约20% +- **兼容**: Go原生支持,无外部依赖 + +--- + +## 5. 核心组件实现 + +### 5.1 工作池系统 (`util/pool/`) + +#### 5.1.1 worker_pool.go 实现 +- **批量任务处理**: `ExecuteBatchWithTimeout`方法 +- **超时控制**: 支持任务级别的超时设置 +- **并发限制**: 控制最大工作者数量 + +#### 5.1.2 object_pool.go 实现 +- **对象复用**: 减少内存分配和GC压力 +- **线程安全**: 支持并发访问 + +### 5.2 HTTP服务配置 + +#### 5.2.1 服务器优化(基于config/config.go) +```go +// 自动计算HTTP连接数,防止资源耗尽 +func getHTTPMaxConns() int { + cpuCount := runtime.NumCPU() + maxConns := cpuCount * 25 // 保守配置 + + if maxConns < 100 { + maxConns = 100 + } + if maxConns > 500 { + maxConns = 500 // 限制最大值 + } + + return maxConns +} +``` + +#### 5.2.2 连接池配置(基于util/http_util.go) +```go +// HTTP客户端优化配置 +transport := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, +} +``` + +### 5.3 结果处理系统 + +#### 5.3.1 智能排序(service/search_service.go) +- **时间权重排序**: 基于时间和关键词权重 +- **优先关键词**: 合集、系列、全、完等优先显示 + +#### 5.3.2 结果合并(mergeSearchResults函数) +- **去重合并**: 基于UniqueID去重 +- **完整性选择**: 选择更完整的结果保留 +- **增量更新**: 新结果与缓存结果智能合并 + +### 5.4 网盘类型识别 + +支持自动识别的网盘类型(共12种): +- 百度网盘、阿里云盘、夸克网盘、天翼云盘 +- UC网盘、移动云盘、115网盘、PikPak +- 迅雷网盘、123网盘、磁力链接、电驴链接 + +--- + +## 6. API接口设计 + +### 6.1 核心接口实现(基于api/handler.go) + +#### 6.1.1 搜索接口 +``` +POST /api/search +GET /api/search +``` + +**核心参数**: +- `kw`: 搜索关键词(必填) +- `channels`: TG频道列表 +- `plugins`: 插件列表 +- `cloud_types`: 网盘类型过滤 +- `ext`: 扩展参数(JSON格式) +- `refresh`: 强制刷新缓存 +- `res`: 返回格式(merge/all/results) +- `src`: 数据源(all/tg/plugin) + +#### 6.1.2 健康检查接口 +``` +GET /api/health +``` + +返回系统状态和已注册插件信息。 + +### 6.2 中间件系统(api/middleware.go) + +- **日志中间件**: 记录请求响应,支持URL解码显示 +- **CORS中间件**: 跨域请求支持 +- **错误处理**: 统一错误响应格式 + +### 6.3 扩展参数系统 + +通过`ext`参数支持插件特定选项: +```json +{ + "title_en": "English Title", + "is_all": true, + "year": 2023 +} +``` + +--- + +## 7. 插件开发框架 + +### 7.1 基础开发模板 + +```go +package myplugin + +import ( + "net/http" + "pansou/model" + "pansou/plugin" +) + +type MyPlugin struct { + *plugin.BaseAsyncPlugin +} + +func init() { + p := &MyPlugin{ + BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3), + } + plugin.RegisterGlobalPlugin(p) +} + +func (p *MyPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + return p.AsyncSearch(keyword, p.searchImpl, p.GetMainCacheKey(), ext) +} + +func (p *MyPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + // 实现具体搜索逻辑 + // 1. 构建请求URL + // 2. 发送HTTP请求 + // 3. 解析响应数据 + // 4. 转换为标准格式 + // 5. 关键词过滤 + return plugin.FilterResultsByKeyword(results, keyword), nil +} +``` + +### 7.2 插件注册流程 + +1. **自动注册**: 通过`init()`函数自动注册到全局注册表 +2. **管理器加载**: `PluginManager`统一管理所有插件 +3. **导入触发**: 在`main.go`中通过空导入触发注册 + +### 7.3 开发最佳实践 + +- **命名规范**: 插件名使用小写字母 +- **优先级设置**: 1-5,数字越小优先级越高 +- **错误处理**: 详细错误信息,便于调试 +- **资源管理**: 及时释放HTTP连接 + +--- + +## 8. 性能优化实现 + +### 8.1 环境配置优化 + +基于实际性能测试结果的配置方案: + +#### 8.1.1 macOS优化配置 +```bash +export HTTP_MAX_CONNS=200 +export ASYNC_MAX_BACKGROUND_WORKERS=15 +export ASYNC_MAX_BACKGROUND_TASKS=75 +export CONCURRENCY=30 +``` + +#### 8.1.2 服务器优化配置 +```bash +export HTTP_MAX_CONNS=500 +export ASYNC_MAX_BACKGROUND_WORKERS=40 +export ASYNC_MAX_BACKGROUND_TASKS=200 +export CONCURRENCY=50 +``` + +### 8.2 性能测试脚本 + +项目包含完整的测试套件: +- **stress_test.py**: 高并发压力测试(100-500用户) +- **test_performance.py**: 基础性能测试 +- **test_optimized.py**: 渐进式并发测试 + +### 8.3 日志控制系统 + +基于`config.go`的日志控制: +```bash +export ASYNC_LOG_ENABLED=false # 控制异步插件详细日志 +``` + +异步插件缓存更新日志可通过环境变量开关,避免生产环境日志过多。 + +--- + +## 9. 技术选型说明 + +### 9.1 Go语言优势 +- **并发支持**: 原生goroutine,适合高并发场景 +- **性能优秀**: 编译型语言,接近C的性能 +- **部署简单**: 单一可执行文件,无外部依赖 +- **标准库丰富**: HTTP、JSON、并发原语完备 + +### 9.2 GIN框架选择 +- **高性能**: 路由和中间件处理效率高 +- **简洁易用**: API设计简洁,学习成本低 +- **中间件生态**: 丰富的中间件支持 +- **社区活跃**: 文档完善,问题解决快 + +### 9.3 GOB序列化选择 +- **性能优势**: 比JSON快约30% +- **体积优势**: 比JSON小约20% +- **Go原生**: 无需第三方依赖 +- **类型安全**: 保持Go类型信息 + +### 9.4 无数据库架构 +- **简化部署**: 无需数据库安装配置 +- **降低复杂度**: 减少组件依赖 +- **提升性能**: 避免数据库IO瓶颈 +- **易于扩展**: 无状态设计,支持水平扩展 + +--- + +## 📚 参考资料 + +### 相关文档 +- **[插件开发指南](插件开发指南.md)** - 详细的插件开发文档 +- **[README.md](../README.md)** - 项目使用说明和性能测试结果 + +### 性能测试 +- **stress_test.py**: 高并发压力测试脚本 +- **run_macos_optimized.sh**: macOS优化配置脚本 +- **run_optimized.sh**: 服务器优化配置脚本 + +--- + +**文档版本**: v1.0 +**最后更新**: 2025-01-30 +**维护者**: PanSou开发团队 + +--- + +*本文档基于PanSou系统的实际实现编写,包含了系统设计的核心信息。文档内容与代码实现保持同步,专注于已实现的功能特性。* \ No newline at end of file diff --git a/docs/插件开发指南.md b/docs/插件开发指南.md index 39e5613..3daaa27 100644 --- a/docs/插件开发指南.md +++ b/docs/插件开发指南.md @@ -1,1515 +1,350 @@ -# PanSou 搜索插件开发指南 +# PanSou 插件开发指南 -## 目录 +## 概述 -1. [插件系统概述](#插件系统概述) -2. [插件接口说明](#插件接口说明) -3. [插件开发流程](#插件开发流程) -4. [数据结构标准](#数据结构标准) -5. [超时控制](#超时控制) -6. [异步插件开发](#异步插件开发) -7. [最佳实践](#最佳实践) -8. [示例插件](#示例插件) -9. [常见问题](#常见问题) +PanSou 采用异步插件架构,支持通过插件扩展搜索来源。插件系统基于 Go 接口设计,提供高性能的并发搜索能力和智能缓存机制。 -## 插件系统概述 +## 系统架构 -PanSou 网盘搜索系统采用了灵活的插件架构,允许开发者轻松扩展搜索来源。插件系统具有以下特点: +### 核心组件 -- **自动注册机制**:插件通过 init 函数自动注册,无需修改主程序代码 -- **统一接口**:所有插件实现相同的 SearchPlugin 接口 -- **双层超时控制**:插件内部使用自定义超时时间,系统外部提供强制超时保障 -- **并发执行**:插件搜索与频道搜索并发执行,提高整体性能 -- **结果标准化**:插件返回标准化的搜索结果,便于统一处理 -- **异步处理**:支持异步插件,实现"尽快响应,持续处理"的模式 +- **插件管理器 (PluginManager)**: 管理所有插件的注册和调度 +- **异步插件 (AsyncSearchPlugin)**: 实现异步搜索接口的插件 +- **基础插件 (BaseAsyncPlugin)**: 提供通用功能的基础结构 +- **工作池**: 管理并发请求和资源限制 +- **缓存系统**: 二级缓存提供高性能数据存储 -插件系统的核心是全局插件注册表,它在应用启动时收集所有已注册的插件,并在搜索时并行调用这些插件。 +### 异步处理机制 -## 插件接口说明 +1. **双级超时控制**: + - 短超时 (4秒): 确保快速响应用户 + - 长超时 (30秒): 允许完整数据处理 -每个插件必须实现 `SearchPlugin` 接口,该接口定义如下: +2. **渐进式结果返回**: + - `isFinal=false`: 部分结果,继续后台处理 + - `isFinal=true`: 完整结果,停止处理 + +3. **智能缓存更新**: + - 实时更新主缓存 (内存+磁盘) + - 结果合并去重 + - 用户无感知数据更新 + +## 插件接口规范 + +### AsyncSearchPlugin 接口 ```go -// SearchPlugin 搜索插件接口 -type SearchPlugin interface { - // Name 返回插件名称 +type AsyncSearchPlugin interface { + // Name 返回插件名称 (必须唯一) Name() string - // Search 执行搜索并返回结果 - // ext参数用于传递额外的搜索参数,插件可以根据需要使用或忽略 - Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) - - // Priority 返回插件优先级(用于控制结果排序) + // Priority 返回插件优先级 (1-5,数字越小优先级越高) Priority() int + + // AsyncSearch 异步搜索方法 (核心方法) + AsyncSearch(keyword string, searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), mainCacheKey string, ext map[string]interface{}) ([]model.SearchResult, error) + + // SetMainCacheKey 设置主缓存键 (由系统调用) + SetMainCacheKey(key string) + + // SetCurrentKeyword 设置当前搜索关键词 (用于日志显示) + SetCurrentKeyword(keyword string) + + // Search 同步搜索方法 (兼容性方法) + Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) } ``` -### 接口方法说明 +### 参数说明 -1. **Name()** - - 返回插件的唯一标识名称 - - 名称应简洁明了,全小写,不含特殊字符 - - 例如:`pansearch`、`hunhepan`、`jikepan` +- **keyword**: 搜索关键词 +- **searchFunc**: HTTP搜索函数,处理实际的网络请求 +- **mainCacheKey**: 主缓存键,用于缓存管理 +- **ext**: 扩展参数,支持自定义搜索选项 -2. **Search(keyword string, ext map[string]interface{})** - - 执行搜索并返回结果 - - 参数 `keyword` 是用户输入的搜索关键词 - - 参数 `ext` 是扩展参数,用于传递额外的搜索参数,如 `title_en`(英文标题) - - 返回值是搜索结果数组和可能的错误 - - 实现时应处理超时和错误,确保不会无限阻塞 +## 开发新插件 -3. **Priority()** - - 返回插件的优先级,用于控制结果排序 - - 建议值:1(低)、2(中)、3(高) - - 优先级高的插件结果可能会被优先展示 - -## 插件开发流程 - -### 1. 创建插件包 - -在 `pansou/plugin` 目录下创建新的插件包: - -``` -pansou/ - └── plugin/ - └── myplugin/ - └── myplugin.go -``` - -### 2. 实现插件结构体 +### 1. 基础结构 ```go package myplugin import ( "net/http" - "time" - "pansou/model" "pansou/plugin" ) -// 常量定义 -const ( - // 默认超时时间 - DefaultTimeout = 5 * time.Second -) - -// MyPlugin 自定义插件结构体 type MyPlugin struct { - client *http.Client - timeout time.Duration -} - -// NewMyPlugin 创建新的插件实例 -func NewMyPlugin() *MyPlugin { - timeout := DefaultTimeout - - return &MyPlugin{ - client: &http.Client{ - Timeout: timeout, - }, - timeout: timeout, - } -} -``` - -### 3. 实现 SearchPlugin 接口 - -```go -// Name 返回插件名称 -func (p *MyPlugin) Name() string { - return "myplugin" -} - -// Priority 返回插件优先级 -func (p *MyPlugin) Priority() int { - return 2 // 中等优先级 -} - -// Search 执行搜索并返回结果 -func (p *MyPlugin) Search(keyword string) ([]model.SearchResult, error) { - // 实现搜索逻辑 - // ... - - return results, nil -} -``` - -### 4. 注册插件 - -在插件包的 init 函数中注册插件: - -```go -// 在init函数中注册插件 -func init() { - plugin.RegisterGlobalPlugin(NewMyPlugin()) -} -``` - -### 5. 在主程序中导入插件 - -在 `pansou/main.go` 中导入插件包(使用空导入): - -```go -import ( - // 导入插件包以触发init函数 - _ "pansou/plugin/myplugin" -) -``` - -## 数据结构标准 - -### SearchResult 结构体 - -插件需要返回 `[]model.SearchResult` 类型的数据: - -```go -// SearchResult 表示搜索结果 -type SearchResult struct { - UniqueID string // 唯一标识 - Title string // 标题 - Content string // 内容描述 - Datetime time.Time // 日期时间 - Links []Link // 链接列表 - Tags []string // 标签列表 -} - -// Link 表示网盘链接 -type Link struct { - URL string // 链接地址 - Type string // 链接类型 - Password string // 提取码 -} -``` - -### 字段说明 - -1. **UniqueID**: - - 结果的唯一标识,建议格式:`插件名-序号` - - 例如:`myplugin-1`、`myplugin-2` - -2. **Title**: - - 资源的标题 - - 应尽可能保留原始标题,不要添加额外信息 - - 例如:`火影忍者全集高清资源` - -3. **Content**: - - 资源的描述内容 - - 可以包含文件列表、大小、格式等信息 - - 应清理HTML标签等无关内容 - -4. **Datetime**: - - 资源的发布时间或更新时间 - - 如果没有时间信息,使用零值 `time.Time{}` - - 不要使用当前时间 `time.Now()` - -5. **Links**: - - 资源的链接列表 - - 每个资源可以有多个不同类型的链接 - - 每个链接必须包含URL和Type,Password可选 - -6. **URL**: - - 网盘链接的完整URL - - 必须包含协议部分(如 http:// 或 https://) - - 例如:`https://pan.baidu.com/s/1abcdefg` - -7. **Type**: - - 链接类型,必须使用以下标准值之一: - - `baidu` - 百度网盘 - - `aliyun` - 阿里云盘 - - `xunlei` - 迅雷云盘 - - `quark` - 夸克网盘 - - `tianyi` - 天翼云盘 - - `115` - 115网盘 - - `weiyun` - 微云 - - `lanzou` - 蓝奏云 - - `jianguoyun` - 坚果云 - - `mobile` - 移动云盘(彩云) - - `uc` - UC网盘 - - `123` - 123网盘 - - `pikpak` - PikPak网盘 - - `ed2k` - 电驴链接 - - `magnet` - 磁力链接 - - `others` - 其他类型 - -8. **Password**: - - 提取码或访问密码 - - 如果没有密码,设置为空字符串 - -9. **Tags**: - - 资源的标签列表 - - 可选字段,不是必须提供 - -### 具体示例 - -下面是几个完整的 `SearchResult` 结构体示例,展示了不同情况下的数据填充方式: - -#### 示例1:带有百度网盘链接的电影资源 - -```go -// 创建一个带有百度网盘链接的电影资源搜索结果 -movieResult := model.SearchResult{ - UniqueID: "myplugin-1", - Title: "速度与激情10 4K蓝光原盘", - Content: "文件列表:\n- 速度与激情10.mp4 (25.6GB)\n- 花絮.mp4 (1.2GB)\n- 字幕.zip (15MB)", - Datetime: time.Date(2023, 8, 15, 10, 30, 0, 0, time.Local), // 2023-08-15 10:30:00 - Links: []model.Link{ - { - URL: "https://pan.baidu.com/s/1abcdefghijklmn", - Type: "baidu", - Password: "a1b2", - }, - }, - Tags: []string{"电影", "动作", "4K"}, -} -``` - -#### 示例2:带有多个网盘链接的软件资源 - -```go -// 创建一个带有多个网盘链接的软件资源搜索结果 -softwareResult := model.SearchResult{ - UniqueID: "myplugin-2", - Title: "Photoshop 2023 完整破解版 Win+Mac", - Content: "Adobe Photoshop 2023 完整破解版,支持Windows和Mac系统,内含安装教程和注册机。", - Datetime: time.Date(2023, 6, 20, 15, 45, 0, 0, time.Local), // 2023-06-20 15:45:00 - Links: []model.Link{ - { - URL: "https://pan.baidu.com/s/1opqrstuvwxyz", - Type: "baidu", - Password: "c3d4", - }, - { - URL: "https://www.aliyundrive.com/s/abcdefghijk", - Type: "aliyun", - Password: "", // 阿里云盘无提取码 - }, - { - URL: "https://pan.xunlei.com/s/12345678", - Type: "xunlei", - Password: "xunl", - }, - }, - Tags: []string{"软件", "设计", "Adobe"}, -} -``` - -#### 示例3:带有磁力链接的资源 - -```go -// 创建一个带有磁力链接的资源搜索结果 -torrentResult := model.SearchResult{ - UniqueID: "myplugin-3", - Title: "权力的游戏 第一季 1080P 中英双字", - Content: "权力的游戏第一季全10集,1080P高清版本,内封中英双字幕。", - Datetime: time.Date(2022, 12, 5, 8, 0, 0, 0, time.Local), // 2022-12-05 08:00:00 - Links: []model.Link{ - { - URL: "magnet:?xt=urn:btih:1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t", - Type: "magnet", - Password: "", // 磁力链接没有密码 - }, - }, - Tags: []string{"美剧", "奇幻", "1080P"}, -} -``` - -#### 示例4:没有时间信息的资源 - -```go -// 创建一个没有时间信息的资源搜索结果 -noTimeResult := model.SearchResult{ - UniqueID: "myplugin-4", - Title: "中国历史文化名人传记合集", - Content: "包含100位中国历史文化名人的详细传记,PDF格式。", - Datetime: time.Time{}, // 使用零值表示没有时间信息 - Links: []model.Link{ - { - URL: "https://pan.quark.cn/s/12345abcde", - Type: "quark", - Password: "qwer", - }, - }, - Tags: []string{"电子书", "历史", "传记"}, -} -``` - -#### 示例5:多种文件格式的教程资源 - -```go -// 创建一个包含多种文件格式的教程资源搜索结果 -tutorialResult := model.SearchResult{ - UniqueID: "myplugin-5", - Title: "Python数据分析实战教程 2023最新版", - Content: "包含视频教程、源代码、PPT讲义和练习题。适合Python初学者和有一定基础的开发者。", - Datetime: time.Date(2023, 9, 1, 12, 0, 0, 0, time.Local), // 2023-09-01 12:00:00 - Links: []model.Link{ - { - URL: "https://cloud.189.cn/t/abcdefg123456", - Type: "tianyi", - Password: "189t", - }, - { - URL: "https://caiyun.139.com/m/i?abcdefghijk", - Type: "mobile", - Password: "139c", - }, - }, - Tags: []string{"教程", "Python", "数据分析"}, -} -``` - -### 返回结果示例 - -插件的 `Search` 方法应返回一个 `[]model.SearchResult` 切片,包含所有搜索结果: - -```go -// Search 执行搜索并返回结果 -func (p *MyPlugin) Search(keyword string) ([]model.SearchResult, error) { - // ... 执行搜索逻辑 ... - - // 创建结果切片 - results := []model.SearchResult{ - movieResult, - softwareResult, - torrentResult, - noTimeResult, - tutorialResult, - } - - return results, nil -} -``` - -### 注意事项 - -1. **链接类型映射**: - 如果源站点使用的链接类型名称与标准不同,需要进行映射,例如: - - ```go - func mapLinkType(sourceType string) string { - switch strings.ToLower(sourceType) { - case "bd", "bdy", "baidu_pan": - return "baidu" - case "al", "aly", "aliyundrive": - return "aliyun" - case "ty", "tianyi_pan": - return "tianyi" - // ... 其他映射 - default: - return "others" - } - } - ``` - -2. **URL格式化**: - 确保URL格式正确,特别是对于特殊链接类型: - - ```go - // 确保百度网盘链接格式正确 - if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") { - url = "https://" + url - } - - // 确保磁力链接格式正确 - if strings.HasPrefix(url, "magnet:") && !strings.HasPrefix(url, "magnet:?xt=urn:btih:") { - // 格式不正确,尝试修复或跳过 - } - ``` - -3. **密码处理**: - 对于不同网盘的密码格式可能有所不同,需要适当处理: - - ```go - // 百度网盘密码通常为4位 - if linkType == "baidu" && len(password) > 4 { - password = password[:4] - } - - // 有些网盘可能在URL中包含密码参数 - if linkType == "aliyun" && password == "" { - // 尝试从URL中提取密码 - if pwdIndex := strings.Index(url, "password="); pwdIndex != -1 { - password = url[pwdIndex+9:] - if endIndex := strings.Index(password, "&"); endIndex != -1 { - password = password[:endIndex] - } - } - } - ``` - -## 超时控制 - -PanSou 采用双层超时控制机制,确保搜索请求能够在合理的时间内完成: - -### 插件内部超时控制 - -每个插件应定义并使用自己的默认超时时间: - -```go -const ( - // 默认超时时间 - DefaultTimeout = 5 * time.Second -) - -// NewMyPlugin 创建新的插件实例 -func NewMyPlugin() *MyPlugin { - timeout := DefaultTimeout - - return &MyPlugin{ - client: &http.Client{ - Timeout: timeout, - }, - timeout: timeout, - } -} -``` - -插件应根据自身特点设置合适的超时时间: -- 需要并发请求多个页面的插件可能设置较短的单次请求超时 -- 需要处理大量数据的插件可能设置较长的超时 - -### 系统外部超时控制 - -系统使用 `ExecuteBatchWithTimeout` 函数对所有插件任务进行统一的超时控制。即使插件内部没有正确处理超时,系统也能确保整体搜索在合理时间内完成。 - -超时时间通过环境变量 `PLUGIN_TIMEOUT` 配置,默认为 30 秒。 - -## 6. 异步插件开发 - -异步插件是PanSou系统的高级功能,它实现了"尽快响应,持续处理"的异步模式,特别适合处理响应时间不稳定或较长的API。 - -### 1. 异步插件基础 - -#### 1.1 异步插件特点 - -异步插件具有以下特点: - -1. **快速响应**:即使API响应较慢,也能在超时时间内返回结果 -2. **后台处理**:在返回初步结果后,继续在后台处理完整请求 -3. **缓存更新**:后台处理完成后自动更新缓存 -4. **智能缓存**:支持缓存新鲜度检查和自动刷新 -5. **主缓存协同**:与主程序缓存系统协同工作,保持一致性 - -#### 1.2 异步插件结构 - -```go -// MyAsyncPlugin 自定义异步插件结构体 -type MyAsyncPlugin struct { *plugin.BaseAsyncPlugin } -// NewMyAsyncPlugin 创建新的异步插件实例 -func NewMyAsyncPlugin() *MyAsyncPlugin { - return &MyAsyncPlugin{ +func init() { + p := &MyPlugin{ BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3), } + plugin.RegisterGlobalPlugin(p) +} + +func (p *MyPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + return p.AsyncSearch(keyword, p.searchImpl, p.GetMainCacheKey(), ext) } ``` -### 2. 异步插件实现 - -#### 2.1 创建异步插件 +### 2. 实现搜索逻辑 ```go -package myplugin - -import ( - "net/http" - "time" +func (p *MyPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + // 1. 构建请求URL + url := fmt.Sprintf("https://api.example.com/search?q=%s", url.QueryEscape(keyword)) - "pansou/model" - "pansou/plugin" -) - -// MyAsyncPlugin 自定义异步插件结构体 -type MyAsyncPlugin struct { - *plugin.BaseAsyncPlugin -} - -// NewMyAsyncPlugin 创建新的异步插件实例 -func NewMyAsyncPlugin() *MyAsyncPlugin { - return &MyAsyncPlugin{ - BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin_async", 3), - } -} - -// 在init函数中注册插件 -func init() { - plugin.RegisterGlobalPlugin(NewMyAsyncPlugin()) -} -``` - -#### 2.2 实现Search方法 - -```go -// Search 执行搜索并返回结果 -func (p *MyAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) { - // 使用保存的主缓存键 - return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey) -} - -// doSearch 实际的搜索实现 -func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) { - // 实现具体搜索逻辑 - // 注意:client已经配置了适当的超时时间 - // ... - - // 获取搜索结果 - results, err := actualSearch(client, keyword) - if err != nil { - return nil, err - } - - // 使用过滤功能过滤结果 - filteredResults := p.FilterResultsByKeyword(results, keyword) - - return filteredResults, nil -} -``` - -### 3. 异步搜索流程 - -异步搜索的工作流程如下: - -1. **缓存检查**:首先检查是否有有效缓存 -2. **快速响应**:如果有缓存,立即返回;如果缓存接近过期,在后台刷新 -3. **双通道处理**:如果没有缓存,启动快速响应通道和后台处理通道 -4. **超时控制**:在响应超时时返回当前结果(可能为空),后台继续处理 -5. **缓存更新**:后台处理完成后更新缓存,供后续查询使用 - -### 4. 异步缓存机制 - -#### 4.1 缓存键设计 - -异步插件使用两种缓存键,确保与主缓存系统保持一致: - -```go -// 在AsyncSearch方法中 -// 插件特定的缓存键 -pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword) - -// 主缓存键由主程序传递,通过BaseAsyncPlugin的MainCacheKey字段直接使用 -``` - -这种设计确保了: -1. **一致性**:异步插件直接使用主程序传递的缓存键,避免重复生成不一致的键 -2. **隔离性**:每个插件有自己的缓存命名空间 -3. **可追踪性**:缓存键包含插件名称,便于调试和监控 - -#### 4.2 缓存结构 - -```go -// 缓存响应结构 -type cachedResponse struct { - Results []model.SearchResult // 搜索结果 - Timestamp time.Time // 缓存创建时间 - Complete bool // 是否完整结果 - LastAccess time.Time // 最后访问时间 - AccessCount int // 访问计数 -} -``` - -#### 4.3 缓存持久化 - -异步插件缓存会自动保存到磁盘,并在程序启动时加载: - -- 定期保存:每2分钟保存一次缓存 -- 即时保存:缓存更新后立即触发保存 -- 优雅关闭:程序退出前保存缓存 - -#### 4.4 智能缓存管理 - -系统实现了基于多因素的缓存淘汰策略: - -```go -// 计算得分:访问次数 / (空闲时间的平方 * 年龄) -// 这样: -// - 访问频率高的得分高 -// - 最近访问的得分高 -// - 较新的缓存得分高 -score := float64(item.AccessCount) / (idleTime.Seconds() * idleTime.Seconds() * age.Seconds()) -``` - -### 5. 异步插件示例 - -#### 5.1 混合盘异步插件 - -```go -// HunhepanAsyncPlugin 混合盘搜索异步插件 -type HunhepanAsyncPlugin struct { - *plugin.BaseAsyncPlugin -} - -// NewHunhepanAsyncPlugin 创建新的混合盘搜索异步插件 -func NewHunhepanAsyncPlugin() *HunhepanAsyncPlugin { - return &HunhepanAsyncPlugin{ - BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("hunhepan", 3), - } -} - -// Search 执行搜索并返回结果 -func (p *HunhepanAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) { - // 使用保存的主缓存键 - return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey) -} - -// doSearch 实际的搜索实现 -func (p *HunhepanAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) { - // 创建结果通道和错误通道 - resultChan := make(chan []HunhepanItem, 3) - errChan := make(chan error, 3) - - // 创建等待组 - var wg sync.WaitGroup - wg.Add(3) - - // 并行请求三个API - go func() { - defer wg.Done() - items, err := p.searchAPI(client, HunhepanAPI, keyword) - if err != nil { - errChan <- fmt.Errorf("hunhepan API error: %w", err) - return - } - resultChan <- items - }() - - // ... 其他API请求 ... - - // 启动一个goroutine等待所有请求完成并关闭通道 - go func() { - wg.Wait() - close(resultChan) - close(errChan) - }() - - // 收集结果 - var allItems []HunhepanItem - var errors []error - - // 从通道读取结果 - for items := range resultChan { - allItems = append(allItems, items...) - } - - // 处理错误 - for err := range errChan { - errors = append(errors, err) - } - - // 如果没有获取到任何结果且有错误,则返回第一个错误 - if len(allItems) == 0 && len(errors) > 0 { - return nil, errors[0] - } - - // 去重处理 - uniqueItems := p.deduplicateItems(allItems) - - // 转换为标准格式 - results := p.convertResults(uniqueItems) - - // 使用过滤功能过滤结果 - filteredResults := p.FilterResultsByKeyword(results, keyword) - - return filteredResults, nil -} -``` - -### 5.6 异步插件与主程序缓存协同 - -异步插件系统与主程序的缓存系统协同工作,实现了完整的缓存更新流程: - -1. **主缓存键传递**:主程序在调用异步插件时传递主缓存键 -2. **缓存键保存**:主程序通过`SetMainCacheKey`方法将缓存键保存到插件的`MainCacheKey`字段 -3. **直接使用**:插件在`Search`方法中直接使用`p.MainCacheKey`,不再重新生成缓存键 -4. **缓存更新**:异步插件在后台处理完成后,使用保存的主缓存键更新主缓存 -5. **缓存一致性**:确保异步插件缓存和主缓存保持一致 - -### 5.7 并发保护机制 - -异步插件系统实现了多种并发保护机制,确保在高并发场景下的稳定性: - -1. **互斥锁保护**:使用`saveCacheLock`互斥锁保护缓存保存操作 -2. **随机延迟**:在触发缓存保存前添加随机延迟,减少冲突 -3. **无锁数据结构**:使用`sync.Map`存储缓存项,减少锁竞争 - -## 7. 结果过滤功能 - -PanSou插件系统提供了结果过滤功能,可以根据搜索关键词过滤搜索结果,提高结果的相关性。 - -### 7.1 过滤功能概述 - -过滤功能的主要目的是: -1. **提高相关性**:确保返回的结果与搜索关键词相关 -2. **减少无关结果**:过滤掉与关键词无关的结果 -3. **支持多关键词**:支持按空格分割的多个关键词过滤 - -### 7.2 过滤方法实现 - -BaseAsyncPlugin提供了`FilterResultsByKeyword`方法,用于过滤搜索结果: - -```go -// FilterResultsByKeyword 根据关键词过滤搜索结果 -func (p *BaseAsyncPlugin) FilterResultsByKeyword(results []model.SearchResult, keyword string) []model.SearchResult { - if keyword == "" { - return results - } - - // 预估过滤后会保留80%的结果 - filteredResults := make([]model.SearchResult, 0, len(results)*8/10) - - // 将关键词转为小写,用于不区分大小写的比较 - lowerKeyword := strings.ToLower(keyword) - - // 将关键词按空格分割,用于支持多关键词搜索 - keywords := strings.Fields(lowerKeyword) - - for _, result := range results { - // 将标题和内容转为小写 - lowerTitle := strings.ToLower(result.Title) - lowerContent := strings.ToLower(result.Content) - - // 检查每个关键词是否在标题或内容中 - matched := true - for _, kw := range keywords { - // 对于所有关键词,检查是否在标题或内容中 - if !strings.Contains(lowerTitle, kw) && !strings.Contains(lowerContent, kw) { - matched = false - break - } - } - - if matched { - filteredResults = append(filteredResults, result) - } - } - - return filteredResults -} -``` - -### 7.3 在插件中使用过滤功能 - -在异步插件的`doSearch`方法中,可以使用过滤功能: - -```go -// doSearch 实际的搜索实现 -func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) { - // ... 执行搜索逻辑 ... - - // 获取搜索结果 - results, err := actualSearch(client, keyword) - if err != nil { - return nil, err - } - - // 使用过滤功能过滤结果 - filteredResults := p.FilterResultsByKeyword(results, keyword) - - return filteredResults, nil -} -``` - -对于非异步插件,可以使用全局过滤函数: - -```go -// Search 执行搜索并返回结果 -func (p *MyPlugin) Search(keyword string) ([]model.SearchResult, error) { - // ... 执行搜索逻辑 ... - - // 获取结果 - results, err := someSearchFunction(keyword) - if err != nil { - return nil, err - } - - // 使用全局过滤函数过滤结果 - filteredResults := plugin.FilterResultsByKeyword(results, keyword) - - return filteredResults, nil -} -``` - -### 7.4 过滤功能的性能考虑 - -过滤操作可能会消耗一定的CPU资源,特别是当结果数量很大时。如果性能成为问题,可以考虑以下优化: - -1. **提前过滤**:在API返回大量结果时,先进行初步过滤 -2. **限制结果数量**:对于特别大的结果集,可以先限制数量再过滤 -3. **优化字符串处理**:使用更高效的字符串匹配算法 -4. **并行处理**:对大量结果进行并行过滤 - -## 8. 最佳实践 - -### 1. 错误处理 - -- 妥善处理HTTP请求错误 -- 解析失败时返回有意义的错误信息 -- 单个结果解析失败不应影响整体搜索 - -```go -if err != nil { - return nil, fmt.Errorf("请求失败: %w", err) -} -``` - -### 2. 并发控制 - -- 如果需要发起多个请求,使用并发控制 -- 使用信号量或工作池限制并发数 -- 确保所有goroutine都能正确退出 - -```go -// 创建信号量限制并发数 -semaphore := make(chan struct{}, maxConcurrent) - -// 使用信号量 -semaphore <- struct{}{} -defer func() { <-semaphore }() -``` - -### 3. 结果去重 - -- 在返回结果前进行初步去重 -- 使用map存储唯一标识符 -- 系统会在合并所有插件结果时进行最终去重 - -```go -// 使用map进行去重 -uniqueMap := make(map[string]Item) - -// 将去重后的结果转换为切片 -results := make([]Item, 0, len(uniqueMap)) -for _, item := range uniqueMap { - results = append(results, item) -} -``` - -### 4. 清理HTML标签 - -- 清理标题和内容中的HTML标签 -- 移除多余的空格和换行符 -- 保留有用的格式信息 - -```go -func cleanHTML(html string) string { - // 替换常见HTML标签 - replacements := map[string]string{ - "": "", - "": "", - "": "", - "": "", - } - - result := html - for tag, replacement := range replacements { - result = strings.Replace(result, tag, replacement, -1) - } - - return strings.TrimSpace(result) -} -``` - -### 5. 时间解析 - -- 正确解析资源的发布时间 -- 如果无法获取时间,使用零值 -- 不要使用当前时间代替缺失的时间 - -```go -// 尝试解析时间 -var datetime time.Time -if item.Time != "" { - parsedTime, err := time.Parse("2006-01-02 15:04:05", item.Time) - if err == nil { - datetime = parsedTime - } -} - -// 如果解析失败,使用零值 -if datetime.IsZero() { - datetime = time.Time{} -} -``` - -### 6. 扩展参数处理 - -- 正确处理ext参数,提供额外搜索功能 -- 始终检查ext是否为nil,避免空指针异常 -- 使用类型断言安全地获取参数值 -- 在处理ext参数时保持向后兼容性 - -```go -// 处理ext参数 -if ext != nil { - // 使用类型断言安全地获取参数 + // 2. 处理扩展参数 if titleEn, ok := ext["title_en"].(string); ok && titleEn != "" { - // 使用英文标题替换关键词 - searchKeyword = titleEn + url += "&title_en=" + url.QueryEscape(titleEn) } - // 处理年份参数 - if year, ok := ext["year"].(float64); ok && year > 0 { - // 将年份添加到搜索条件中 - searchKeyword = fmt.Sprintf("%s %d", searchKeyword, int(year)) - } else if yearStr, ok := ext["year"].(string); ok && yearStr != "" { - // 处理字符串形式的年份 - searchKeyword = fmt.Sprintf("%s %s", searchKeyword, yearStr) - } - - // 处理质量参数 - if quality, ok := ext["quality"].(string); ok && quality != "" { - // 将质量添加到搜索条件中 - searchKeyword = fmt.Sprintf("%s %s", searchKeyword, quality) - } -} -``` - -## 示例插件 - -以下是一个完整的示例插件实现: - -```go -package exampleplugin - -import ( - "pansou/util/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "pansou/model" - "pansou/plugin" -) - -// 在init函数中注册插件 -func init() { - plugin.RegisterGlobalPlugin(NewExamplePlugin()) -} - -const ( - // API端点 - ApiURL = "https://example.com/api/search" - - // 默认超时时间 - DefaultTimeout = 5 * time.Second -) - -// ExamplePlugin 示例插件 -type ExamplePlugin struct { - client *http.Client - timeout time.Duration -} - -// NewExamplePlugin 创建新的示例插件 -func NewExamplePlugin() *ExamplePlugin { - timeout := DefaultTimeout - - return &ExamplePlugin{ - client: &http.Client{ - Timeout: timeout, - }, - timeout: timeout, - } -} - -// Name 返回插件名称 -func (p *ExamplePlugin) Name() string { - return "exampleplugin" -} - -// Priority 返回插件优先级 -func (p *ExamplePlugin) Priority() int { - return 2 // 中等优先级 -} - -// Search 执行搜索并返回结果 -func (p *ExamplePlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 处理ext参数 - searchKeyword := keyword - if ext != nil { - // 使用类型断言安全地获取参数 - if titleEn, ok := ext["title_en"].(string); ok && titleEn != "" { - // 使用英文标题替换关键词 - searchKeyword = titleEn - } - } - - // 构建请求URL - reqURL := fmt.Sprintf("%s?q=%s", ApiURL, url.QueryEscape(searchKeyword)) - - // 发送请求 - req, err := http.NewRequest("GET", reqURL, nil) + // 3. 发送HTTP请求 + resp, err := client.Get(url) if err != nil { - return nil, fmt.Errorf("创建请求失败: %w", err) - } - - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - - // 发送请求 - resp, err := p.client.Do(req) - if err != nil { - return nil, fmt.Errorf("请求失败: %w", err) + return nil, err } defer resp.Body.Close() - // 读取响应体 - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("读取响应失败: %w", err) + // 4. 解析响应 + var apiResp APIResponse + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + return nil, err } - // 解析响应 - var apiResp ApiResponse - if err := json.Unmarshal(respBody, &apiResp); err != nil { - return nil, fmt.Errorf("解析响应失败: %w", err) - } - - // 转换为标准格式 - results := make([]model.SearchResult, 0, len(apiResp.Items)) - - for i, item := range apiResp.Items { - // 解析时间 - var datetime time.Time - if item.Time != "" { - parsedTime, err := time.Parse("2006-01-02 15:04:05", item.Time) - if err == nil { - datetime = parsedTime - } - } - - // 如果解析失败,使用零值 - if datetime.IsZero() { - datetime = time.Time{} - } - - // 创建链接 - link := model.Link{ - URL: item.URL, - Type: p.determineLinkType(item.URL), - Password: item.Password, - } - - // 创建唯一ID - uniqueID := fmt.Sprintf("exampleplugin-%d", i) - - // 创建搜索结果 + // 5. 转换为标准格式 + results := make([]model.SearchResult, 0, len(apiResp.Data)) + for _, item := range apiResp.Data { result := model.SearchResult{ - UniqueID: uniqueID, - Title: cleanHTML(item.Title), - Content: cleanHTML(item.Description), - Datetime: datetime, - Links: []model.Link{link}, + UniqueID: fmt.Sprintf("%s-%s", p.Name(), item.ID), + Title: item.Title, + Content: item.Description, + Datetime: item.CreateTime, + Tags: item.Tags, + Links: convertLinks(item.Links), // 转换链接格式 } - results = append(results, result) } - return results, nil + // 6. 关键词过滤 + return plugin.FilterResultsByKeyword(results, keyword), nil +} +``` + +### 3. 链接转换 + +```go +func convertLinks(apiLinks []APILink) []model.Link { + links := make([]model.Link, 0, len(apiLinks)) + for _, apiLink := range apiLinks { + link := model.Link{ + Type: determineCloudType(apiLink.URL), // 自动识别网盘类型 + URL: apiLink.URL, + Password: apiLink.Password, + } + links = append(links, link) + } + return links } -// determineLinkType 根据URL确定链接类型 -func (p *ExamplePlugin) determineLinkType(url string) string { - lowerURL := strings.ToLower(url) - +func determineCloudType(url string) string { switch { - case strings.Contains(lowerURL, "pan.baidu.com"): + case strings.Contains(url, "pan.baidu.com"): return "baidu" - case strings.Contains(lowerURL, "alipan.com") || strings.Contains(lowerURL, "aliyundrive.com"): + case strings.Contains(url, "aliyundrive.com"): return "aliyun" - case strings.Contains(lowerURL, "pan.xunlei.com"): - return "xunlei" - // ... 其他类型判断 + case strings.Contains(url, "pan.quark.cn"): + return "quark" default: return "others" } } - -// cleanHTML 清理HTML标签 -func cleanHTML(html string) string { - // 替换常见HTML标签 - replacements := map[string]string{ - "": "", - "": "", - "": "", - "": "", - } - - result := html - for tag, replacement := range replacements { - result = strings.Replace(result, tag, replacement, -1) - } - - return strings.TrimSpace(result) -} - -// ApiResponse API响应结构 -type ApiResponse struct { - Items []ApiItem `json:"items"` - Total int `json:"total"` -} - -// ApiItem API响应中的单个结果项 -type ApiItem struct { - Title string `json:"title"` - Description string `json:"description"` - URL string `json:"url"` - Password string `json:"password"` - Time string `json:"time"` -} ``` +## 高级特性 -## 插件缓存实现 - -### 1. 缓存概述 - -插件级缓存是对系统整体两级缓存(内存+磁盘)的补充,主要针对插件内部的API调用和数据处理进行优化,减少重复计算和网络请求,提高系统整体性能和响应速度。 - -### 2. 设计目标 - -1. **减少重复请求**:避免短时间内对同一资源的重复请求,降低外部API负载 -2. **提高响应速度**:通过缓存常用查询结果,减少网络延迟和处理时间 -3. **降低资源消耗**:减少CPU和网络资源的使用 -4. **保持数据新鲜度**:通过合理的缓存过期策略,平衡性能和数据时效性 -5. **线程安全**:支持并发访问,避免竞态条件 -6. **内存管理**:防止内存泄漏,控制缓存大小 - -### 3. 架构设计 - -#### 3.1 整体架构 - -插件缓存采用分层设计,主要包含以下组件: - -``` -┌─────────────────────────┐ -│ 插件缓存系统 │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 缓存存储层 │ -│ (sync.Map实现的内存缓存) │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 缓存管理层 │ -│ (缓存清理、过期策略、统计) │ -└───────────┬─────────────┘ - │ -┌───────────▼─────────────┐ -│ 缓存接口层 │ -│ (Load/Store操作封装) │ -└─────────────────────────┘ -``` - -#### 3.2 缓存类型 - -根据不同插件的需求,可以实现多种类型的缓存: - -1. **API响应缓存**:缓存外部API的响应结果 -2. **解析结果缓存**:缓存HTML解析、正则匹配等计算密集型操作的结果 -3. **链接提取缓存**:缓存从文本中提取的链接结果 -4. **元数据缓存**:缓存帖子ID、发布时间等元数据信息 - -### 4. 核心组件 - -#### 4.1 缓存存储 - -使用`sync.Map`实现线程安全的内存缓存: +### 1. 扩展参数处理 ```go -// 缓存相关变量 -var ( - // API响应缓存,键为"apiURL:keyword",值为缓存的响应 - apiResponseCache = sync.Map{} - - // 最后一次清理缓存的时间 - lastCacheCleanTime = time.Now() - - // 缓存有效期 - cacheTTL = 1 * time.Hour -) -``` - -#### 4.2 缓存结构 - -每个缓存项包含数据和时间戳,用于判断是否过期: - -```go -// 缓存响应结构 -type cachedResponse struct { - data interface{} // 缓存的数据 - timestamp time.Time // 缓存创建时间 +// 支持的扩展参数示例 +ext := map[string]interface{}{ + "title_en": "English Title", // 英文标题 + "is_all": true, // 全量搜索标志 + "year": 2023, // 年份限制 + "type": "movie", // 内容类型 } -``` -#### 4.3 缓存清理机制 - -定期清理过期缓存,防止内存泄漏: - -```go -// startCacheCleaner 启动一个定期清理缓存的goroutine -func startCacheCleaner() { - // 每小时清理一次缓存 - ticker := time.NewTicker(1 * time.Hour) - defer ticker.Stop() +// 在插件中处理 +func (p *MyPlugin) handleExtParams(ext map[string]interface{}) searchOptions { + opts := searchOptions{} - for range ticker.C { - // 清空所有缓存 - apiResponseCache = sync.Map{} - lastCacheCleanTime = time.Now() + if titleEn, ok := ext["title_en"].(string); ok { + opts.TitleEn = titleEn } + + if isAll, ok := ext["is_all"].(bool); ok { + opts.IsAll = isAll + } + + return opts } ``` -### 5. 实现方案 +### 2. 缓存策略 -#### 5.1 初始化缓存 +```go +// 设置缓存TTL +p.SetCacheTTL(2 * time.Hour) -在插件初始化时启动缓存清理机制: +// 手动缓存更新 +p.UpdateMainCache(cacheKey, results, ttl, true, keyword) +``` + +### 3. 错误处理 + +```go +func (p *MyPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + // 网络错误处理 + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("[%s] 网络请求失败: %w", p.Name(), err) + } + + // HTTP状态码检查 + if resp.StatusCode != 200 { + return nil, fmt.Errorf("[%s] HTTP错误: %d", p.Name(), resp.StatusCode) + } + + // JSON解析错误 + var apiResp APIResponse + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + return nil, fmt.Errorf("[%s] JSON解析失败: %w", p.Name(), err) + } + + // 业务逻辑错误 + if apiResp.Code != 0 { + return nil, fmt.Errorf("[%s] API错误: %s", p.Name(), apiResp.Message) + } + + return results, nil +} +``` + +## 性能优化 + +### 1. HTTP客户端优化 + +```go +// 使用连接池 +client := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + }, +} +``` + +### 2. 内存优化 + +```go +// 预分配切片容量 +results := make([]model.SearchResult, 0, expectedCount) + +// 及时释放大对象 +defer func() { + apiResp = APIResponse{} +}() +``` + +### 3. 并发控制 + +```go +// 使用插件内置的工作池,避免创建过多goroutine +// BaseAsyncPlugin 已经提供了工作池管理 +``` + +## 测试和调试 + +### 1. 单元测试 + +```go +func TestMyPlugin_Search(t *testing.T) { + plugin := &MyPlugin{ + BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("test", 3), + } + + results, err := plugin.Search("测试关键词", nil) + assert.NoError(t, err) + assert.NotEmpty(t, results) +} +``` + +### 2. 集成测试 + +```bash +# 使用API测试插件 +curl "http://localhost:8888/api/search?kw=测试&plugins=myplugin" +``` + +### 3. 性能测试 + +```bash +# 使用压力测试脚本 +python3 stress_test.py +``` + +## 部署和配置 + +### 1. 插件注册 + +确保在 `init()` 函数中注册插件: ```go func init() { - // 注册插件 - plugin.RegisterGlobalPlugin(NewPlugin()) - - // 启动缓存清理goroutine - go startCacheCleaner() -} -``` - -#### 5.2 缓存键生成 - -设计合理的缓存键,确保唯一性和高效查找: - -```go -// 生成缓存键 -cacheKey := fmt.Sprintf("%s:%s", apiURL, keyword) -``` - -对于复杂的缓存键,可以使用结构体: - -```go -// 缓存键结构 -type passwordCacheKey struct { - content string - url string -} -``` - -#### 5.3 缓存读取 - -在执行操作前先检查缓存: - -```go -// 检查缓存中是否已有结果 -if cachedItems, ok := apiResponseCache.Load(cacheKey); ok { - // 检查缓存是否过期 - cachedResult := cachedItems.(cachedResponse) - if time.Since(cachedResult.timestamp) < cacheTTL { - return cachedResult.items, nil + p := &MyPlugin{ + BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3), } + plugin.RegisterGlobalPlugin(p) } ``` -#### 5.4 缓存写入 +### 2. 环境配置 -操作完成后更新缓存: - -```go -// 缓存结果 -apiResponseCache.Store(cacheKey, cachedResponse{ - items: result, - timestamp: time.Now(), -}) +```bash +# 异步插件配置 +export ASYNC_PLUGIN_ENABLED=true +export ASYNC_RESPONSE_TIMEOUT=4 +export ASYNC_MAX_BACKGROUND_WORKERS=40 +export ASYNC_MAX_BACKGROUND_TASKS=200 ``` -#### 5.5 缓存过期策略 +### 3. 生产部署注意事项 -使用TTL(Time-To-Live)机制控制缓存过期: +1. **资源限制**: 根据服务器配置调整工作池大小 +2. **监控告警**: 监控插件响应时间和错误率 +3. **日志管理**: 合理设置日志级别,避免日志过多 +4. **缓存配置**: 根据数据更新频率调整缓存TTL -```go -// 检查缓存是否过期 -if time.Since(cachedResult.timestamp) < cacheTTL { - return cachedResult.items, nil -} -``` +## 现有插件参考 -### 6. 具体实现案例 +- **jikepan**: 即刻盘搜索 +- **pan666**: 666盘搜索 +- **hunhepan**: 混合盘搜索 +- **pansearch**: 盘搜索 +- **qupansou**: 去盘搜 +- **panta**: 盘塔搜索 -#### 6.1 API响应缓存(Hunhepan插件) - -```go -// searchAPI 向单个API发送请求 -func (p *HunhepanPlugin) searchAPI(apiURL, keyword string) ([]HunhepanItem, error) { - // 生成缓存键 - cacheKey := fmt.Sprintf("%s:%s", apiURL, keyword) - - // 检查缓存中是否已有结果 - if cachedItems, ok := apiResponseCache.Load(cacheKey); ok { - // 检查缓存是否过期 - cachedResult := cachedItems.(cachedResponse) - if time.Since(cachedResult.timestamp) < cacheTTL { - return cachedResult.items, nil - } - } - - // 构建请求并发送... - - // 缓存结果 - apiResponseCache.Store(cacheKey, cachedResponse{ - items: apiResp.Data.List, - timestamp: time.Now(), - }) - - return apiResp.Data.List, nil -} -``` - -#### 6.2 解析结果缓存(Panta插件) - -```go -// extractLinksFromElement 从HTML元素中提取链接 -func (p *PantaPlugin) extractLinksFromElement(s *goquery.Selection, yearFromTitle string) []model.Link { - // 创建缓存键 - html, _ := s.Html() - cacheKey := fmt.Sprintf("%s:%s", html, yearFromTitle) - - // 检查缓存 - if cachedLinks, ok := linkExtractCache.Load(cacheKey); ok { - return cachedLinks.([]model.Link) - } - - // 提取链接... - - // 缓存结果 - linkExtractCache.Store(cacheKey, links) - - return links -} -``` - -#### 6.3 元数据缓存(Panta插件) - -```go -// 从href中提取topicId - 使用缓存 -var topicID string -if cachedID, ok := topicIDCache.Load(href); ok { - topicID = cachedID.(string) -} else { - match := topicIDRegex.FindStringSubmatch(href) - if len(match) < 2 { - return - } - topicID = match[1] - topicIDCache.Store(href, topicID) -} -``` - -### 7. 性能优化 - -#### 7.1 缓存粒度控制 - -根据数据特性选择合适的缓存粒度: - -1. **粗粒度缓存**:缓存整个API响应,适合查询结果较小且稳定的场景 -2. **细粒度缓存**:缓存处理过程中的中间结果,适合复杂处理流程 - -#### 7.2 缓存预热 - -对于常用查询,可以实现缓存预热机制: - -```go -// 预热常用关键词的缓存 -func warmupCache() { - commonKeywords := []string{"电影", "音乐", "软件", "教程"} - for _, keyword := range commonKeywords { - go func(kw string) { - _, _ = searchAPI(apiURL, kw) - }(keyword) - } -} -``` - -#### 7.3 自适应TTL - -根据数据更新频率动态调整缓存有效期: - -```go -// 根据内容类型确定TTL -func determineTTL(contentType string) time.Duration { - switch contentType { - case "movie": - return 24 * time.Hour // 电影资源更新较慢 - case "news": - return 30 * time.Minute // 新闻更新较快 - default: - return 1 * time.Hour - } -} -``` - -### 8. 监控与统计 - -#### 8.1 缓存命中率统计 - -```go -var ( - cacheHits int64 - cacheMisses int64 -) - -// 记录缓存命中 -func recordCacheHit() { - atomic.AddInt64(&cacheHits, 1) -} - -// 记录缓存未命中 -func recordCacheMiss() { - atomic.AddInt64(&cacheMisses, 1) -} - -// 获取缓存命中率 -func getCacheHitRate() float64 { - hits := atomic.LoadInt64(&cacheHits) - misses := atomic.LoadInt64(&cacheMisses) - total := hits + misses - if total == 0 { - return 0 - } - return float64(hits) / float64(total) -} -``` - -#### 8.2 缓存大小监控 - -```go -// 估算缓存大小 -func estimateCacheSize() int64 { - var size int64 - apiResponseCache.Range(func(key, value interface{}) bool { - // 估算每个缓存项的大小 - cachedResp := value.(cachedResponse) - // 根据实际数据结构估算大小 - size += int64(len(fmt.Sprintf("%v", cachedResp.data))) * 2 // 粗略估计 - return true - }) - return size -} -``` - -### 9. 最佳实践 - -1. **选择性缓存**:只缓存频繁访问且计算成本高的数据 -2. **合理的TTL**:根据数据更新频率设置合适的过期时间 -3. **缓存键设计**:确保缓存键唯一且能反映所有影响结果的因素 -4. **错误处理**:不缓存错误响应,避免错误传播 -5. **缓存大小控制**:设置缓存项数量上限,避免内存溢出 -6. **并发安全**:使用线程安全的数据结构和原子操作 -7. **定期清理**:实现自动清理机制,避免内存泄漏 +## 最佳实践 +1. **命名规范**: 插件名使用小写字母和数字 +2. **优先级设置**: 1-2为高优先级,3为标准,4-5为低优先级 +3. **错误处理**: 详细的错误信息,便于调试 +4. **关键词过滤**: 使用 `FilterResultsByKeyword` 提高结果相关性 +5. **缓存友好**: 合理设置缓存TTL,避免频繁请求 +6. **资源清理**: 及时关闭连接和释放资源 \ No newline at end of file diff --git a/main.go b/main.go index e74a119..a26681b 100644 --- a/main.go +++ b/main.go @@ -122,8 +122,7 @@ func startServer() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - // 保存异步插件缓存 - plugin.SaveCacheToDisk() + // 异步插件本地缓存系统已移除 // 优雅关闭服务器 if err := srv.Shutdown(ctx); err != nil { diff --git a/model/plugin_result.go b/model/plugin_result.go new file mode 100644 index 0000000..5e6a092 --- /dev/null +++ b/model/plugin_result.go @@ -0,0 +1,32 @@ +package model + +import ( + "time" +) + +// PluginSearchResult 插件搜索结果 +type PluginSearchResult struct { + Results []SearchResult `json:"results"` // 搜索结果 + IsFinal bool `json:"is_final"` // 是否为最终完整结果 + Timestamp time.Time `json:"timestamp"` // 结果时间戳 + Source string `json:"source"` // 插件来源 + Message string `json:"message"` // 状态描述(可选) +} + +// IsEmpty 检查结果是否为空 +func (p *PluginSearchResult) IsEmpty() bool { + return len(p.Results) == 0 +} + +// Count 返回结果数量 +func (p *PluginSearchResult) Count() int { + return len(p.Results) +} + +// GetResults 获取搜索结果列表 +func (p *PluginSearchResult) GetResults() []SearchResult { + if p.Results == nil { + return []SearchResult{} + } + return p.Results +} \ No newline at end of file diff --git a/plugin/baseasyncplugin.go b/plugin/baseasyncplugin.go index 3b4cf5b..8f87f14 100644 --- a/plugin/baseasyncplugin.go +++ b/plugin/baseasyncplugin.go @@ -1,35 +1,22 @@ package plugin import ( - "compress/gzip" - "encoding/gob" - "pansou/util/json" "fmt" "net/http" - "os" - "path/filepath" - "sort" "strings" "sync" "sync/atomic" "time" - "math/rand" "pansou/config" "pansou/model" ) -// 缓存相关变量 +// 工作池和统计相关变量 var ( - // API响应缓存,键为关键词,值为缓存的响应 + // API响应缓存,键为关键词,值为缓存的响应(仅内存,不持久化) apiResponseCache = sync.Map{} - // 最后一次清理缓存的时间 - lastCacheCleanTime = time.Now() - - // 最后一次保存缓存的时间 - lastCacheSaveTime = time.Now() - // 工作池相关变量 backgroundWorkerPool chan struct{} backgroundTasksCount int32 = 0 @@ -43,24 +30,18 @@ var ( initialized bool = false initLock sync.Mutex - // 缓存保存锁,防止并发保存导致的竞态条件 - saveCacheLock sync.Mutex - // 默认配置值 defaultAsyncResponseTimeout = 4 * time.Second defaultPluginTimeout = 30 * time.Second - defaultCacheTTL = 1 * time.Hour + defaultCacheTTL = 1 * time.Hour // 恢复但仅用于内存缓存 defaultMaxBackgroundWorkers = 20 defaultMaxBackgroundTasks = 100 - // 缓存保存间隔 (2分钟) - cacheSaveInterval = 2 * time.Minute - // 缓存访问频率记录 cacheAccessCount = sync.Map{} ) -// 缓存响应结构 +// 缓存响应结构(仅内存,不持久化到磁盘) type cachedResponse struct { Results []model.SearchResult `json:"results"` Timestamp time.Time `json:"timestamp"` @@ -69,11 +50,6 @@ type cachedResponse struct { AccessCount int `json:"access_count"` } -// 可序列化的缓存结构,用于持久化 -type persistentCache struct { - Entries map[string]cachedResponse -} - // initAsyncPlugin 初始化异步插件配置 func initAsyncPlugin() { initLock.Lock() @@ -91,12 +67,7 @@ func initAsyncPlugin() { backgroundWorkerPool = make(chan struct{}, maxWorkers) - // 启动缓存清理和保存goroutine - go startCacheCleaner() - go startCachePersistence() - - // 尝试从磁盘加载缓存 - loadCacheFromDisk() + // 异步插件本地缓存系统已移除,现在只依赖主缓存系统 initialized = true } @@ -106,297 +77,11 @@ func InitAsyncPluginSystem() { initAsyncPlugin() } -// startCacheCleaner 启动一个定期清理缓存的goroutine -func startCacheCleaner() { - // 每小时清理一次缓存 - ticker := time.NewTicker(1 * time.Hour) - defer ticker.Stop() - - for range ticker.C { - cleanCache() - } -} +// 缓存清理和持久化系统已移除 +// 异步插件现在只负责搜索,缓存统一由主缓存系统管理 -// cleanCache 清理过期缓存 -func cleanCache() { - now := time.Now() - lastCacheCleanTime = now - - // 获取缓存TTL - cacheTTL := defaultCacheTTL - if config.AppConfig != nil && config.AppConfig.AsyncCacheTTLHours > 0 { - cacheTTL = time.Duration(config.AppConfig.AsyncCacheTTLHours) * time.Hour - } - - // 第一步:收集所有缓存项和它们的信息 - type cacheInfo struct { - key string - item cachedResponse - age time.Duration // 年龄(当前时间 - 创建时间) - idleTime time.Duration // 空闲时间(当前时间 - 最后访问时间) - score float64 // 缓存项得分(用于决定是否保留) - } - - var allItems []cacheInfo - var totalSize int = 0 - - apiResponseCache.Range(func(k, v interface{}) bool { - key := k.(string) - item := v.(cachedResponse) - age := now.Sub(item.Timestamp) - idleTime := now.Sub(item.LastAccess) - - // 如果超过TTL,直接删除 - if age > cacheTTL { - apiResponseCache.Delete(key) - return true - } - - // 计算大小(简单估算,每个结果占用1单位) - itemSize := len(item.Results) - totalSize += itemSize - - // 计算得分:访问次数 / (空闲时间的平方 * 年龄) - // 这样: - // - 访问频率高的得分高 - // - 最近访问的得分高 - // - 较新的缓存得分高 - score := float64(item.AccessCount) / (idleTime.Seconds() * idleTime.Seconds() * age.Seconds()) - - allItems = append(allItems, cacheInfo{ - key: key, - item: item, - age: age, - idleTime: idleTime, - score: score, - }) - - return true - }) - - // 获取缓存大小限制(默认10000项) - maxCacheSize := 10000 - if config.AppConfig != nil && config.AppConfig.CacheMaxSizeMB > 0 { - // 这里我们将MB转换为大致的项目数,假设每项平均1KB - maxCacheSize = config.AppConfig.CacheMaxSizeMB * 1024 - } - - // 如果缓存不超过限制,不需要清理 - if totalSize <= maxCacheSize { - return - } - - // 按得分排序(从低到高) - sort.Slice(allItems, func(i, j int) bool { - return allItems[i].score < allItems[j].score - }) - - // 需要删除的大小 - sizeToRemove := totalSize - maxCacheSize - - // 从得分低的开始删除,直到满足大小要求 - removedSize := 0 - removedCount := 0 - - for _, item := range allItems { - if removedSize >= sizeToRemove { - break - } - - apiResponseCache.Delete(item.key) - removedSize += len(item.item.Results) - removedCount++ - - // 最多删除总数的20% - if removedCount >= len(allItems) / 5 { - break - } - } - - fmt.Printf("缓存清理完成: 删除了%d个项目(总共%d个)\n", removedCount, len(allItems)) -} - -// startCachePersistence 启动定期保存缓存到磁盘的goroutine -func startCachePersistence() { - // 每2分钟保存一次缓存 - ticker := time.NewTicker(cacheSaveInterval) - defer ticker.Stop() - - for range ticker.C { - // 检查是否有缓存项需要保存 - if hasCacheItems() { - saveCacheToDisk() - } - } -} - -// hasCacheItems 检查是否有缓存项 -func hasCacheItems() bool { - hasItems := false - apiResponseCache.Range(func(k, v interface{}) bool { - hasItems = true - return false // 找到一个就停止遍历 - }) - return hasItems -} - -// getCachePath 获取缓存文件路径 -func getCachePath() string { - // 默认缓存路径 - cachePath := "cache" - - // 如果配置已加载,则使用配置中的缓存路径 - if config.AppConfig != nil && config.AppConfig.CachePath != "" { - cachePath = config.AppConfig.CachePath - } - - // 创建缓存目录(如果不存在) - if _, err := os.Stat(cachePath); os.IsNotExist(err) { - os.MkdirAll(cachePath, 0755) - } - - return filepath.Join(cachePath, "async_cache.gob") -} - -// saveCacheToDisk 将缓存保存到磁盘 -func saveCacheToDisk() { - // 使用互斥锁确保同一时间只有一个goroutine可以执行 - saveCacheLock.Lock() - defer saveCacheLock.Unlock() - - cacheFile := getCachePath() - lastCacheSaveTime = time.Now() - - // 创建临时文件 - tempFile := cacheFile + ".tmp" - file, err := os.Create(tempFile) - if err != nil { - fmt.Printf("创建缓存文件失败: %v\n", err) - return - } - defer file.Close() - - // 创建gzip压缩写入器 - gzipWriter := gzip.NewWriter(file) - defer gzipWriter.Close() - - // 将缓存内容转换为可序列化的结构 - persistent := persistentCache{ - Entries: make(map[string]cachedResponse), - } - - // 记录缓存项数量和总结果数 - itemCount := 0 - resultCount := 0 - - apiResponseCache.Range(func(k, v interface{}) bool { - key := k.(string) - value := v.(cachedResponse) - persistent.Entries[key] = value - itemCount++ - resultCount += len(value.Results) - return true - }) - - // 使用gob编码器保存 - encoder := gob.NewEncoder(gzipWriter) - if err := encoder.Encode(persistent); err != nil { - fmt.Printf("编码缓存失败: %v\n", err) - return - } - - // 确保所有数据已写入 - gzipWriter.Close() - file.Sync() - file.Close() - - // 使用原子重命名(这确保了替换是原子的,避免了缓存文件损坏) - if err := os.Rename(tempFile, cacheFile); err != nil { - fmt.Printf("重命名缓存文件失败: %v\n", err) - return - } - - // fmt.Printf("缓存已保存到磁盘,条目数: %d,结果总数: %d\n", itemCount, resultCount) -} - -// SaveCacheToDisk 导出的缓存保存函数,用于程序退出时调用 -func SaveCacheToDisk() { - if initialized { - // fmt.Println("程序退出,正在保存异步插件缓存...") - saveCacheToDisk() - // fmt.Println("异步插件缓存保存完成") - } -} - -// loadCacheFromDisk 从磁盘加载缓存 -func loadCacheFromDisk() { - cacheFile := getCachePath() - - // 检查缓存文件是否存在 - if _, err := os.Stat(cacheFile); os.IsNotExist(err) { - // fmt.Println("缓存文件不存在,跳过加载") - return - } - - // 打开缓存文件 - file, err := os.Open(cacheFile) - if err != nil { - // fmt.Printf("打开缓存文件失败: %v\n", err) - return - } - defer file.Close() - - // 创建gzip读取器 - gzipReader, err := gzip.NewReader(file) - if err != nil { - // 尝试作为非压缩文件读取(向后兼容) - file.Seek(0, 0) // 重置文件指针 - decoder := gob.NewDecoder(file) - var persistent persistentCache - if err := decoder.Decode(&persistent); err != nil { - fmt.Printf("解码缓存失败: %v\n", err) - return - } - loadCacheEntries(persistent) - return - } - defer gzipReader.Close() - - // 使用gob解码器加载 - var persistent persistentCache - decoder := gob.NewDecoder(gzipReader) - if err := decoder.Decode(&persistent); err != nil { - fmt.Printf("解码缓存失败: %v\n", err) - return - } - - loadCacheEntries(persistent) -} - -// loadCacheEntries 加载缓存条目到内存 -func loadCacheEntries(persistent persistentCache) { - // 获取缓存TTL,用于过滤过期项 - cacheTTL := defaultCacheTTL - if config.AppConfig != nil && config.AppConfig.AsyncCacheTTLHours > 0 { - cacheTTL = time.Duration(config.AppConfig.AsyncCacheTTLHours) * time.Hour - } - - now := time.Now() - loadedCount := 0 - totalResultCount := 0 - - // 将解码后的缓存加载到内存 - for key, value := range persistent.Entries { - // 只加载未过期的缓存 - if now.Sub(value.Timestamp) <= cacheTTL { - apiResponseCache.Store(key, value) - loadedCount++ - totalResultCount += len(value.Results) - } - } - - // fmt.Printf("从磁盘加载了%d条缓存(过滤后),包含%d个搜索结果\n", loadedCount, totalResultCount) -} +// 异步插件本地缓存系统已完全移除 +// 现在异步插件只负责搜索,缓存统一由主缓存系统管理 // acquireWorkerSlot 尝试获取工作槽 func acquireWorkerSlot() bool { @@ -442,7 +127,7 @@ func recordAsyncCompletion() { atomic.AddInt64(&asyncCompletions, 1) } -// recordCacheAccess 记录缓存访问次数,用于智能缓存策略 +// recordCacheAccess 记录缓存访问次数,用于智能缓存策略(仅内存) func recordCacheAccess(key string) { // 更新缓存项的访问时间和计数 if cached, ok := apiResponseCache.Load(key); ok { @@ -460,15 +145,18 @@ func recordCacheAccess(key string) { } } -// BaseAsyncPlugin 基础异步插件结构 +// BaseAsyncPlugin 基础异步插件结构(保留内存缓存,移除磁盘持久化) type BaseAsyncPlugin struct { - name string - priority int - client *http.Client // 用于短超时的客户端 - backgroundClient *http.Client // 用于长超时的客户端 - cacheTTL time.Duration // 缓存有效期 - mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数 - MainCacheKey string // 主缓存键,导出字段 + name string + priority int + client *http.Client // 用于短超时的客户端 + backgroundClient *http.Client // 用于长超时的客户端 + cacheTTL time.Duration // 内存缓存有效期 + mainCacheUpdater func(string, []model.SearchResult, time.Duration, bool, string) error // 主缓存更新函数(支持IsFinal参数,接收原始数据,最后参数为关键词) + MainCacheKey string // 主缓存键,导出字段 + currentKeyword string // 当前搜索的关键词,用于日志显示 + finalUpdateTracker map[string]bool // 追踪已更新的最终结果缓存 + finalUpdateMutex sync.RWMutex // 保护finalUpdateTracker的并发访问 } // NewBaseAsyncPlugin 创建基础异步插件 @@ -499,7 +187,8 @@ func NewBaseAsyncPlugin(name string, priority int) *BaseAsyncPlugin { backgroundClient: &http.Client{ Timeout: processingTimeout, }, - cacheTTL: cacheTTL, + cacheTTL: cacheTTL, + finalUpdateTracker: make(map[string]bool), // 初始化缓存更新追踪器 } } @@ -508,8 +197,13 @@ func (p *BaseAsyncPlugin) SetMainCacheKey(key string) { p.MainCacheKey = key } -// SetMainCacheUpdater 设置主缓存更新函数 -func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []byte, time.Duration) error) { +// SetCurrentKeyword 设置当前搜索关键词(用于日志显示) +func (p *BaseAsyncPlugin) SetCurrentKeyword(keyword string) { + p.currentKeyword = keyword +} + +// SetMainCacheUpdater 设置主缓存更新函数(修复后的签名,增加关键词参数) +func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []model.SearchResult, time.Duration, bool, string) error) { p.mainCacheUpdater = updater } @@ -611,8 +305,9 @@ func (p *BaseAsyncPlugin) AsyncSearch( AccessCount: 1, }) - // 更新主缓存系统 - p.updateMainCache(mainCacheKey, results) + // 🔧 工作池满时4秒内完成,这是完整结果 + fmt.Printf("[%s] 🕐 工作池满-直接完成: %v\n", p.name, time.Since(now)) + p.updateMainCacheWithFinal(mainCacheKey, results, true) return } @@ -625,7 +320,7 @@ func (p *BaseAsyncPlugin) AsyncSearch( select { case <-doneChan: // 已经响应,只更新缓存 - if err == nil && len(results) > 0 { + if err == nil { // 检查是否存在旧缓存 var accessCount int = 1 var lastAccess time.Time = now @@ -668,11 +363,10 @@ func (p *BaseAsyncPlugin) AsyncSearch( }) recordAsyncCompletion() - // 更新主缓存系统 - p.updateMainCache(mainCacheKey, results) + // 异步插件后台完成时更新主缓存(标记为最终结果) + p.updateMainCacheWithFinal(mainCacheKey, results, true) - // 更新缓存后立即触发保存 - go saveCacheToDisk() + // 异步插件本地缓存系统已移除 } default: // 尚未响应,发送结果 @@ -722,11 +416,11 @@ func (p *BaseAsyncPlugin) AsyncSearch( AccessCount: 1, }) - // 更新主缓存系统 - p.updateMainCache(mainCacheKey, results) + // 🔧 4秒内正常完成,这是完整的最终结果 + fmt.Printf("[%s] 🕐 4秒内正常完成: %v\n", p.name, time.Since(now)) + p.updateMainCacheWithFinal(mainCacheKey, results, true) - // 更新缓存后立即触发保存 - go saveCacheToDisk() + // 异步插件本地缓存系统已移除 } } }() @@ -746,6 +440,8 @@ func (p *BaseAsyncPlugin) AsyncSearch( close(doneChan) return nil, err case <-time.After(responseTimeout): + // 插件响应超时,后台继续处理(优化完成,日志简化) + // 响应超时,返回空结果,后台继续处理 go func() { defer close(doneChan) @@ -772,11 +468,233 @@ func (p *BaseAsyncPlugin) AsyncSearch( AccessCount: 1, }) + // 🔧 修复:4秒超时时也要更新主缓存,标记为部分结果(空结果) + p.updateMainCacheWithFinal(mainCacheKey, []model.SearchResult{}, false) + // fmt.Printf("[%s] 响应超时,后台继续处理: %s\n", p.name, pluginSpecificCacheKey) return []model.SearchResult{}, nil } } +// AsyncSearchWithResult 异步搜索方法,返回PluginSearchResult +func (p *BaseAsyncPlugin) AsyncSearchWithResult( + keyword string, + searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), + mainCacheKey string, + ext map[string]interface{}, +) (model.PluginSearchResult, error) { + // 确保ext不为nil + if ext == nil { + ext = make(map[string]interface{}) + } + + now := time.Now() + + // 修改缓存键,确保包含插件名称 + pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword) + + // 检查缓存 + if cachedItems, ok := apiResponseCache.Load(pluginSpecificCacheKey); ok { + cachedResult := cachedItems.(cachedResponse) + + // 缓存完全有效(未过期且完整) + if time.Since(cachedResult.Timestamp) < p.cacheTTL && cachedResult.Complete { + recordCacheHit() + recordCacheAccess(pluginSpecificCacheKey) + + // 如果缓存接近过期(已用时间超过TTL的80%),在后台刷新缓存 + if time.Since(cachedResult.Timestamp) > (p.cacheTTL * 4 / 5) { + go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey, ext) + } + + return model.PluginSearchResult{ + Results: cachedResult.Results, + IsFinal: cachedResult.Complete, + Timestamp: cachedResult.Timestamp, + Source: p.name, + Message: "从缓存获取", + }, nil + } + + // 缓存已过期但有结果,启动后台刷新,同时返回旧结果 + if len(cachedResult.Results) > 0 { + recordCacheHit() + recordCacheAccess(pluginSpecificCacheKey) + + // 标记为部分过期 + if time.Since(cachedResult.Timestamp) >= p.cacheTTL { + // 在后台刷新缓存 + go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey, ext) + } + + return model.PluginSearchResult{ + Results: cachedResult.Results, + IsFinal: false, // 🔥 过期数据标记为非最终结果 + Timestamp: cachedResult.Timestamp, + Source: p.name, + Message: "缓存已过期,后台刷新中", + }, nil + } + } + + recordCacheMiss() + + // 创建通道 + resultChan := make(chan []model.SearchResult, 1) + errorChan := make(chan error, 1) + doneChan := make(chan struct{}) + + // 启动后台处理 + go func() { + defer func() { + select { + case <-doneChan: + default: + close(doneChan) + } + }() + + // 尝试获取工作槽 + if !acquireWorkerSlot() { + // 工作池已满,使用快速响应客户端直接处理 + results, err := searchFunc(p.client, keyword, ext) + if err != nil { + select { + case errorChan <- err: + default: + } + return + } + + select { + case resultChan <- results: + default: + } + return + } + defer releaseWorkerSlot() + + // 使用长超时客户端进行搜索 + results, err := searchFunc(p.backgroundClient, keyword, ext) + if err != nil { + select { + case errorChan <- err: + default: + } + } else { + select { + case resultChan <- results: + default: + } + } + }() + + // 等待结果或超时 + responseTimeout := defaultAsyncResponseTimeout + if config.AppConfig != nil { + responseTimeout = config.AppConfig.AsyncResponseTimeoutDur + } + + select { + case results := <-resultChan: + // 不直接关闭,让defer处理 + + // 缓存结果 + apiResponseCache.Store(pluginSpecificCacheKey, cachedResponse{ + Results: results, + Timestamp: now, + Complete: true, // 🔥 及时完成,标记为完整结果 + LastAccess: now, + AccessCount: 1, + }) + + // 🔧 恢复主缓存更新:使用统一的GOB序列化 + // 传递原始数据,由主程序负责序列化 + if mainCacheKey != "" && p.mainCacheUpdater != nil { + err := p.mainCacheUpdater(mainCacheKey, results, p.cacheTTL, true, p.currentKeyword) + if err != nil { + fmt.Printf("❌ [%s] 及时完成缓存更新失败: %s | 错误: %v\n", p.name, mainCacheKey, err) + } + } + + return model.PluginSearchResult{ + Results: results, + IsFinal: true, // 🔥 及时完成,最终结果 + Timestamp: now, + Source: p.name, + Message: "搜索完成", + }, nil + + case err := <-errorChan: + // 不直接关闭,让defer处理 + return model.PluginSearchResult{}, err + + case <-time.After(responseTimeout): + // 🔥 超时处理:返回空结果,后台继续处理 + go p.completeSearchInBackground(keyword, searchFunc, pluginSpecificCacheKey, mainCacheKey, doneChan, ext) + + // 存储临时缓存(标记为不完整) + apiResponseCache.Store(pluginSpecificCacheKey, cachedResponse{ + Results: []model.SearchResult{}, + Timestamp: now, + Complete: false, // 🔥 标记为不完整 + LastAccess: now, + AccessCount: 1, + }) + + return model.PluginSearchResult{ + Results: []model.SearchResult{}, + IsFinal: false, // 🔥 超时返回,非最终结果 + Timestamp: now, + Source: p.name, + Message: "处理中,后台继续...", + }, nil + } +} + +// completeSearchInBackground 后台完成搜索 +func (p *BaseAsyncPlugin) completeSearchInBackground( + keyword string, + searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), + pluginCacheKey string, + mainCacheKey string, + doneChan chan struct{}, + ext map[string]interface{}, +) { + defer func() { + select { + case <-doneChan: + default: + close(doneChan) + } + }() + + // 执行完整搜索 + results, err := searchFunc(p.backgroundClient, keyword, ext) + if err != nil { + return + } + + // 更新插件缓存 + now := time.Now() + apiResponseCache.Store(pluginCacheKey, cachedResponse{ + Results: results, + Timestamp: now, + Complete: true, // 🔥 标记为完整结果 + LastAccess: now, + AccessCount: 1, + }) + + // 🔧 恢复主缓存更新:使用统一的GOB序列化 + // 传递原始数据,由主程序负责序列化 + if mainCacheKey != "" && p.mainCacheUpdater != nil { + err := p.mainCacheUpdater(mainCacheKey, results, p.cacheTTL, true, p.currentKeyword) + if err != nil { + fmt.Printf("❌ [%s] 后台完成缓存更新失败: %s | 错误: %v\n", p.name, mainCacheKey, err) + } + } +} + // refreshCacheInBackground 在后台刷新缓存 func (p *BaseAsyncPlugin) refreshCacheInBackground( keyword string, @@ -834,41 +752,57 @@ func (p *BaseAsyncPlugin) refreshCacheInBackground( AccessCount: oldCache.AccessCount, }) - // 更新主缓存系统 - // 使用传入的originalCacheKey,直接传递给updateMainCache - p.updateMainCache(originalCacheKey, mergedResults) + // 🔥 异步插件后台刷新完成时更新主缓存(标记为最终结果) + p.updateMainCacheWithFinal(originalCacheKey, mergedResults, true) // 记录刷新时间 refreshTime := time.Since(refreshStart) fmt.Printf("[%s] 后台刷新完成: %s (耗时: %v, 新项目: %d, 合并项目: %d)\n", p.name, cacheKey, refreshTime, len(results), len(mergedResults)) - // 添加随机延迟,避免多个goroutine同时调用saveCacheToDisk - time.Sleep(time.Duration(100+rand.Intn(500)) * time.Millisecond) - - // 更新缓存后立即触发保存 - go saveCacheToDisk() + // 异步插件本地缓存系统已移除 } -// updateMainCache 更新主缓存系统 +// updateMainCache 更新主缓存系统(兼容性方法,默认IsFinal=true) func (p *BaseAsyncPlugin) updateMainCache(cacheKey string, results []model.SearchResult) { + p.updateMainCacheWithFinal(cacheKey, results, true) +} + +// updateMainCacheWithFinal 更新主缓存系统,支持IsFinal参数 +func (p *BaseAsyncPlugin) updateMainCacheWithFinal(cacheKey string, results []model.SearchResult, isFinal bool) { // 如果主缓存更新函数为空或缓存键为空,直接返回 if p.mainCacheUpdater == nil || cacheKey == "" { return } - // 序列化结果 - data, err := json.Marshal(results) - if err != nil { - fmt.Printf("[%s] 序列化结果失败: %v\n", p.name, err) - return + // 🔥 防止重复更新导致LRU缓存淘汰的优化 + // 如果是最终结果,检查缓存中是否已经存在相同的最终结果 + // 使用全局缓存键追踪已更新的最终结果 + updateKey := fmt.Sprintf("final_updated_%s_%s", p.name, cacheKey) + + if isFinal { + if p.hasUpdatedFinalCache(updateKey) { + // 已经更新过最终结果,跳过重复更新 + return + } + // 标记已更新 + p.markFinalCacheUpdated(updateKey) + } else { + // 🔧 修复:如果已经有最终结果,不允许部分结果覆盖 + if p.hasUpdatedFinalCache(updateKey) { + return + } } - // 调用主缓存更新函数 - if err := p.mainCacheUpdater(cacheKey, data, p.cacheTTL); err != nil { - fmt.Printf("[%s] 更新主缓存失败: %v\n", p.name, err) - } else { - fmt.Printf("[%s] 成功更新主缓存: %s\n", p.name, cacheKey) + // 缓存更新时机验证(优化完成,日志简化) + + // 🔧 恢复异步插件缓存更新,使用修复后的统一序列化 + // 传递原始数据,由主程序负责GOB序列化 + if p.mainCacheUpdater != nil { + err := p.mainCacheUpdater(cacheKey, results, p.cacheTTL, isFinal, p.currentKeyword) + if err != nil { + fmt.Printf("❌ [%s] 主缓存更新失败: %s | 错误: %v\n", p.name, cacheKey, err) + } } } @@ -913,4 +847,40 @@ func (p *BaseAsyncPlugin) FilterResultsByKeyword(results []model.SearchResult, k // GetClient 返回短超时客户端 func (p *BaseAsyncPlugin) GetClient() *http.Client { return p.client +} + +// hasUpdatedFinalCache 检查是否已经更新过指定的最终结果缓存 +func (p *BaseAsyncPlugin) hasUpdatedFinalCache(updateKey string) bool { + p.finalUpdateMutex.RLock() + defer p.finalUpdateMutex.RUnlock() + return p.finalUpdateTracker[updateKey] +} + +// markFinalCacheUpdated 标记已更新指定的最终结果缓存 +func (p *BaseAsyncPlugin) markFinalCacheUpdated(updateKey string) { + p.finalUpdateMutex.Lock() + defer p.finalUpdateMutex.Unlock() + p.finalUpdateTracker[updateKey] = true +} + +// 全局序列化器引用(由主程序设置) +var globalCacheSerializer interface { + Serialize(interface{}) ([]byte, error) + Deserialize([]byte, interface{}) error +} + +// SetGlobalCacheSerializer 设置全局缓存序列化器(由主程序调用) +func SetGlobalCacheSerializer(serializer interface { + Serialize(interface{}) ([]byte, error) + Deserialize([]byte, interface{}) error +}) { + globalCacheSerializer = serializer +} + +// getEnhancedCacheSerializer 获取增强缓存的序列化器 +func getEnhancedCacheSerializer() interface { + Serialize(interface{}) ([]byte, error) + Deserialize([]byte, interface{}) error +} { + return globalCacheSerializer } \ No newline at end of file diff --git a/plugin/hdr4k/hdr4k.go b/plugin/hdr4k/hdr4k.go index 419c728..cea420b 100644 --- a/plugin/hdr4k/hdr4k.go +++ b/plugin/hdr4k/hdr4k.go @@ -106,9 +106,18 @@ func NewHdr4kAsyncPlugin() *Hdr4kAsyncPlugin { } } -// Search 执行搜索并返回结果 +// Search 执行搜索并返回结果(兼容性方法) func (p *Hdr4kAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext) + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err + } + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *Hdr4kAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) } // doSearch 实际的搜索实现 diff --git a/plugin/hunhepan/hunhepan.go b/plugin/hunhepan/hunhepan.go index 8fc01a8..1f5c419 100644 --- a/plugin/hunhepan/hunhepan.go +++ b/plugin/hunhepan/hunhepan.go @@ -42,10 +42,18 @@ func NewHunhepanAsyncPlugin() *HunhepanAsyncPlugin { } } -// Search 执行搜索并返回结果 +// Search 执行搜索并返回结果(兼容性方法) func (p *HunhepanAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 使用保存的主缓存键,传递ext参数但不使用 - return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext) + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err + } + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *HunhepanAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) } // doSearch 实际的搜索实现 diff --git a/plugin/jikepan/jikepan.go b/plugin/jikepan/jikepan.go index b16274e..4ad235a 100644 --- a/plugin/jikepan/jikepan.go +++ b/plugin/jikepan/jikepan.go @@ -35,10 +35,18 @@ func NewJikepanAsyncV2Plugin() *JikepanAsyncV2Plugin { } } -// Search 执行搜索并返回结果 +// Search 执行搜索并返回结果(兼容性方法) func (p *JikepanAsyncV2Plugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 使用保存的主缓存键,传递ext参数但不使用 - return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext) + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err + } + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *JikepanAsyncV2Plugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) } // doSearch 实际的搜索实现 diff --git a/plugin/pan666/pan666.go b/plugin/pan666/pan666.go index 8249a84..f5e18d1 100644 --- a/plugin/pan666/pan666.go +++ b/plugin/pan666/pan666.go @@ -55,10 +55,18 @@ func NewPan666AsyncPlugin() *Pan666AsyncPlugin { } } -// Search 执行搜索并返回结果 +// Search 执行搜索并返回结果(兼容性方法) func (p *Pan666AsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 使用保存的主缓存键,传递ext参数但不使用 - return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext) + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err + } + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *Pan666AsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) } // doSearch 实际的搜索实现 diff --git a/plugin/pansearch/pansearch.go b/plugin/pansearch/pansearch.go index 2393732..64cd8af 100644 --- a/plugin/pansearch/pansearch.go +++ b/plugin/pansearch/pansearch.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "net" "net/http" "net/url" "regexp" @@ -96,9 +95,9 @@ var ( buildIdMutex sync.RWMutex ) -// PanSearchPlugin 盘搜插件 -type PanSearchPlugin struct { - client *http.Client +// PanSearchAsyncPlugin 盘搜异步插件 +type PanSearchAsyncPlugin struct { + *plugin.BaseAsyncPlugin timeout time.Duration maxResults int maxConcurrent int @@ -231,42 +230,18 @@ func (wp *WorkerPool) Close() { } } -// NewPanSearchPlugin 创建新的盘搜插件 -func NewPanSearchPlugin() *PanSearchPlugin { +// NewPanSearchPlugin 创建新的盘搜异步插件 +func NewPanSearchPlugin() *PanSearchAsyncPlugin { timeout := DefaultTimeout - - // 创建自定义 Transport 以优化连接池 - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 60 * time.Second, - DualStack: true, - }).DialContext, - ForceAttemptHTTP2: true, - MaxIdleConns: MaxIdleConns, - MaxIdleConnsPerHost: MaxIdleConnsPerHost, - MaxConnsPerHost: MaxConnsPerHost, - IdleConnTimeout: IdleConnTimeout, - TLSHandshakeTimeout: TLSHandshakeTimeout, - ExpectContinueTimeout: ExpectContinueTimeout, - WriteBufferSize: WriteBufferSize, - ReadBufferSize: ReadBufferSize, - DisableKeepAlives: false, - } - maxConcurrent := MaxConcurrent - p := &PanSearchPlugin{ - client: &http.Client{ - Transport: transport, - Timeout: timeout, - }, - timeout: timeout, - maxResults: MaxResults, - maxConcurrent: maxConcurrent, - retries: MaxRetries, - workerPool: NewWorkerPool(maxConcurrent), // 初始化工作池 + p := &PanSearchAsyncPlugin{ + BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("pansearch", 4), + timeout: timeout, + maxResults: MaxResults, + maxConcurrent: maxConcurrent, + retries: MaxRetries, + workerPool: NewWorkerPool(maxConcurrent), // 初始化工作池 } // 初始化时预热获取 buildId @@ -284,7 +259,7 @@ func NewPanSearchPlugin() *PanSearchPlugin { } // startBuildIdUpdater 启动一个定期更新 buildId 的后台协程 -func (p *PanSearchPlugin) startBuildIdUpdater() { +func (p *PanSearchAsyncPlugin) startBuildIdUpdater() { // 每10分钟更新一次 buildId ticker := time.NewTicker(10 * time.Minute) defer ticker.Stop() @@ -295,7 +270,7 @@ func (p *PanSearchPlugin) startBuildIdUpdater() { } // updateBuildId 更新 buildId 缓存 -func (p *PanSearchPlugin) updateBuildId() { +func (p *PanSearchAsyncPlugin) updateBuildId() { // 创建带超时的上下文 ctx, cancel := context.WithTimeout(context.Background(), p.timeout) defer cancel() @@ -315,7 +290,7 @@ func (p *PanSearchPlugin) updateBuildId() { req.Header.Set("Upgrade-Insecure-Requests", "1") req.Header.Set("Cache-Control", "max-age=0") - resp, err := p.client.Do(req) + resp, err := p.GetClient().Do(req) if err != nil { fmt.Printf("请求失败: %v\n", err) return @@ -380,17 +355,17 @@ func extractBuildId(body string) string { } // Name 返回插件名称 -func (p *PanSearchPlugin) Name() string { +func (p *PanSearchAsyncPlugin) Name() string { return "pansearch" } // Priority 返回插件优先级 -func (p *PanSearchPlugin) Priority() int { +func (p *PanSearchAsyncPlugin) Priority() int { return 3 // 中等优先级 } // getBuildId 获取buildId,优先使用缓存 -func (p *PanSearchPlugin) getBuildId() (string, error) { +func (p *PanSearchAsyncPlugin) getBuildId() (string, error) { // 检查缓存是否有效 buildIdMutex.RLock() if buildIdCache != "" && time.Since(buildIdCacheTime) < BuildIdCacheDuration*time.Minute { @@ -442,7 +417,7 @@ func (p *PanSearchPlugin) getBuildId() (string, error) { time.Sleep(backoffTime) } - resp, respErr = p.client.Do(req) + resp, respErr = p.GetClient().Do(req) if respErr == nil && resp.StatusCode == 200 { break } @@ -504,7 +479,7 @@ func (p *PanSearchPlugin) getBuildId() (string, error) { } // getBaseURL 获取完整的API基础URL -func (p *PanSearchPlugin) getBaseURL() (string, error) { +func (p *PanSearchAsyncPlugin) getBaseURL(client *http.Client) (string, error) { buildId, err := p.getBuildId() if err != nil { return "", err @@ -513,28 +488,30 @@ func (p *PanSearchPlugin) getBaseURL() (string, error) { return fmt.Sprintf(BaseURLTemplate, buildId), nil } -// Search 执行搜索并返回结果 -func (p *PanSearchPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 生成缓存键 - cacheKey := keyword - - // 检查缓存中是否已有结果 - if cachedItems, ok := searchResultCache.Load(cacheKey); ok { - // 检查缓存是否过期 - cachedResult := cachedItems.(cachedResponse) - if time.Since(cachedResult.timestamp) < cacheTTL { - return cachedResult.results, nil - } +// Search 执行搜索并返回结果(兼容性方法) +func (p *PanSearchAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err } - + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *PanSearchAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) +} + +// doSearch 执行具体的搜索逻辑 +func (p *PanSearchAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { // 获取API基础URL - baseURL, err := p.getBaseURL() + baseURL, err := p.getBaseURL(client) if err != nil { return nil, fmt.Errorf("获取API基础URL失败: %w", err) } // 1. 发起首次请求获取total和第一页数据 - firstPageResults, total, err := p.fetchFirstPage(keyword, baseURL) + firstPageResults, total, err := p.fetchFirstPage(keyword, baseURL, client) if err != nil { // 如果返回404错误,可能是buildId过期,尝试强制刷新buildId if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "Not Found") { @@ -547,13 +524,13 @@ func (p *PanSearchPlugin) Search(keyword string, ext map[string]interface{}) ([] buildIdMutex.Unlock() // 重新获取buildId - baseURL, err = p.getBaseURL() + baseURL, err = p.getBaseURL(client) if err != nil { return nil, fmt.Errorf("刷新buildId失败: %w", err) } // 重试请求 - firstPageResults, total, err = p.fetchFirstPage(keyword, baseURL) + firstPageResults, total, err = p.fetchFirstPage(keyword, baseURL, client) if err != nil { return nil, fmt.Errorf("刷新buildId后获取首页仍然失败: %w", err) } @@ -573,7 +550,7 @@ func (p *PanSearchPlugin) Search(keyword string, ext map[string]interface{}) ([] results := p.convertResults(allResults, keyword) // 缓存结果 - searchResultCache.Store(cacheKey, cachedResponse{ + searchResultCache.Store(keyword, cachedResponse{ results: results, timestamp: time.Now(), }) @@ -589,7 +566,7 @@ func (p *PanSearchPlugin) Search(keyword string, ext map[string]interface{}) ([] results := p.convertResults(allResults, keyword) // 缓存结果 - searchResultCache.Store(cacheKey, cachedResponse{ + searchResultCache.Store(keyword, cachedResponse{ results: results, timestamp: time.Now(), }) @@ -775,7 +752,7 @@ CollectResults: results := p.convertResults(allResults, keyword) // 缓存结果(即使超时也缓存已获取的结果) - searchResultCache.Store(cacheKey, cachedResponse{ + searchResultCache.Store(keyword, cachedResponse{ results: results, timestamp: time.Now(), }) @@ -790,7 +767,7 @@ ProcessResults: results := p.convertResults(allResults, keyword) // 缓存结果(即使有错误也缓存已获取的结果) - searchResultCache.Store(cacheKey, cachedResponse{ + searchResultCache.Store(keyword, cachedResponse{ results: results, timestamp: time.Now(), }) @@ -803,7 +780,7 @@ ProcessResults: results := p.convertResults(uniqueResults, keyword) // 缓存结果 - searchResultCache.Store(cacheKey, cachedResponse{ + searchResultCache.Store(keyword, cachedResponse{ results: results, timestamp: time.Now(), }) @@ -812,7 +789,7 @@ ProcessResults: } // fetchFirstPage 获取第一页结果和总数 -func (p *PanSearchPlugin) fetchFirstPage(keyword string, baseURL string) ([]PanSearchItem, int, error) { +func (p *PanSearchAsyncPlugin) fetchFirstPage(keyword string, baseURL string, client *http.Client) ([]PanSearchItem, int, error) { // 构建请求URL reqURL := fmt.Sprintf("%s?keyword=%s&offset=0", baseURL, url.QueryEscape(keyword)) @@ -836,7 +813,7 @@ func (p *PanSearchPlugin) fetchFirstPage(keyword string, baseURL string) ([]PanS req.Header.Set("Pragma", "no-cache") // 发送请求 - resp, err := p.client.Do(req) + resp, err := client.Do(req) if err != nil { return nil, 0, fmt.Errorf("请求失败: %w", err) } @@ -871,7 +848,7 @@ func (p *PanSearchPlugin) fetchFirstPage(keyword string, baseURL string) ([]PanS } // fetchPage 获取指定偏移量的页面 -func (p *PanSearchPlugin) fetchPage(keyword string, offset int, baseURL string) ([]PanSearchItem, error) { +func (p *PanSearchAsyncPlugin) fetchPage(keyword string, offset int, baseURL string) ([]PanSearchItem, error) { // 构建请求URL reqURL := fmt.Sprintf("%s?keyword=%s&offset=%d", baseURL, url.QueryEscape(keyword), offset) @@ -895,7 +872,7 @@ func (p *PanSearchPlugin) fetchPage(keyword string, offset int, baseURL string) req.Header.Set("Pragma", "no-cache") // 发送请求 - resp, err := p.client.Do(req) + resp, err := p.GetClient().Do(req) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } @@ -926,7 +903,7 @@ func (p *PanSearchPlugin) fetchPage(keyword string, offset int, baseURL string) } // deduplicateItems 去重处理 -func (p *PanSearchPlugin) deduplicateItems(items []PanSearchItem) []PanSearchItem { +func (p *PanSearchAsyncPlugin) deduplicateItems(items []PanSearchItem) []PanSearchItem { // 使用map进行去重,键为资源ID uniqueMap := make(map[int]PanSearchItem) @@ -944,7 +921,7 @@ func (p *PanSearchPlugin) deduplicateItems(items []PanSearchItem) []PanSearchIte } // convertResults 将API响应转换为标准SearchResult格式 -func (p *PanSearchPlugin) convertResults(items []PanSearchItem, keyword string) []model.SearchResult { +func (p *PanSearchAsyncPlugin) convertResults(items []PanSearchItem, keyword string) []model.SearchResult { results := make([]model.SearchResult, 0, len(items)) for _, item := range items { diff --git a/plugin/panta/panta.go b/plugin/panta/panta.go index 43ad4f5..fd6f7ae 100644 --- a/plugin/panta/panta.go +++ b/plugin/panta/panta.go @@ -3,7 +3,6 @@ package panta import ( "context" "fmt" - "net" "net/http" "net/url" "pansou/model" @@ -148,10 +147,9 @@ const ( maxBackoff = 5000 ) -// PantaPlugin 是PanTa网站的搜索插件实现 -type PantaPlugin struct { - // HTTP客户端,用于发送请求 - client *http.Client +// PantaAsyncPlugin 是PanTa网站的异步搜索插件实现 +type PantaAsyncPlugin struct { + *plugin.BaseAsyncPlugin // 并发控制 maxConcurrency int @@ -163,63 +161,30 @@ type PantaPlugin struct { lastAdjustTime time.Time } -// 确保PantaPlugin实现了SearchPlugin接口 -var _ plugin.SearchPlugin = (*PantaPlugin)(nil) +// 确保PantaAsyncPlugin实现了AsyncSearchPlugin接口 +var _ plugin.AsyncSearchPlugin = (*PantaAsyncPlugin)(nil) // 在包初始化时注册插件 func init() { // 创建并注册插件实例 - plugin.RegisterGlobalPlugin(NewPantaPlugin()) + plugin.RegisterGlobalPlugin(NewPantaAsyncPlugin()) } -// NewPantaPlugin 创建一个新的PanTa插件实例 -func NewPantaPlugin() *PantaPlugin { - // 创建一个带有更多配置的HTTP传输层 - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 60 * time.Second, // 增加保持活动时间 - DualStack: true, // 启用IPv4/IPv6双栈 - }).DialContext, - MaxIdleConns: 200, // 增加最大空闲连接数 - IdleConnTimeout: 120 * time.Second, // 增加空闲连接超时 - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - MaxIdleConnsPerHost: 50, // 增加每个主机的最大空闲连接数 - MaxConnsPerHost: 100, // 限制每个主机的最大连接数 - DisableKeepAlives: false, // 确保启用长连接 - ForceAttemptHTTP2: true, // 尝试使用HTTP/2 - WriteBufferSize: 16 * 1024, // 增加写缓冲区大小 - ReadBufferSize: 16 * 1024, // 增加读缓冲区大小 - } - - // 创建HTTP客户端 - client := &http.Client{ - Timeout: time.Duration(defaultTimeout) * time.Second, - Transport: transport, - // 禁用重定向,因为我们只关心初始响应 - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } - +// NewPantaAsyncPlugin 创建一个新的PanTa异步插件实例 +func NewPantaAsyncPlugin() *PantaAsyncPlugin { // 启动定期清理缓存的goroutine go startCacheCleaner() // 创建插件实例 - plugin := &PantaPlugin{ - client: client, + p := &PantaAsyncPlugin{ + BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("panta", 2), maxConcurrency: defaultConcurrency, currentConcurrency: defaultConcurrency, - responseTimes: make([]time.Duration, 0, 100), + responseTimes: make([]time.Duration, 0, 10), lastAdjustTime: time.Now(), } - // 启动自适应并发控制 - go plugin.startConcurrencyAdjuster() - - return plugin + return p } // startCacheCleaner 启动一个定期清理缓存的goroutine @@ -242,17 +207,31 @@ func startCacheCleaner() { } // Name 返回插件名称 -func (p *PantaPlugin) Name() string { +func (p *PantaAsyncPlugin) Name() string { return pluginName } // Priority 返回插件优先级 -func (p *PantaPlugin) Priority() int { +func (p *PantaAsyncPlugin) Priority() int { return defaultPriority } -// Search 执行搜索并返回结果 -func (p *PantaPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { +// Search 执行搜索并返回结果(兼容性方法) +func (p *PantaAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err + } + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *PantaAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) +} + +// doSearch 执行具体的搜索逻辑 +func (p *PantaAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { // 对关键词进行URL编码 encodedKeyword := url.QueryEscape(keyword) @@ -279,7 +258,7 @@ func (p *PantaPlugin) Search(keyword string, ext map[string]interface{}) ([]mode req.Header.Set("Cache-Control", "max-age=0") // 使用带重试的请求方法发送HTTP请求 - resp, err := p.doRequestWithRetry(req) + resp, err := p.doRequestWithRetry(req, client) if err != nil { return nil, fmt.Errorf("请求PanTa搜索页面失败: %v", err) } @@ -297,7 +276,7 @@ func (p *PantaPlugin) Search(keyword string, ext map[string]interface{}) ([]mode } // 解析搜索结果 - results, err := p.parseSearchResults(doc) + results, err := p.parseSearchResults(doc, client) if err != nil { return nil, err } @@ -309,7 +288,7 @@ func (p *PantaPlugin) Search(keyword string, ext map[string]interface{}) ([]mode } // parseSearchResults 使用goquery解析搜索结果 -func (p *PantaPlugin) parseSearchResults(doc *goquery.Document) ([]model.SearchResult, error) { +func (p *PantaAsyncPlugin) parseSearchResults(doc *goquery.Document, client *http.Client) ([]model.SearchResult, error) { var results []model.SearchResult // 创建信号量控制并发数,使用自适应并发数 @@ -428,7 +407,7 @@ func (p *PantaPlugin) parseSearchResults(doc *goquery.Document) ([]model.SearchR time.Sleep(backoffTime) } - threadLinks, err := p.fetchThreadLinks(topicID) + threadLinks, err := p.fetchThreadLinks(topicID, client) if err == nil && len(threadLinks) > 0 { foundLinks = threadLinks break @@ -494,7 +473,7 @@ func (p *PantaPlugin) parseSearchResults(doc *goquery.Document) ([]model.SearchR } // extractLinksFromElement 从元素中提取链接 -func (p *PantaPlugin) extractLinksFromElement(s *goquery.Selection, yearFromTitle string) []model.Link { +func (p *PantaAsyncPlugin) extractLinksFromElement(s *goquery.Selection, yearFromTitle string) []model.Link { // 创建缓存键 html, _ := s.Html() cacheKey := fmt.Sprintf("%s_%s", html, yearFromTitle) @@ -611,7 +590,7 @@ func (p *PantaPlugin) extractLinksFromElement(s *goquery.Selection, yearFromTitl } // fetchThreadLinks 获取帖子详情页中的链接 -func (p *PantaPlugin) fetchThreadLinks(topicID string) ([]model.Link, error) { +func (p *PantaAsyncPlugin) fetchThreadLinks(topicID string, client *http.Client) ([]model.Link, error) { // 检查缓存中是否已有结果 if cachedLinks, ok := threadLinksCache.Load(topicID); ok { return cachedLinks.([]model.Link), nil @@ -640,7 +619,7 @@ func (p *PantaPlugin) fetchThreadLinks(topicID string) ([]model.Link, error) { req.Header.Set("Cache-Control", "max-age=0") // 使用带重试的请求方法发送HTTP请求 - resp, err := p.doRequestWithRetry(req) + resp, err := p.doRequestWithRetry(req, client) if err != nil { return nil, err } @@ -1218,7 +1197,7 @@ func isNetDiskLink(url string) bool { } // startConcurrencyAdjuster 启动一个定期调整并发数的goroutine -func (p *PantaPlugin) startConcurrencyAdjuster() { +func (p *PantaAsyncPlugin) startConcurrencyAdjuster() { ticker := time.NewTicker(concurrencyAdjustInterval * time.Second) defer ticker.Stop() @@ -1228,7 +1207,7 @@ func (p *PantaPlugin) startConcurrencyAdjuster() { } // adjustConcurrency 根据响应时间调整并发数 -func (p *PantaPlugin) adjustConcurrency() { +func (p *PantaAsyncPlugin) adjustConcurrency() { p.responseTimesMutex.Lock() defer p.responseTimesMutex.Unlock() @@ -1258,7 +1237,7 @@ func (p *PantaPlugin) adjustConcurrency() { } // recordResponseTime 记录请求响应时间 -func (p *PantaPlugin) recordResponseTime(d time.Duration) { +func (p *PantaAsyncPlugin) recordResponseTime(d time.Duration) { p.responseTimesMutex.Lock() defer p.responseTimesMutex.Unlock() @@ -1272,7 +1251,7 @@ func (p *PantaPlugin) recordResponseTime(d time.Duration) { } // doRequestWithRetry 发送HTTP请求,带重试机制 -func (p *PantaPlugin) doRequestWithRetry(req *http.Request) (*http.Response, error) { +func (p *PantaAsyncPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) { var resp *http.Response var err error var startTime time.Time @@ -1294,7 +1273,7 @@ func (p *PantaPlugin) doRequestWithRetry(req *http.Request) (*http.Response, err startTime = time.Now() // 发送请求 - resp, err = p.client.Do(req) + resp, err = client.Do(req) // 记录响应时间 responseTime := time.Since(startTime) diff --git a/plugin/panyq/panyq.go b/plugin/panyq/panyq.go index b004597..e118d62 100644 --- a/plugin/panyq/panyq.go +++ b/plugin/panyq/panyq.go @@ -173,8 +173,12 @@ func (p *PanyqPlugin) Search(keyword string, ext map[string]interface{}) ([]mode } } - // 使用基础异步插件的搜索方法 - results, err := p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext) + // 使用新的异步搜索方法 + result, err := p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) + if err != nil { + return nil, err + } + results := result.Results // 如果搜索成功,缓存结果 if err == nil && len(results) > 0 { @@ -186,6 +190,11 @@ func (p *PanyqPlugin) Search(keyword string, ext map[string]interface{}) ([]mode return results, err } +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *PanyqPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) +} + // doSearch 实际的搜索实现 func (p *PanyqPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { if DebugLog { diff --git a/plugin/plugin.go b/plugin/plugin.go index 12d19f3..65dba7e 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,34 +1,42 @@ package plugin import ( + "net/http" "strings" "sync" "pansou/model" ) -// 全局插件注册表 +// 全局异步插件注册表 var ( - globalRegistry = make(map[string]SearchPlugin) + globalRegistry = make(map[string]AsyncSearchPlugin) globalRegistryLock sync.RWMutex ) -// SearchPlugin 搜索插件接口 -type SearchPlugin interface { +// AsyncSearchPlugin 异步搜索插件接口 +type AsyncSearchPlugin interface { // Name 返回插件名称 Name() string - // Search 执行搜索并返回结果 - // ext参数用于传递额外的搜索参数,插件可以根据需要使用或忽略 - Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) - - // Priority 返回插件优先级(可选,用于控制结果排序) + // Priority 返回插件优先级 Priority() int + + // AsyncSearch 异步搜索方法 + AsyncSearch(keyword string, searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), mainCacheKey string, ext map[string]interface{}) ([]model.SearchResult, error) + + // SetMainCacheKey 设置主缓存键 + SetMainCacheKey(key string) + + // SetCurrentKeyword 设置当前搜索关键词(用于日志显示) + SetCurrentKeyword(keyword string) + + // Search 兼容性方法(内部调用AsyncSearch) + Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) } -// RegisterGlobalPlugin 注册插件到全局注册表 -// 这个函数应该在每个插件的init函数中被调用 -func RegisterGlobalPlugin(plugin SearchPlugin) { +// RegisterGlobalPlugin 注册异步插件到全局注册表 +func RegisterGlobalPlugin(plugin AsyncSearchPlugin) { if plugin == nil { return } @@ -44,12 +52,12 @@ func RegisterGlobalPlugin(plugin SearchPlugin) { globalRegistry[name] = plugin } -// GetRegisteredPlugins 获取所有已注册的插件 -func GetRegisteredPlugins() []SearchPlugin { +// GetRegisteredPlugins 获取所有已注册的异步插件 +func GetRegisteredPlugins() []AsyncSearchPlugin { globalRegistryLock.RLock() defer globalRegistryLock.RUnlock() - plugins := make([]SearchPlugin, 0, len(globalRegistry)) + plugins := make([]AsyncSearchPlugin, 0, len(globalRegistry)) for _, plugin := range globalRegistry { plugins = append(plugins, plugin) } @@ -57,37 +65,36 @@ func GetRegisteredPlugins() []SearchPlugin { return plugins } -// PluginManager 插件管理器 +// PluginManager 异步插件管理器 type PluginManager struct { - plugins []SearchPlugin + plugins []AsyncSearchPlugin } -// NewPluginManager 创建新的插件管理器 +// NewPluginManager 创建新的异步插件管理器 func NewPluginManager() *PluginManager { return &PluginManager{ - plugins: make([]SearchPlugin, 0), + plugins: make([]AsyncSearchPlugin, 0), } } -// RegisterPlugin 注册插件 -func (pm *PluginManager) RegisterPlugin(plugin SearchPlugin) { +// RegisterPlugin 注册异步插件 +func (pm *PluginManager) RegisterPlugin(plugin AsyncSearchPlugin) { pm.plugins = append(pm.plugins, plugin) } -// RegisterAllGlobalPlugins 注册所有全局插件 +// RegisterAllGlobalPlugins 注册所有全局异步插件 func (pm *PluginManager) RegisterAllGlobalPlugins() { for _, plugin := range GetRegisteredPlugins() { pm.RegisterPlugin(plugin) } } -// GetPlugins 获取所有注册的插件 -func (pm *PluginManager) GetPlugins() []SearchPlugin { +// GetPlugins 获取所有注册的异步插件 +func (pm *PluginManager) GetPlugins() []AsyncSearchPlugin { return pm.plugins -} +} // FilterResultsByKeyword 根据关键词过滤搜索结果的全局辅助函数 -// 供非BaseAsyncPlugin类型的插件使用 func FilterResultsByKeyword(results []model.SearchResult, keyword string) []model.SearchResult { if keyword == "" { return results diff --git a/plugin/qupansou/qupansou.go b/plugin/qupansou/qupansou.go index 0200ac3..436b00f 100644 --- a/plugin/qupansou/qupansou.go +++ b/plugin/qupansou/qupansou.go @@ -59,50 +59,53 @@ const ( DefaultPageSize = 1000 ) -// QuPanSouPlugin 趣盘搜插件 -type QuPanSouPlugin struct { - client *http.Client +// QuPanSouAsyncPlugin 趣盘搜异步插件 +type QuPanSouAsyncPlugin struct { + *plugin.BaseAsyncPlugin timeout time.Duration } -// NewQuPanSouPlugin 创建新的趣盘搜插件 -func NewQuPanSouPlugin() *QuPanSouPlugin { +// NewQuPanSouPlugin 创建新的趣盘搜异步插件 +func NewQuPanSouPlugin() *QuPanSouAsyncPlugin { timeout := DefaultTimeout - return &QuPanSouPlugin{ - client: &http.Client{ - Timeout: timeout, - }, - timeout: timeout, + return &QuPanSouAsyncPlugin{ + BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("qupansou", 3), + timeout: timeout, } } +// 确保QuPanSouAsyncPlugin实现了AsyncSearchPlugin接口 +var _ plugin.AsyncSearchPlugin = (*QuPanSouAsyncPlugin)(nil) + // Name 返回插件名称 -func (p *QuPanSouPlugin) Name() string { +func (p *QuPanSouAsyncPlugin) Name() string { return "qupansou" } // Priority 返回插件优先级 -func (p *QuPanSouPlugin) Priority() int { +func (p *QuPanSouAsyncPlugin) Priority() int { return 3 // 中等优先级 } -// Search 执行搜索并返回结果 -func (p *QuPanSouPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 生成缓存键 - cacheKey := keyword - - // 检查缓存中是否已有结果 - if cachedItems, ok := apiResponseCache.Load(cacheKey); ok { - // 检查缓存是否过期 - cachedResult := cachedItems.(cachedResponse) - if time.Since(cachedResult.timestamp) < cacheTTL { - return cachedResult.results, nil - } +// Search 执行搜索并返回结果(兼容性方法) +func (p *QuPanSouAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err } - + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *QuPanSouAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) +} + +// doSearch 执行具体的搜索逻辑 +func (p *QuPanSouAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { // 发送API请求 - items, err := p.searchAPI(keyword) + items, err := p.searchAPI(keyword, client) if err != nil { return nil, fmt.Errorf("qupansou API error: %w", err) } @@ -110,12 +113,6 @@ func (p *QuPanSouPlugin) Search(keyword string, ext map[string]interface{}) ([]m // 转换为标准格式 results := p.convertResults(items) - // 缓存结果 - apiResponseCache.Store(cacheKey, cachedResponse{ - results: results, - timestamp: time.Now(), - }) - return results, nil } @@ -126,7 +123,7 @@ type cachedResponse struct { } // searchAPI 向API发送请求 -func (p *QuPanSouPlugin) searchAPI(keyword string) ([]QuPanSouItem, error) { +func (p *QuPanSouAsyncPlugin) searchAPI(keyword string, client *http.Client) ([]QuPanSouItem, error) { // 构建请求体 reqBody := map[string]interface{}{ "style": "get", @@ -168,7 +165,7 @@ func (p *QuPanSouPlugin) searchAPI(keyword string) ([]QuPanSouItem, error) { req.Header.Set("Referer", "https://pan.funletu.com/") // 发送请求 - resp, err := p.client.Do(req) + resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } @@ -195,7 +192,7 @@ func (p *QuPanSouPlugin) searchAPI(keyword string) ([]QuPanSouItem, error) { } // convertResults 将API响应转换为标准SearchResult格式 -func (p *QuPanSouPlugin) convertResults(items []QuPanSouItem) []model.SearchResult { +func (p *QuPanSouAsyncPlugin) convertResults(items []QuPanSouItem) []model.SearchResult { results := make([]model.SearchResult, 0, len(items)) for _, item := range items { @@ -248,7 +245,7 @@ func (p *QuPanSouPlugin) convertResults(items []QuPanSouItem) []model.SearchResu } // determineLinkType 根据URL确定链接类型 -func (p *QuPanSouPlugin) determineLinkType(url string) string { +func (p *QuPanSouAsyncPlugin) determineLinkType(url string) string { lowerURL := strings.ToLower(url) if strings.Contains(lowerURL, "pan.baidu.com") { diff --git a/plugin/susu/susu.go b/plugin/susu/susu.go index 51c2fda..4f6a3d0 100644 --- a/plugin/susu/susu.go +++ b/plugin/susu/susu.go @@ -103,10 +103,18 @@ func NewSusuAsyncPlugin() *SusuAsyncPlugin { } } -// Search 执行搜索并返回结果 +// Search 执行搜索并返回结果(兼容性方法) func (p *SusuAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 使用保存的主缓存键,传递ext参数但不使用 - return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext) + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err + } + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *SusuAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) } // doSearch 实际的搜索实现 diff --git a/plugin/xuexizhinan/xuexizhinan.go b/plugin/xuexizhinan/xuexizhinan.go index 1b00bd1..dcbc272 100644 --- a/plugin/xuexizhinan/xuexizhinan.go +++ b/plugin/xuexizhinan/xuexizhinan.go @@ -92,10 +92,18 @@ func startCacheCleaner() { } } -// Search 执行搜索并返回结果 +// Search 执行搜索并返回结果(兼容性方法) func (p *XuexizhinanPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { - // 使用保存的主缓存键 - return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext) + result, err := p.SearchWithResult(keyword, ext) + if err != nil { + return nil, err + } + return result.Results, nil +} + +// SearchWithResult 执行搜索并返回包含IsFinal标记的结果 +func (p *XuexizhinanPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) { + return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext) } // doSearch 实际的搜索实现 diff --git a/service/search_service.go b/service/search_service.go index a124b58..13bb7f8 100644 --- a/service/search_service.go +++ b/service/search_service.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "io/ioutil" "net/http" "sort" @@ -21,6 +22,37 @@ import ( // 优先关键词列表 var priorityKeywords = []string{"合集", "系列", "全", "完", "最新", "附"} +// extractKeywordFromCacheKey 从缓存键中提取关键词(简化版) +func extractKeywordFromCacheKey(cacheKey string) string { + // 这是一个简化的实现,实际中我们会通过传递来获得关键词 + // 为了演示,这里返回简化的显示 + return "搜索关键词" +} + +// logAsyncCacheWithKeyword 异步缓存日志输出辅助函数(带关键词) +func logAsyncCacheWithKeyword(keyword, cacheKey string, format string, args ...interface{}) { + // 检查配置开关 + if config.AppConfig == nil || !config.AppConfig.AsyncLogEnabled { + return + } + + // 构建显示的关键词信息 + displayKeyword := keyword + if displayKeyword == "" { + displayKeyword = "未知" + } + + // 将缓存键替换为简化版本+关键词 + shortKey := cacheKey + if len(cacheKey) > 8 { + shortKey = cacheKey[:8] + "..." + } + + // 替换格式字符串中的缓存键 + enhancedFormat := strings.Replace(format, cacheKey, fmt.Sprintf("%s(关键词:%s)", shortKey, displayKeyword), 1) + fmt.Printf(enhancedFormat, args...) +} + // 全局缓存实例和缓存是否初始化标志 var ( enhancedTwoLevelCache *cache.EnhancedTwoLevelCache @@ -39,6 +71,102 @@ func init() { } } +// mergeSearchResults 智能合并搜索结果,去重并保留最完整的信息 +func mergeSearchResults(existing []model.SearchResult, newResults []model.SearchResult) []model.SearchResult { + // 使用map进行去重和合并,以UniqueID作为唯一标识 + resultMap := make(map[string]model.SearchResult) + + // 先添加现有结果 + for _, result := range existing { + key := generateResultKey(result) + resultMap[key] = result + } + + // 合并新结果,如果UniqueID相同则选择信息更完整的 + for _, newResult := range newResults { + key := generateResultKey(newResult) + if existingResult, exists := resultMap[key]; exists { + // 选择信息更完整的结果 + resultMap[key] = selectBetterResult(existingResult, newResult) + } else { + // 新结果,直接添加 + resultMap[key] = newResult + } + } + + // 转换回切片 + merged := make([]model.SearchResult, 0, len(resultMap)) + for _, result := range resultMap { + merged = append(merged, result) + } + + // 按时间排序(最新的在前) + sort.Slice(merged, func(i, j int) bool { + return merged[i].Datetime.After(merged[j].Datetime) + }) + + return merged +} + +// generateResultKey 生成结果的唯一标识键 +func generateResultKey(result model.SearchResult) string { + // 使用UniqueID作为主要标识,如果没有则使用MessageID,最后使用标题 + if result.UniqueID != "" { + return result.UniqueID + } + if result.MessageID != "" { + return result.MessageID + } + return fmt.Sprintf("title_%s_%s", result.Title, result.Channel) +} + +// selectBetterResult 选择信息更完整的结果 +func selectBetterResult(existing, new model.SearchResult) model.SearchResult { + // 计算信息完整度得分 + existingScore := calculateCompletenessScore(existing) + newScore := calculateCompletenessScore(new) + + if newScore > existingScore { + return new + } + return existing +} + +// calculateCompletenessScore 计算结果信息的完整度得分 +func calculateCompletenessScore(result model.SearchResult) int { + score := 0 + + // 有UniqueID加分 + if result.UniqueID != "" { + score += 10 + } + + // 有链接信息加分 + if len(result.Links) > 0 { + score += 5 + // 每个链接额外加分 + score += len(result.Links) + } + + // 有内容加分 + if result.Content != "" { + score += 3 + } + + // 标题长度加分(更详细的标题) + score += len(result.Title) / 10 + + // 有频道信息加分 + if result.Channel != "" { + score += 2 + } + + // 有标签加分 + score += len(result.Tags) + + return score +} + // SearchService 搜索服务 type SearchService struct { pluginManager *plugin.PluginManager @@ -71,9 +199,91 @@ func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCach return } - // 创建缓存更新函数 - cacheUpdater := func(key string, data []byte, ttl time.Duration) error { - return mainCache.Set(key, data, ttl) + // 🔧 设置全局序列化器,确保异步插件与主程序使用相同的序列化格式 + serializer := mainCache.GetSerializer() + if serializer != nil { + plugin.SetGlobalCacheSerializer(serializer) + } + + // 创建缓存更新函数(支持IsFinal参数)- 接收原始数据并与现有缓存合并 + cacheUpdater := func(key string, newResults []model.SearchResult, ttl time.Duration, isFinal bool, keyword string) error { + // 🔧 获取现有缓存数据进行合并 + var finalResults []model.SearchResult + if existingData, hit, err := mainCache.Get(key); err == nil && hit { + var existingResults []model.SearchResult + if err := mainCache.GetSerializer().Deserialize(existingData, &existingResults); err == nil { + // 合并新旧结果,去重保留最完整的数据 + finalResults = mergeSearchResults(existingResults, newResults) + if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled { + displayKey := key[:8] + "..." + if keyword != "" { + fmt.Printf("🔄 [异步插件] 缓存合并: %s(关键词:%s) | 原有: %d + 新增: %d = 合并后: %d\n", + displayKey, keyword, len(existingResults), len(newResults), len(finalResults)) + } else { + fmt.Printf("🔄 [异步插件] 缓存合并: %s | 原有: %d + 新增: %d = 合并后: %d\n", + key, len(existingResults), len(newResults), len(finalResults)) + } + } + } else { + // 反序列化失败,使用新结果 + finalResults = newResults + if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled { + displayKey := key[:8] + "..." + if keyword != "" { + fmt.Printf("⚠️ [异步插件] 缓存反序列化失败,使用新结果: %s(关键词:%s) | 结果数: %d\n", displayKey, keyword, len(newResults)) + } else { + fmt.Printf("⚠️ [异步插件] 缓存反序列化失败,使用新结果: %s | 结果数: %d\n", key, len(newResults)) + } + } + } + } else { + // 无现有缓存,直接使用新结果 + finalResults = newResults + if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled { + displayKey := key[:8] + "..." + if keyword != "" { + fmt.Printf("📝 [异步插件] 初始缓存创建: %s(关键词:%s) | 结果数: %d\n", displayKey, keyword, len(newResults)) + } else { + fmt.Printf("📝 [异步插件] 初始缓存创建: %s | 结果数: %d\n", key, len(newResults)) + } + } + } + + // 🔧 序列化合并后的结果 + data, err := mainCache.GetSerializer().Serialize(finalResults) + if err != nil { + fmt.Printf("❌ [缓存更新] 序列化失败: %s | 错误: %v\n", key, err) + return err + } + + // 🔥 根据IsFinal参数选择缓存更新策略 + if isFinal { + // 最终结果:更新内存+磁盘缓存 + if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled { + displayKey := key[:8] + "..." + if keyword != "" { + fmt.Printf("📝 [异步插件] 最终结果缓存更新: %s(关键词:%s) | 结果数: %d | 数据长度: %d\n", + displayKey, keyword, len(finalResults), len(data)) + } else { + fmt.Printf("📝 [异步插件] 最终结果缓存更新: %s | 结果数: %d | 数据长度: %d\n", + key, len(finalResults), len(data)) + } + } + return mainCache.SetBothLevels(key, data, ttl) + } else { + // 部分结果:仅更新内存缓存 + if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled { + displayKey := key[:8] + "..." + if keyword != "" { + fmt.Printf("📝 [异步插件] 部分结果缓存更新: %s(关键词:%s) | 结果数: %d | 数据长度: %d\n", + displayKey, keyword, len(finalResults), len(data)) + } else { + fmt.Printf("📝 [异步插件] 部分结果缓存更新: %s | 结果数: %d | 数据长度: %d\n", + key, len(finalResults), len(data)) + } + } + return mainCache.SetMemoryOnly(key, data, ttl) + } } // 获取所有插件 @@ -81,8 +291,8 @@ func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCach // 遍历所有插件,找出异步插件 for _, p := range plugins { - // 检查插件是否实现了SetMainCacheUpdater方法 - if asyncPlugin, ok := p.(interface{ SetMainCacheUpdater(func(string, []byte, time.Duration) error) }); ok { + // 检查插件是否实现了SetMainCacheUpdater方法(修复后的签名,增加关键词参数) + if asyncPlugin, ok := p.(interface{ SetMainCacheUpdater(func(string, []model.SearchResult, time.Duration, bool, string) error) }); ok { // 注入缓存更新函数 asyncPlugin.SetMainCacheUpdater(cacheUpdater) } @@ -893,6 +1103,7 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef // 生成缓存键 cacheKey := cache.GeneratePluginCacheKey(keyword, plugins) + // 如果未启用强制刷新,尝试从缓存获取结果 if !forceRefresh && cacheInitialized && config.AppConfig.CacheEnabled { var data []byte @@ -906,19 +1117,30 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef // 如果磁盘缓存比内存缓存更新,会自动更新内存缓存并返回最新数据 data, hit, err = enhancedTwoLevelCache.Get(cacheKey) + // 🔍 添加缓存状态调试日志 + displayKey := cacheKey[:8] + "..." + fmt.Printf("🔍 [主服务] 缓存检查: %s(关键词:%s) | 命中: %v | 错误: %v | 数据长度: %d\n", + displayKey, keyword, hit, err, len(data)) + if err == nil && hit { var results []model.SearchResult if err := enhancedTwoLevelCache.GetSerializer().Deserialize(data, &results); err == nil { // 返回缓存数据 + displayKey := cacheKey[:8] + "..." + fmt.Printf("✅ [主服务] 缓存命中返回: %s(关键词:%s) | 结果数: %d\n", displayKey, keyword, len(results)) return results, nil + } else { + displayKey := cacheKey[:8] + "..." + fmt.Printf("❌ [主服务] 缓存反序列化失败: %s(关键词:%s) | 错误: %v\n", displayKey, keyword, err) } } } } // 缓存未命中或强制刷新,执行实际搜索 + // 获取所有可用插件 - var availablePlugins []plugin.SearchPlugin + var availablePlugins []plugin.AsyncSearchPlugin if s.pluginManager != nil { allPlugins := s.pluginManager.GetPlugins() @@ -966,32 +1188,20 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef for _, p := range availablePlugins { plugin := p // 创建副本,避免闭包问题 tasks = append(tasks, func() interface{} { - // 检查插件是否为异步插件 - if asyncPlugin, ok := plugin.(interface { - AsyncSearch(keyword string, searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), mainCacheKey string, ext map[string]interface{}) ([]model.SearchResult, error) - SetMainCacheKey(string) - }); ok { - // 先设置主缓存键 - asyncPlugin.SetMainCacheKey(cacheKey) - - // 是异步插件,调用AsyncSearch方法并传递主缓存键和ext参数 - results, err := asyncPlugin.AsyncSearch(keyword, func(client *http.Client, kw string, extParams map[string]interface{}) ([]model.SearchResult, error) { - // 这里使用插件的Search方法作为搜索函数,传递ext参数 - return plugin.Search(kw, extParams) - }, cacheKey, ext) - - if err != nil { - return nil - } - return results - } else { - // 不是异步插件,直接调用Search方法,传递ext参数 - results, err := plugin.Search(keyword, ext) - if err != nil { - return nil - } - return results + // 设置主缓存键和当前关键词 + plugin.SetMainCacheKey(cacheKey) + plugin.SetCurrentKeyword(keyword) + + // 调用异步插件的AsyncSearch方法 + results, err := plugin.AsyncSearch(keyword, func(client *http.Client, kw string, extParams map[string]interface{}) ([]model.SearchResult, error) { + // 使用插件的Search方法作为搜索函数 + return plugin.Search(kw, extParams) + }, cacheKey, ext) + + if err != nil { + return nil } + return results }) } @@ -1007,61 +1217,33 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef } } - // 异步缓存结果 + // 🔧 恢复主程序缓存更新:确保最终合并结果被正确缓存 if cacheInitialized && config.AppConfig.CacheEnabled { - go func(res []model.SearchResult) { + go func(res []model.SearchResult, kw string, key string) { ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute - // 使用增强版缓存 + // 使用增强版缓存,确保与异步插件使用相同的序列化器 if enhancedTwoLevelCache != nil { data, err := enhancedTwoLevelCache.GetSerializer().Serialize(res) if err != nil { + fmt.Printf("❌ [主程序] 缓存序列化失败: %s | 错误: %v\n", key, err) return } - enhancedTwoLevelCache.Set(cacheKey, data, ttl) + + // 主程序最后更新,覆盖可能有问题的异步插件缓存 + enhancedTwoLevelCache.Set(key, data, ttl) + if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled { + fmt.Printf("📝 [主程序] 缓存更新完成: %s | 结果数: %d | 数据长度: %d\n", + key, len(res), len(data)) + } } - }(allResults) + }(allResults, keyword, cacheKey) } return allResults, nil } -// 合并搜索结果 -func mergeSearchResults(tgResults, pluginResults []model.SearchResult) []model.SearchResult { - // 预估合并后的结果数量 - totalSize := len(tgResults) + len(pluginResults) - if totalSize == 0 { - return []model.SearchResult{} - } - - // 创建结果映射,用于去重 - resultMap := make(map[string]model.SearchResult, totalSize) - - // 添加TG搜索结果 - for _, result := range tgResults { - resultMap[result.UniqueID] = result - } - - // 添加或更新插件搜索结果(如果有重复,保留较新的) - for _, result := range pluginResults { - if existing, ok := resultMap[result.UniqueID]; ok { - // 如果已存在,保留较新的 - if result.Datetime.After(existing.Datetime) { - resultMap[result.UniqueID] = result - } - } else { - resultMap[result.UniqueID] = result - } - } - - // 转换回切片 - mergedResults := make([]model.SearchResult, 0, len(resultMap)) - for _, result := range resultMap { - mergedResults = append(mergedResults, result) - } - - return mergedResults -} + // GetPluginManager 获取插件管理器 func (s *SearchService) GetPluginManager() *plugin.PluginManager { diff --git a/util/cache/enhanced_two_level_cache.go b/util/cache/enhanced_two_level_cache.go index 055dfeb..5d46a49 100644 --- a/util/cache/enhanced_two_level_cache.go +++ b/util/cache/enhanced_two_level_cache.go @@ -33,6 +33,9 @@ func NewEnhancedTwoLevelCache() (*EnhancedTwoLevelCache, error) { // 创建序列化器 serializer := NewGobSerializer() + // 🔥 设置内存缓存的磁盘缓存引用,用于LRU淘汰时的备份 + memCache.SetDiskCacheReference(diskCache) + return &EnhancedTwoLevelCache{ memory: memCache, disk: diskCache, @@ -57,6 +60,40 @@ func (c *EnhancedTwoLevelCache) Set(key string, data []byte, ttl time.Duration) return nil } +// SetMemoryOnly 仅更新内存缓存 +func (c *EnhancedTwoLevelCache) SetMemoryOnly(key string, data []byte, ttl time.Duration) error { + now := time.Now() + + // 🔥 只更新内存缓存,不触发磁盘写入 + c.memory.SetWithTimestamp(key, data, ttl, now) + + return nil +} + +// SetBothLevels 更新内存和磁盘缓存 +func (c *EnhancedTwoLevelCache) SetBothLevels(key string, data []byte, ttl time.Duration) error { + now := time.Now() + + // 同步更新内存缓存 + c.memory.SetWithTimestamp(key, data, ttl, now) + + // 异步更新磁盘缓存 + go func(k string, d []byte, t time.Duration) { + _ = c.disk.Set(k, d, t) + }(key, data, ttl) + + return nil +} + +// SetWithFinalFlag 根据结果状态选择更新策略 +func (c *EnhancedTwoLevelCache) SetWithFinalFlag(key string, data []byte, ttl time.Duration, isFinal bool) error { + if isFinal { + return c.SetBothLevels(key, data, ttl) + } else { + return c.SetMemoryOnly(key, data, ttl) + } +} + // Get 获取缓存 func (c *EnhancedTwoLevelCache) Get(key string) ([]byte, bool, error) { diff --git a/util/cache/sharded_memory_cache.go b/util/cache/sharded_memory_cache.go index cca274c..d746a96 100644 --- a/util/cache/sharded_memory_cache.go +++ b/util/cache/sharded_memory_cache.go @@ -32,6 +32,8 @@ type ShardedMemoryCache struct { maxSize int64 itemsPerShard int sizePerShard int64 + diskCache *ShardedDiskCache // 🔥 新增:磁盘缓存引用 + diskCacheMutex sync.RWMutex // 🔥 新增:磁盘缓存引用的保护锁 } // 创建新的分片内存缓存 @@ -198,23 +200,37 @@ func (c *ShardedMemoryCache) GetLastModified(key string) (time.Time, bool) { return item.lastModified, true } -// 从指定分片中驱逐最久未使用的项 +// 从指定分片中驱逐最久未使用的项(带磁盘备份) func (c *ShardedMemoryCache) evictFromShard(shard *memoryCacheShard) { var oldestKey string + var oldestItem *shardedMemoryCacheItem var oldestTime int64 = 9223372036854775807 // int64最大值 for k, v := range shard.items { lastUsed := atomic.LoadInt64(&v.lastUsed) if lastUsed < oldestTime { oldestKey = k + oldestItem = v oldestTime = lastUsed } } // 如果找到了最久未使用的项,删除它 - if oldestKey != "" { - item := shard.items[oldestKey] - atomic.AddInt64(&shard.currSize, -int64(item.size)) + if oldestKey != "" && oldestItem != nil { + // 🔥 关键优化:淘汰前检查是否需要刷盘保护 + diskCache := c.getDiskCacheReference() + if time.Now().Before(oldestItem.expiry) && diskCache != nil { + // 数据还没过期,异步刷新到磁盘保存 + go func(key string, data []byte, expiry time.Time) { + ttl := time.Until(expiry) + if ttl > 0 { + diskCache.Set(key, data, ttl) // 🔥 保持相同TTL + } + }(oldestKey, oldestItem.data, oldestItem.expiry) + } + + // 从内存中删除 + atomic.AddInt64(&shard.currSize, -int64(oldestItem.size)) delete(shard.items, oldestKey) } } @@ -281,4 +297,18 @@ func (c *ShardedMemoryCache) StartCleanupTask() { c.CleanExpired() } }() +} + +// SetDiskCacheReference 设置磁盘缓存引用 +func (c *ShardedMemoryCache) SetDiskCacheReference(diskCache *ShardedDiskCache) { + c.diskCacheMutex.Lock() + defer c.diskCacheMutex.Unlock() + c.diskCache = diskCache +} + +// getDiskCacheReference 获取磁盘缓存引用 +func (c *ShardedMemoryCache) getDiskCacheReference() *ShardedDiskCache { + c.diskCacheMutex.RLock() + defer c.diskCacheMutex.RUnlock() + return c.diskCache } \ No newline at end of file