重构缓存实现,大幅优化并发性能

This commit is contained in:
www.xueximeng.com
2025-07-30 20:07:49 +08:00
parent 36d3a1c200
commit 53b83a0899
27 changed files with 1846 additions and 5351 deletions

View File

@@ -2,6 +2,16 @@
PanSou是一个高性能的网盘资源搜索API服务支持TG搜索和自定义插件搜索。系统设计以性能和可扩展性为核心支持并发搜索、结果智能排序和网盘类型分类。 PanSou是一个高性能的网盘资源搜索API服务支持TG搜索和自定义插件搜索。系统设计以性能和可扩展性为核心支持并发搜索、结果智能排序和网盘类型分类。
## 🚀 性能表现
实测 PanSou 在 8GB MacBook Pro 上的性能表现:
-**500用户瞬时并发**: 100%成功率平均响应167ms
-**200用户持续并发**: 30秒内处理4725请求QPS=148
-**缓存命中**: 99.8%请求<100ms响应时间
-**高可用性**: 长时间运行无故障
## 特性 ## 特性
- **高性能搜索**并发搜索多个Telegram频道显著提升搜索速度工作池设计高效管理并发任务 - **高性能搜索**并发搜索多个Telegram频道显著提升搜索速度工作池设计高效管理并发任务
@@ -120,36 +130,45 @@ cd pansou
| HTTP_READ_TIMEOUT | HTTP读取超时时间(秒) | 自动计算最小30 | | HTTP_READ_TIMEOUT | HTTP读取超时时间(秒) | 自动计算最小30 |
| HTTP_WRITE_TIMEOUT | HTTP写入超时时间(秒) | 自动计算最小60 | | HTTP_WRITE_TIMEOUT | HTTP写入超时时间(秒) | 自动计算最小60 |
| HTTP_IDLE_TIMEOUT | HTTP空闲连接超时时间(秒) | 120 | | 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 ```bash
# 默认频道 # 针对macOS系统线程限制优化的配置
export CHANNELS="tgsearchers2,xxx" export HTTP_MAX_CONNS=200 # 降低连接数
export ASYNC_MAX_BACKGROUND_WORKERS=15 # 减少工作者数量
# 缓存配置 export ASYNC_MAX_BACKGROUND_TASKS=75 # 减少任务队列
export CACHE_ENABLED=true export CONCURRENCY=30 # 适中的并发数
export CACHE_PATH="./cache" export ASYNC_LOG_ENABLED=false # 关闭详细日志
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"
``` ```
#### 服务器/云环境配置
```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. 构建 3. 构建
```linux ```linux

View File

@@ -2,6 +2,8 @@ package api
import ( import (
"fmt" "fmt"
"net/url"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -44,6 +46,19 @@ func LoggerMiddleware() gin.HandlerFunc {
// 请求路由 // 请求路由
reqURI := c.Request.RequestURI 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() statusCode := c.Writer.Status()
@@ -53,6 +68,6 @@ func LoggerMiddleware() gin.HandlerFunc {
// 日志格式 // 日志格式
gin.DefaultWriter.Write([]byte( gin.DefaultWriter.Write([]byte(
fmt.Sprintf("| %s | %s | %s | %d | %s\n", fmt.Sprintf("| %s | %s | %s | %d | %s\n",
clientIP, reqMethod, reqURI, statusCode, latencyTime.String()))) clientIP, reqMethod, displayURI, statusCode, latencyTime.String())))
} }
} }

View File

@@ -38,6 +38,7 @@ type Config struct {
AsyncMaxBackgroundWorkers int // 最大后台工作者数量 AsyncMaxBackgroundWorkers int // 最大后台工作者数量
AsyncMaxBackgroundTasks int // 最大后台任务数量 AsyncMaxBackgroundTasks int // 最大后台任务数量
AsyncCacheTTLHours int // 异步缓存有效期(小时) AsyncCacheTTLHours int // 异步缓存有效期(小时)
AsyncLogEnabled bool // 是否启用异步插件详细日志
// HTTP服务器配置 // HTTP服务器配置
HTTPReadTimeout time.Duration // 读取超时 HTTPReadTimeout time.Duration // 读取超时
HTTPWriteTimeout time.Duration // 写入超时 HTTPWriteTimeout time.Duration // 写入超时
@@ -81,6 +82,7 @@ func Init() {
AsyncMaxBackgroundWorkers: getAsyncMaxBackgroundWorkers(), AsyncMaxBackgroundWorkers: getAsyncMaxBackgroundWorkers(),
AsyncMaxBackgroundTasks: getAsyncMaxBackgroundTasks(), AsyncMaxBackgroundTasks: getAsyncMaxBackgroundTasks(),
AsyncCacheTTLHours: getAsyncCacheTTLHours(), AsyncCacheTTLHours: getAsyncCacheTTLHours(),
AsyncLogEnabled: getAsyncLogEnabled(),
// HTTP服务器配置 // HTTP服务器配置
HTTPReadTimeout: getHTTPReadTimeout(), HTTPReadTimeout: getHTTPReadTimeout(),
HTTPWriteTimeout: getHTTPWriteTimeout(), HTTPWriteTimeout: getHTTPWriteTimeout(),
@@ -453,6 +455,19 @@ func getHTTPMaxConns() int {
return maxConns 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设置 // 应用GC设置
func applyGCSettings() { func applyGCSettings() {
// 设置GC百分比 // 设置GC百分比

View File

@@ -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 # 项目说明
```

View File

@@ -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. **参数统一处理**:对相同语义的不同形式参数进行统一处理,确保缓存一致性

View File

@@ -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. **异步操作**:非关键路径使用异步处理,如缓存写入

View File

@@ -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
}
```

File diff suppressed because it is too large Load Diff

View File

@@ -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网关<br/>中间件}
B --> C[参数解析<br/>与验证]
C --> D[搜索服务<br/>SearchService]
D --> E{数据来源<br/>选择}
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[插件管理器<br/>PluginManager]
M --> N[异步插件调度]
N --> O[插件工作池]
O --> P[HTTP客户端]
P --> Q[目标网站API]
Q --> R[响应解析]
R --> S[结果过滤]
L --> T{二级缓存系统}
S --> T
T --> U[分片内存缓存<br/>LRU + 原子操作]
T --> V[分片磁盘缓存<br/>GOB序列化]
U --> W[缓存检查]
V --> W
W --> X{缓存命中?}
X -->|是| Y[缓存反序列化]
X -->|否| Z[执行搜索]
Z --> AA[异步更新缓存]
AA --> U
AA --> V
Y --> BB[结果合并<br/>mergeSearchResults]
AA --> BB
BB --> CC[网盘类型分类]
CC --> DD[智能排序<br/>时间+权重]
DD --> EE[结果过滤<br/>cloud_types]
EE --> FF[JSON响应]
FF --> GG[用户]
%% 异步处理流程
N --> HH[短超时处理<br/>4秒]
HH --> II{是否完成?}
II -->|是| JJ[返回完整结果<br/>isFinal=true]
II -->|否| KK[返回部分结果<br/>isFinal=false]
KK --> LL[后台继续处理<br/>最长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系统的实际实现编写包含了系统设计的核心信息。文档内容与代码实现保持同步专注于已实现的功能特性。*

File diff suppressed because it is too large Load Diff

View File

@@ -122,8 +122,7 @@ func startServer() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
// 保存异步插件缓存 // 异步插件本地缓存系统已移除
plugin.SaveCacheToDisk()
// 优雅关闭服务器 // 优雅关闭服务器
if err := srv.Shutdown(ctx); err != nil { if err := srv.Shutdown(ctx); err != nil {

32
model/plugin_result.go Normal file
View File

@@ -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
}

View File

@@ -1,35 +1,22 @@
package plugin package plugin
import ( import (
"compress/gzip"
"encoding/gob"
"pansou/util/json"
"fmt" "fmt"
"net/http" "net/http"
"os"
"path/filepath"
"sort"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"math/rand"
"pansou/config" "pansou/config"
"pansou/model" "pansou/model"
) )
// 缓存相关变量 // 工作池和统计相关变量
var ( var (
// API响应缓存键为关键词值为缓存的响应 // API响应缓存键为关键词值为缓存的响应(仅内存,不持久化)
apiResponseCache = sync.Map{} apiResponseCache = sync.Map{}
// 最后一次清理缓存的时间
lastCacheCleanTime = time.Now()
// 最后一次保存缓存的时间
lastCacheSaveTime = time.Now()
// 工作池相关变量 // 工作池相关变量
backgroundWorkerPool chan struct{} backgroundWorkerPool chan struct{}
backgroundTasksCount int32 = 0 backgroundTasksCount int32 = 0
@@ -43,24 +30,18 @@ var (
initialized bool = false initialized bool = false
initLock sync.Mutex initLock sync.Mutex
// 缓存保存锁,防止并发保存导致的竞态条件
saveCacheLock sync.Mutex
// 默认配置值 // 默认配置值
defaultAsyncResponseTimeout = 4 * time.Second defaultAsyncResponseTimeout = 4 * time.Second
defaultPluginTimeout = 30 * time.Second defaultPluginTimeout = 30 * time.Second
defaultCacheTTL = 1 * time.Hour defaultCacheTTL = 1 * time.Hour // 恢复但仅用于内存缓存
defaultMaxBackgroundWorkers = 20 defaultMaxBackgroundWorkers = 20
defaultMaxBackgroundTasks = 100 defaultMaxBackgroundTasks = 100
// 缓存保存间隔 (2分钟)
cacheSaveInterval = 2 * time.Minute
// 缓存访问频率记录 // 缓存访问频率记录
cacheAccessCount = sync.Map{} cacheAccessCount = sync.Map{}
) )
// 缓存响应结构 // 缓存响应结构(仅内存,不持久化到磁盘)
type cachedResponse struct { type cachedResponse struct {
Results []model.SearchResult `json:"results"` Results []model.SearchResult `json:"results"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
@@ -69,11 +50,6 @@ type cachedResponse struct {
AccessCount int `json:"access_count"` AccessCount int `json:"access_count"`
} }
// 可序列化的缓存结构,用于持久化
type persistentCache struct {
Entries map[string]cachedResponse
}
// initAsyncPlugin 初始化异步插件配置 // initAsyncPlugin 初始化异步插件配置
func initAsyncPlugin() { func initAsyncPlugin() {
initLock.Lock() initLock.Lock()
@@ -91,12 +67,7 @@ func initAsyncPlugin() {
backgroundWorkerPool = make(chan struct{}, maxWorkers) backgroundWorkerPool = make(chan struct{}, maxWorkers)
// 启动缓存清理和保存goroutine // 异步插件本地缓存系统已移除,现在只依赖主缓存系统
go startCacheCleaner()
go startCachePersistence()
// 尝试从磁盘加载缓存
loadCacheFromDisk()
initialized = true initialized = true
} }
@@ -106,297 +77,11 @@ func InitAsyncPluginSystem() {
initAsyncPlugin() 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 尝试获取工作槽 // acquireWorkerSlot 尝试获取工作槽
func acquireWorkerSlot() bool { func acquireWorkerSlot() bool {
@@ -442,7 +127,7 @@ func recordAsyncCompletion() {
atomic.AddInt64(&asyncCompletions, 1) atomic.AddInt64(&asyncCompletions, 1)
} }
// recordCacheAccess 记录缓存访问次数,用于智能缓存策略 // recordCacheAccess 记录缓存访问次数,用于智能缓存策略(仅内存)
func recordCacheAccess(key string) { func recordCacheAccess(key string) {
// 更新缓存项的访问时间和计数 // 更新缓存项的访问时间和计数
if cached, ok := apiResponseCache.Load(key); ok { if cached, ok := apiResponseCache.Load(key); ok {
@@ -460,15 +145,18 @@ func recordCacheAccess(key string) {
} }
} }
// BaseAsyncPlugin 基础异步插件结构 // BaseAsyncPlugin 基础异步插件结构(保留内存缓存,移除磁盘持久化)
type BaseAsyncPlugin struct { type BaseAsyncPlugin struct {
name string name string
priority int priority int
client *http.Client // 用于短超时的客户端 client *http.Client // 用于短超时的客户端
backgroundClient *http.Client // 用于长超时的客户端 backgroundClient *http.Client // 用于长超时的客户端
cacheTTL time.Duration // 缓存有效期 cacheTTL time.Duration // 内存缓存有效期
mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数 mainCacheUpdater func(string, []model.SearchResult, time.Duration, bool, string) error // 主缓存更新函数支持IsFinal参数接收原始数据最后参数为关键词
MainCacheKey string // 主缓存键,导出字段 MainCacheKey string // 主缓存键,导出字段
currentKeyword string // 当前搜索的关键词,用于日志显示
finalUpdateTracker map[string]bool // 追踪已更新的最终结果缓存
finalUpdateMutex sync.RWMutex // 保护finalUpdateTracker的并发访问
} }
// NewBaseAsyncPlugin 创建基础异步插件 // NewBaseAsyncPlugin 创建基础异步插件
@@ -499,7 +187,8 @@ func NewBaseAsyncPlugin(name string, priority int) *BaseAsyncPlugin {
backgroundClient: &http.Client{ backgroundClient: &http.Client{
Timeout: processingTimeout, Timeout: processingTimeout,
}, },
cacheTTL: cacheTTL, cacheTTL: cacheTTL,
finalUpdateTracker: make(map[string]bool), // 初始化缓存更新追踪器
} }
} }
@@ -508,8 +197,13 @@ func (p *BaseAsyncPlugin) SetMainCacheKey(key string) {
p.MainCacheKey = key p.MainCacheKey = key
} }
// SetMainCacheUpdater 设置主缓存更新函数 // SetCurrentKeyword 设置当前搜索关键词(用于日志显示)
func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []byte, time.Duration) error) { 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 p.mainCacheUpdater = updater
} }
@@ -611,8 +305,9 @@ func (p *BaseAsyncPlugin) AsyncSearch(
AccessCount: 1, AccessCount: 1,
}) })
// 更新主缓存系统 // 🔧 工作池满时4秒内完成这是完整结果
p.updateMainCache(mainCacheKey, results) fmt.Printf("[%s] 🕐 工作池满-直接完成: %v\n", p.name, time.Since(now))
p.updateMainCacheWithFinal(mainCacheKey, results, true)
return return
} }
@@ -625,7 +320,7 @@ func (p *BaseAsyncPlugin) AsyncSearch(
select { select {
case <-doneChan: case <-doneChan:
// 已经响应,只更新缓存 // 已经响应,只更新缓存
if err == nil && len(results) > 0 { if err == nil {
// 检查是否存在旧缓存 // 检查是否存在旧缓存
var accessCount int = 1 var accessCount int = 1
var lastAccess time.Time = now var lastAccess time.Time = now
@@ -668,11 +363,10 @@ func (p *BaseAsyncPlugin) AsyncSearch(
}) })
recordAsyncCompletion() recordAsyncCompletion()
// 更新主缓存系统 // 异步插件后台完成时更新主缓存(标记为最终结果)
p.updateMainCache(mainCacheKey, results) p.updateMainCacheWithFinal(mainCacheKey, results, true)
// 更新缓存后立即触发保存 // 异步插件本地缓存系统已移除
go saveCacheToDisk()
} }
default: default:
// 尚未响应,发送结果 // 尚未响应,发送结果
@@ -722,11 +416,11 @@ func (p *BaseAsyncPlugin) AsyncSearch(
AccessCount: 1, AccessCount: 1,
}) })
// 更新主缓存系统 // 🔧 4秒内正常完成这是完整的最终结果
p.updateMainCache(mainCacheKey, results) 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) close(doneChan)
return nil, err return nil, err
case <-time.After(responseTimeout): case <-time.After(responseTimeout):
// 插件响应超时,后台继续处理(优化完成,日志简化)
// 响应超时,返回空结果,后台继续处理 // 响应超时,返回空结果,后台继续处理
go func() { go func() {
defer close(doneChan) defer close(doneChan)
@@ -772,11 +468,233 @@ func (p *BaseAsyncPlugin) AsyncSearch(
AccessCount: 1, AccessCount: 1,
}) })
// 🔧 修复4秒超时时也要更新主缓存标记为部分结果空结果
p.updateMainCacheWithFinal(mainCacheKey, []model.SearchResult{}, false)
// fmt.Printf("[%s] 响应超时,后台继续处理: %s\n", p.name, pluginSpecificCacheKey) // fmt.Printf("[%s] 响应超时,后台继续处理: %s\n", p.name, pluginSpecificCacheKey)
return []model.SearchResult{}, nil 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 在后台刷新缓存 // refreshCacheInBackground 在后台刷新缓存
func (p *BaseAsyncPlugin) refreshCacheInBackground( func (p *BaseAsyncPlugin) refreshCacheInBackground(
keyword string, keyword string,
@@ -834,41 +752,57 @@ func (p *BaseAsyncPlugin) refreshCacheInBackground(
AccessCount: oldCache.AccessCount, AccessCount: oldCache.AccessCount,
}) })
// 更新主缓存系统 // 🔥 异步插件后台刷新完成时更新主缓存(标记为最终结果)
// 使用传入的originalCacheKey直接传递给updateMainCache p.updateMainCacheWithFinal(originalCacheKey, mergedResults, true)
p.updateMainCache(originalCacheKey, mergedResults)
// 记录刷新时间 // 记录刷新时间
refreshTime := time.Since(refreshStart) refreshTime := time.Since(refreshStart)
fmt.Printf("[%s] 后台刷新完成: %s (耗时: %v, 新项目: %d, 合并项目: %d)\n", fmt.Printf("[%s] 后台刷新完成: %s (耗时: %v, 新项目: %d, 合并项目: %d)\n",
p.name, cacheKey, refreshTime, len(results), len(mergedResults)) 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) { 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 == "" { if p.mainCacheUpdater == nil || cacheKey == "" {
return return
} }
// 序列化结果 // 🔥 防止重复更新导致LRU缓存淘汰的优化
data, err := json.Marshal(results) // 如果是最终结果,检查缓存中是否已经存在相同的最终结果
if err != nil { // 使用全局缓存键追踪已更新的最终结果
fmt.Printf("[%s] 序列化结果失败: %v\n", p.name, err) updateKey := fmt.Sprintf("final_updated_%s_%s", p.name, cacheKey)
return
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 { // 传递原始数据由主程序负责GOB序列化
fmt.Printf("[%s] 成功更新主缓存: %s\n", p.name, cacheKey) 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 返回短超时客户端 // GetClient 返回短超时客户端
func (p *BaseAsyncPlugin) GetClient() *http.Client { func (p *BaseAsyncPlugin) GetClient() *http.Client {
return p.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
} }

View File

@@ -106,9 +106,18 @@ func NewHdr4kAsyncPlugin() *Hdr4kAsyncPlugin {
} }
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果(兼容性方法)
func (p *Hdr4kAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { 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 实际的搜索实现 // doSearch 实际的搜索实现

View File

@@ -42,10 +42,18 @@ func NewHunhepanAsyncPlugin() *HunhepanAsyncPlugin {
} }
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果(兼容性方法)
func (p *HunhepanAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { func (p *HunhepanAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数但不使用 result, err := p.SearchWithResult(keyword, ext)
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, 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 实际的搜索实现 // doSearch 实际的搜索实现

View File

@@ -35,10 +35,18 @@ func NewJikepanAsyncV2Plugin() *JikepanAsyncV2Plugin {
} }
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果(兼容性方法)
func (p *JikepanAsyncV2Plugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { func (p *JikepanAsyncV2Plugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数但不使用 result, err := p.SearchWithResult(keyword, ext)
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, 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 实际的搜索实现 // doSearch 实际的搜索实现

View File

@@ -55,10 +55,18 @@ func NewPan666AsyncPlugin() *Pan666AsyncPlugin {
} }
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果(兼容性方法)
func (p *Pan666AsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { func (p *Pan666AsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数但不使用 result, err := p.SearchWithResult(keyword, ext)
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, 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 实际的搜索实现 // doSearch 实际的搜索实现

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
@@ -96,9 +95,9 @@ var (
buildIdMutex sync.RWMutex buildIdMutex sync.RWMutex
) )
// PanSearchPlugin 盘搜插件 // PanSearchAsyncPlugin 盘搜异步插件
type PanSearchPlugin struct { type PanSearchAsyncPlugin struct {
client *http.Client *plugin.BaseAsyncPlugin
timeout time.Duration timeout time.Duration
maxResults int maxResults int
maxConcurrent int maxConcurrent int
@@ -231,42 +230,18 @@ func (wp *WorkerPool) Close() {
} }
} }
// NewPanSearchPlugin 创建新的盘搜插件 // NewPanSearchPlugin 创建新的盘搜异步插件
func NewPanSearchPlugin() *PanSearchPlugin { func NewPanSearchPlugin() *PanSearchAsyncPlugin {
timeout := DefaultTimeout 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 maxConcurrent := MaxConcurrent
p := &PanSearchPlugin{ p := &PanSearchAsyncPlugin{
client: &http.Client{ BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("pansearch", 4),
Transport: transport, timeout: timeout,
Timeout: timeout, maxResults: MaxResults,
}, maxConcurrent: maxConcurrent,
timeout: timeout, retries: MaxRetries,
maxResults: MaxResults, workerPool: NewWorkerPool(maxConcurrent), // 初始化工作池
maxConcurrent: maxConcurrent,
retries: MaxRetries,
workerPool: NewWorkerPool(maxConcurrent), // 初始化工作池
} }
// 初始化时预热获取 buildId // 初始化时预热获取 buildId
@@ -284,7 +259,7 @@ func NewPanSearchPlugin() *PanSearchPlugin {
} }
// startBuildIdUpdater 启动一个定期更新 buildId 的后台协程 // startBuildIdUpdater 启动一个定期更新 buildId 的后台协程
func (p *PanSearchPlugin) startBuildIdUpdater() { func (p *PanSearchAsyncPlugin) startBuildIdUpdater() {
// 每10分钟更新一次 buildId // 每10分钟更新一次 buildId
ticker := time.NewTicker(10 * time.Minute) ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop() defer ticker.Stop()
@@ -295,7 +270,7 @@ func (p *PanSearchPlugin) startBuildIdUpdater() {
} }
// updateBuildId 更新 buildId 缓存 // updateBuildId 更新 buildId 缓存
func (p *PanSearchPlugin) updateBuildId() { func (p *PanSearchAsyncPlugin) updateBuildId() {
// 创建带超时的上下文 // 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), p.timeout) ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
defer cancel() defer cancel()
@@ -315,7 +290,7 @@ func (p *PanSearchPlugin) updateBuildId() {
req.Header.Set("Upgrade-Insecure-Requests", "1") req.Header.Set("Upgrade-Insecure-Requests", "1")
req.Header.Set("Cache-Control", "max-age=0") req.Header.Set("Cache-Control", "max-age=0")
resp, err := p.client.Do(req) resp, err := p.GetClient().Do(req)
if err != nil { if err != nil {
fmt.Printf("请求失败: %v\n", err) fmt.Printf("请求失败: %v\n", err)
return return
@@ -380,17 +355,17 @@ func extractBuildId(body string) string {
} }
// Name 返回插件名称 // Name 返回插件名称
func (p *PanSearchPlugin) Name() string { func (p *PanSearchAsyncPlugin) Name() string {
return "pansearch" return "pansearch"
} }
// Priority 返回插件优先级 // Priority 返回插件优先级
func (p *PanSearchPlugin) Priority() int { func (p *PanSearchAsyncPlugin) Priority() int {
return 3 // 中等优先级 return 3 // 中等优先级
} }
// getBuildId 获取buildId优先使用缓存 // getBuildId 获取buildId优先使用缓存
func (p *PanSearchPlugin) getBuildId() (string, error) { func (p *PanSearchAsyncPlugin) getBuildId() (string, error) {
// 检查缓存是否有效 // 检查缓存是否有效
buildIdMutex.RLock() buildIdMutex.RLock()
if buildIdCache != "" && time.Since(buildIdCacheTime) < BuildIdCacheDuration*time.Minute { if buildIdCache != "" && time.Since(buildIdCacheTime) < BuildIdCacheDuration*time.Minute {
@@ -442,7 +417,7 @@ func (p *PanSearchPlugin) getBuildId() (string, error) {
time.Sleep(backoffTime) time.Sleep(backoffTime)
} }
resp, respErr = p.client.Do(req) resp, respErr = p.GetClient().Do(req)
if respErr == nil && resp.StatusCode == 200 { if respErr == nil && resp.StatusCode == 200 {
break break
} }
@@ -504,7 +479,7 @@ func (p *PanSearchPlugin) getBuildId() (string, error) {
} }
// getBaseURL 获取完整的API基础URL // getBaseURL 获取完整的API基础URL
func (p *PanSearchPlugin) getBaseURL() (string, error) { func (p *PanSearchAsyncPlugin) getBaseURL(client *http.Client) (string, error) {
buildId, err := p.getBuildId() buildId, err := p.getBuildId()
if err != nil { if err != nil {
return "", err return "", err
@@ -513,28 +488,30 @@ func (p *PanSearchPlugin) getBaseURL() (string, error) {
return fmt.Sprintf(BaseURLTemplate, buildId), nil return fmt.Sprintf(BaseURLTemplate, buildId), nil
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果(兼容性方法)
func (p *PanSearchPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { func (p *PanSearchAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 生成缓存键 result, err := p.SearchWithResult(keyword, ext)
cacheKey := keyword if err != nil {
return nil, err
// 检查缓存中是否已有结果
if cachedItems, ok := searchResultCache.Load(cacheKey); ok {
// 检查缓存是否过期
cachedResult := cachedItems.(cachedResponse)
if time.Since(cachedResult.timestamp) < cacheTTL {
return cachedResult.results, nil
}
} }
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 // 获取API基础URL
baseURL, err := p.getBaseURL() baseURL, err := p.getBaseURL(client)
if err != nil { if err != nil {
return nil, fmt.Errorf("获取API基础URL失败: %w", err) return nil, fmt.Errorf("获取API基础URL失败: %w", err)
} }
// 1. 发起首次请求获取total和第一页数据 // 1. 发起首次请求获取total和第一页数据
firstPageResults, total, err := p.fetchFirstPage(keyword, baseURL) firstPageResults, total, err := p.fetchFirstPage(keyword, baseURL, client)
if err != nil { if err != nil {
// 如果返回404错误可能是buildId过期尝试强制刷新buildId // 如果返回404错误可能是buildId过期尝试强制刷新buildId
if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "Not Found") { 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() buildIdMutex.Unlock()
// 重新获取buildId // 重新获取buildId
baseURL, err = p.getBaseURL() baseURL, err = p.getBaseURL(client)
if err != nil { if err != nil {
return nil, fmt.Errorf("刷新buildId失败: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("刷新buildId后获取首页仍然失败: %w", err) 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) results := p.convertResults(allResults, keyword)
// 缓存结果 // 缓存结果
searchResultCache.Store(cacheKey, cachedResponse{ searchResultCache.Store(keyword, cachedResponse{
results: results, results: results,
timestamp: time.Now(), timestamp: time.Now(),
}) })
@@ -589,7 +566,7 @@ func (p *PanSearchPlugin) Search(keyword string, ext map[string]interface{}) ([]
results := p.convertResults(allResults, keyword) results := p.convertResults(allResults, keyword)
// 缓存结果 // 缓存结果
searchResultCache.Store(cacheKey, cachedResponse{ searchResultCache.Store(keyword, cachedResponse{
results: results, results: results,
timestamp: time.Now(), timestamp: time.Now(),
}) })
@@ -775,7 +752,7 @@ CollectResults:
results := p.convertResults(allResults, keyword) results := p.convertResults(allResults, keyword)
// 缓存结果(即使超时也缓存已获取的结果) // 缓存结果(即使超时也缓存已获取的结果)
searchResultCache.Store(cacheKey, cachedResponse{ searchResultCache.Store(keyword, cachedResponse{
results: results, results: results,
timestamp: time.Now(), timestamp: time.Now(),
}) })
@@ -790,7 +767,7 @@ ProcessResults:
results := p.convertResults(allResults, keyword) results := p.convertResults(allResults, keyword)
// 缓存结果(即使有错误也缓存已获取的结果) // 缓存结果(即使有错误也缓存已获取的结果)
searchResultCache.Store(cacheKey, cachedResponse{ searchResultCache.Store(keyword, cachedResponse{
results: results, results: results,
timestamp: time.Now(), timestamp: time.Now(),
}) })
@@ -803,7 +780,7 @@ ProcessResults:
results := p.convertResults(uniqueResults, keyword) results := p.convertResults(uniqueResults, keyword)
// 缓存结果 // 缓存结果
searchResultCache.Store(cacheKey, cachedResponse{ searchResultCache.Store(keyword, cachedResponse{
results: results, results: results,
timestamp: time.Now(), timestamp: time.Now(),
}) })
@@ -812,7 +789,7 @@ ProcessResults:
} }
// fetchFirstPage 获取第一页结果和总数 // 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 // 构建请求URL
reqURL := fmt.Sprintf("%s?keyword=%s&offset=0", baseURL, url.QueryEscape(keyword)) 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") req.Header.Set("Pragma", "no-cache")
// 发送请求 // 发送请求
resp, err := p.client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("请求失败: %w", err) return nil, 0, fmt.Errorf("请求失败: %w", err)
} }
@@ -871,7 +848,7 @@ func (p *PanSearchPlugin) fetchFirstPage(keyword string, baseURL string) ([]PanS
} }
// fetchPage 获取指定偏移量的页面 // 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 // 构建请求URL
reqURL := fmt.Sprintf("%s?keyword=%s&offset=%d", baseURL, url.QueryEscape(keyword), offset) 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") req.Header.Set("Pragma", "no-cache")
// 发送请求 // 发送请求
resp, err := p.client.Do(req) resp, err := p.GetClient().Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("请求失败: %w", err) return nil, fmt.Errorf("请求失败: %w", err)
} }
@@ -926,7 +903,7 @@ func (p *PanSearchPlugin) fetchPage(keyword string, offset int, baseURL string)
} }
// deduplicateItems 去重处理 // deduplicateItems 去重处理
func (p *PanSearchPlugin) deduplicateItems(items []PanSearchItem) []PanSearchItem { func (p *PanSearchAsyncPlugin) deduplicateItems(items []PanSearchItem) []PanSearchItem {
// 使用map进行去重键为资源ID // 使用map进行去重键为资源ID
uniqueMap := make(map[int]PanSearchItem) uniqueMap := make(map[int]PanSearchItem)
@@ -944,7 +921,7 @@ func (p *PanSearchPlugin) deduplicateItems(items []PanSearchItem) []PanSearchIte
} }
// convertResults 将API响应转换为标准SearchResult格式 // 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)) results := make([]model.SearchResult, 0, len(items))
for _, item := range items { for _, item := range items {

View File

@@ -3,7 +3,6 @@ package panta
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/url" "net/url"
"pansou/model" "pansou/model"
@@ -148,10 +147,9 @@ const (
maxBackoff = 5000 maxBackoff = 5000
) )
// PantaPlugin 是PanTa网站的搜索插件实现 // PantaAsyncPlugin 是PanTa网站的异步搜索插件实现
type PantaPlugin struct { type PantaAsyncPlugin struct {
// HTTP客户端用于发送请求 *plugin.BaseAsyncPlugin
client *http.Client
// 并发控制 // 并发控制
maxConcurrency int maxConcurrency int
@@ -163,63 +161,30 @@ type PantaPlugin struct {
lastAdjustTime time.Time lastAdjustTime time.Time
} }
// 确保PantaPlugin实现了SearchPlugin接口 // 确保PantaAsyncPlugin实现了AsyncSearchPlugin接口
var _ plugin.SearchPlugin = (*PantaPlugin)(nil) var _ plugin.AsyncSearchPlugin = (*PantaAsyncPlugin)(nil)
// 在包初始化时注册插件 // 在包初始化时注册插件
func init() { func init() {
// 创建并注册插件实例 // 创建并注册插件实例
plugin.RegisterGlobalPlugin(NewPantaPlugin()) plugin.RegisterGlobalPlugin(NewPantaAsyncPlugin())
} }
// NewPantaPlugin 创建一个新的PanTa插件实例 // NewPantaAsyncPlugin 创建一个新的PanTa异步插件实例
func NewPantaPlugin() *PantaPlugin { func NewPantaAsyncPlugin() *PantaAsyncPlugin {
// 创建一个带有更多配置的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
},
}
// 启动定期清理缓存的goroutine // 启动定期清理缓存的goroutine
go startCacheCleaner() go startCacheCleaner()
// 创建插件实例 // 创建插件实例
plugin := &PantaPlugin{ p := &PantaAsyncPlugin{
client: client, BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("panta", 2),
maxConcurrency: defaultConcurrency, maxConcurrency: defaultConcurrency,
currentConcurrency: defaultConcurrency, currentConcurrency: defaultConcurrency,
responseTimes: make([]time.Duration, 0, 100), responseTimes: make([]time.Duration, 0, 10),
lastAdjustTime: time.Now(), lastAdjustTime: time.Now(),
} }
// 启动自适应并发控制 return p
go plugin.startConcurrencyAdjuster()
return plugin
} }
// startCacheCleaner 启动一个定期清理缓存的goroutine // startCacheCleaner 启动一个定期清理缓存的goroutine
@@ -242,17 +207,31 @@ func startCacheCleaner() {
} }
// Name 返回插件名称 // Name 返回插件名称
func (p *PantaPlugin) Name() string { func (p *PantaAsyncPlugin) Name() string {
return pluginName return pluginName
} }
// Priority 返回插件优先级 // Priority 返回插件优先级
func (p *PantaPlugin) Priority() int { func (p *PantaAsyncPlugin) Priority() int {
return defaultPriority return defaultPriority
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果(兼容性方法)
func (p *PantaPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { 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编码 // 对关键词进行URL编码
encodedKeyword := url.QueryEscape(keyword) 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") req.Header.Set("Cache-Control", "max-age=0")
// 使用带重试的请求方法发送HTTP请求 // 使用带重试的请求方法发送HTTP请求
resp, err := p.doRequestWithRetry(req) resp, err := p.doRequestWithRetry(req, client)
if err != nil { if err != nil {
return nil, fmt.Errorf("请求PanTa搜索页面失败: %v", err) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -309,7 +288,7 @@ func (p *PantaPlugin) Search(keyword string, ext map[string]interface{}) ([]mode
} }
// parseSearchResults 使用goquery解析搜索结果 // 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 var results []model.SearchResult
// 创建信号量控制并发数,使用自适应并发数 // 创建信号量控制并发数,使用自适应并发数
@@ -428,7 +407,7 @@ func (p *PantaPlugin) parseSearchResults(doc *goquery.Document) ([]model.SearchR
time.Sleep(backoffTime) time.Sleep(backoffTime)
} }
threadLinks, err := p.fetchThreadLinks(topicID) threadLinks, err := p.fetchThreadLinks(topicID, client)
if err == nil && len(threadLinks) > 0 { if err == nil && len(threadLinks) > 0 {
foundLinks = threadLinks foundLinks = threadLinks
break break
@@ -494,7 +473,7 @@ func (p *PantaPlugin) parseSearchResults(doc *goquery.Document) ([]model.SearchR
} }
// extractLinksFromElement 从元素中提取链接 // 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() html, _ := s.Html()
cacheKey := fmt.Sprintf("%s_%s", html, yearFromTitle) cacheKey := fmt.Sprintf("%s_%s", html, yearFromTitle)
@@ -611,7 +590,7 @@ func (p *PantaPlugin) extractLinksFromElement(s *goquery.Selection, yearFromTitl
} }
// fetchThreadLinks 获取帖子详情页中的链接 // 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 { if cachedLinks, ok := threadLinksCache.Load(topicID); ok {
return cachedLinks.([]model.Link), nil 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") req.Header.Set("Cache-Control", "max-age=0")
// 使用带重试的请求方法发送HTTP请求 // 使用带重试的请求方法发送HTTP请求
resp, err := p.doRequestWithRetry(req) resp, err := p.doRequestWithRetry(req, client)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1218,7 +1197,7 @@ func isNetDiskLink(url string) bool {
} }
// startConcurrencyAdjuster 启动一个定期调整并发数的goroutine // startConcurrencyAdjuster 启动一个定期调整并发数的goroutine
func (p *PantaPlugin) startConcurrencyAdjuster() { func (p *PantaAsyncPlugin) startConcurrencyAdjuster() {
ticker := time.NewTicker(concurrencyAdjustInterval * time.Second) ticker := time.NewTicker(concurrencyAdjustInterval * time.Second)
defer ticker.Stop() defer ticker.Stop()
@@ -1228,7 +1207,7 @@ func (p *PantaPlugin) startConcurrencyAdjuster() {
} }
// adjustConcurrency 根据响应时间调整并发数 // adjustConcurrency 根据响应时间调整并发数
func (p *PantaPlugin) adjustConcurrency() { func (p *PantaAsyncPlugin) adjustConcurrency() {
p.responseTimesMutex.Lock() p.responseTimesMutex.Lock()
defer p.responseTimesMutex.Unlock() defer p.responseTimesMutex.Unlock()
@@ -1258,7 +1237,7 @@ func (p *PantaPlugin) adjustConcurrency() {
} }
// recordResponseTime 记录请求响应时间 // recordResponseTime 记录请求响应时间
func (p *PantaPlugin) recordResponseTime(d time.Duration) { func (p *PantaAsyncPlugin) recordResponseTime(d time.Duration) {
p.responseTimesMutex.Lock() p.responseTimesMutex.Lock()
defer p.responseTimesMutex.Unlock() defer p.responseTimesMutex.Unlock()
@@ -1272,7 +1251,7 @@ func (p *PantaPlugin) recordResponseTime(d time.Duration) {
} }
// doRequestWithRetry 发送HTTP请求带重试机制 // 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 resp *http.Response
var err error var err error
var startTime time.Time var startTime time.Time
@@ -1294,7 +1273,7 @@ func (p *PantaPlugin) doRequestWithRetry(req *http.Request) (*http.Response, err
startTime = time.Now() startTime = time.Now()
// 发送请求 // 发送请求
resp, err = p.client.Do(req) resp, err = client.Do(req)
// 记录响应时间 // 记录响应时间
responseTime := time.Since(startTime) responseTime := time.Since(startTime)

View File

@@ -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 { if err == nil && len(results) > 0 {
@@ -186,6 +190,11 @@ func (p *PanyqPlugin) Search(keyword string, ext map[string]interface{}) ([]mode
return results, err 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 实际的搜索实现 // doSearch 实际的搜索实现
func (p *PanyqPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { func (p *PanyqPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
if DebugLog { if DebugLog {

View File

@@ -1,34 +1,42 @@
package plugin package plugin
import ( import (
"net/http"
"strings" "strings"
"sync" "sync"
"pansou/model" "pansou/model"
) )
// 全局插件注册表 // 全局异步插件注册表
var ( var (
globalRegistry = make(map[string]SearchPlugin) globalRegistry = make(map[string]AsyncSearchPlugin)
globalRegistryLock sync.RWMutex globalRegistryLock sync.RWMutex
) )
// SearchPlugin 搜索插件接口 // AsyncSearchPlugin 异步搜索插件接口
type SearchPlugin interface { type AsyncSearchPlugin interface {
// Name 返回插件名称 // Name 返回插件名称
Name() string Name() string
// Search 执行搜索并返回结果 // Priority 返回插件优先级
// ext参数用于传递额外的搜索参数插件可以根据需要使用或忽略
Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
// Priority 返回插件优先级(可选,用于控制结果排序)
Priority() int 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 注册插件到全局注册表 // RegisterGlobalPlugin 注册异步插件到全局注册表
// 这个函数应该在每个插件的init函数中被调用 func RegisterGlobalPlugin(plugin AsyncSearchPlugin) {
func RegisterGlobalPlugin(plugin SearchPlugin) {
if plugin == nil { if plugin == nil {
return return
} }
@@ -44,12 +52,12 @@ func RegisterGlobalPlugin(plugin SearchPlugin) {
globalRegistry[name] = plugin globalRegistry[name] = plugin
} }
// GetRegisteredPlugins 获取所有已注册的插件 // GetRegisteredPlugins 获取所有已注册的异步插件
func GetRegisteredPlugins() []SearchPlugin { func GetRegisteredPlugins() []AsyncSearchPlugin {
globalRegistryLock.RLock() globalRegistryLock.RLock()
defer globalRegistryLock.RUnlock() defer globalRegistryLock.RUnlock()
plugins := make([]SearchPlugin, 0, len(globalRegistry)) plugins := make([]AsyncSearchPlugin, 0, len(globalRegistry))
for _, plugin := range globalRegistry { for _, plugin := range globalRegistry {
plugins = append(plugins, plugin) plugins = append(plugins, plugin)
} }
@@ -57,37 +65,36 @@ func GetRegisteredPlugins() []SearchPlugin {
return plugins return plugins
} }
// PluginManager 插件管理器 // PluginManager 异步插件管理器
type PluginManager struct { type PluginManager struct {
plugins []SearchPlugin plugins []AsyncSearchPlugin
} }
// NewPluginManager 创建新的插件管理器 // NewPluginManager 创建新的异步插件管理器
func NewPluginManager() *PluginManager { func NewPluginManager() *PluginManager {
return &PluginManager{ return &PluginManager{
plugins: make([]SearchPlugin, 0), plugins: make([]AsyncSearchPlugin, 0),
} }
} }
// RegisterPlugin 注册插件 // RegisterPlugin 注册异步插件
func (pm *PluginManager) RegisterPlugin(plugin SearchPlugin) { func (pm *PluginManager) RegisterPlugin(plugin AsyncSearchPlugin) {
pm.plugins = append(pm.plugins, plugin) pm.plugins = append(pm.plugins, plugin)
} }
// RegisterAllGlobalPlugins 注册所有全局插件 // RegisterAllGlobalPlugins 注册所有全局异步插件
func (pm *PluginManager) RegisterAllGlobalPlugins() { func (pm *PluginManager) RegisterAllGlobalPlugins() {
for _, plugin := range GetRegisteredPlugins() { for _, plugin := range GetRegisteredPlugins() {
pm.RegisterPlugin(plugin) pm.RegisterPlugin(plugin)
} }
} }
// GetPlugins 获取所有注册的插件 // GetPlugins 获取所有注册的异步插件
func (pm *PluginManager) GetPlugins() []SearchPlugin { func (pm *PluginManager) GetPlugins() []AsyncSearchPlugin {
return pm.plugins return pm.plugins
} }
// FilterResultsByKeyword 根据关键词过滤搜索结果的全局辅助函数 // FilterResultsByKeyword 根据关键词过滤搜索结果的全局辅助函数
// 供非BaseAsyncPlugin类型的插件使用
func FilterResultsByKeyword(results []model.SearchResult, keyword string) []model.SearchResult { func FilterResultsByKeyword(results []model.SearchResult, keyword string) []model.SearchResult {
if keyword == "" { if keyword == "" {
return results return results

View File

@@ -59,50 +59,53 @@ const (
DefaultPageSize = 1000 DefaultPageSize = 1000
) )
// QuPanSouPlugin 趣盘搜插件 // QuPanSouAsyncPlugin 趣盘搜异步插件
type QuPanSouPlugin struct { type QuPanSouAsyncPlugin struct {
client *http.Client *plugin.BaseAsyncPlugin
timeout time.Duration timeout time.Duration
} }
// NewQuPanSouPlugin 创建新的趣盘搜插件 // NewQuPanSouPlugin 创建新的趣盘搜异步插件
func NewQuPanSouPlugin() *QuPanSouPlugin { func NewQuPanSouPlugin() *QuPanSouAsyncPlugin {
timeout := DefaultTimeout timeout := DefaultTimeout
return &QuPanSouPlugin{ return &QuPanSouAsyncPlugin{
client: &http.Client{ BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("qupansou", 3),
Timeout: timeout, timeout: timeout,
},
timeout: timeout,
} }
} }
// 确保QuPanSouAsyncPlugin实现了AsyncSearchPlugin接口
var _ plugin.AsyncSearchPlugin = (*QuPanSouAsyncPlugin)(nil)
// Name 返回插件名称 // Name 返回插件名称
func (p *QuPanSouPlugin) Name() string { func (p *QuPanSouAsyncPlugin) Name() string {
return "qupansou" return "qupansou"
} }
// Priority 返回插件优先级 // Priority 返回插件优先级
func (p *QuPanSouPlugin) Priority() int { func (p *QuPanSouAsyncPlugin) Priority() int {
return 3 // 中等优先级 return 3 // 中等优先级
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果(兼容性方法)
func (p *QuPanSouPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { func (p *QuPanSouAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 生成缓存键 result, err := p.SearchWithResult(keyword, ext)
cacheKey := keyword if err != nil {
return nil, err
// 检查缓存中是否已有结果
if cachedItems, ok := apiResponseCache.Load(cacheKey); ok {
// 检查缓存是否过期
cachedResult := cachedItems.(cachedResponse)
if time.Since(cachedResult.timestamp) < cacheTTL {
return cachedResult.results, nil
}
} }
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请求 // 发送API请求
items, err := p.searchAPI(keyword) items, err := p.searchAPI(keyword, client)
if err != nil { if err != nil {
return nil, fmt.Errorf("qupansou API error: %w", err) 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) results := p.convertResults(items)
// 缓存结果
apiResponseCache.Store(cacheKey, cachedResponse{
results: results,
timestamp: time.Now(),
})
return results, nil return results, nil
} }
@@ -126,7 +123,7 @@ type cachedResponse struct {
} }
// searchAPI 向API发送请求 // 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{}{ reqBody := map[string]interface{}{
"style": "get", "style": "get",
@@ -168,7 +165,7 @@ func (p *QuPanSouPlugin) searchAPI(keyword string) ([]QuPanSouItem, error) {
req.Header.Set("Referer", "https://pan.funletu.com/") req.Header.Set("Referer", "https://pan.funletu.com/")
// 发送请求 // 发送请求
resp, err := p.client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("request failed: %w", err) return nil, fmt.Errorf("request failed: %w", err)
} }
@@ -195,7 +192,7 @@ func (p *QuPanSouPlugin) searchAPI(keyword string) ([]QuPanSouItem, error) {
} }
// convertResults 将API响应转换为标准SearchResult格式 // 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)) results := make([]model.SearchResult, 0, len(items))
for _, item := range items { for _, item := range items {
@@ -248,7 +245,7 @@ func (p *QuPanSouPlugin) convertResults(items []QuPanSouItem) []model.SearchResu
} }
// determineLinkType 根据URL确定链接类型 // determineLinkType 根据URL确定链接类型
func (p *QuPanSouPlugin) determineLinkType(url string) string { func (p *QuPanSouAsyncPlugin) determineLinkType(url string) string {
lowerURL := strings.ToLower(url) lowerURL := strings.ToLower(url)
if strings.Contains(lowerURL, "pan.baidu.com") { if strings.Contains(lowerURL, "pan.baidu.com") {

View File

@@ -103,10 +103,18 @@ func NewSusuAsyncPlugin() *SusuAsyncPlugin {
} }
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果(兼容性方法)
func (p *SusuAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { func (p *SusuAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数但不使用 result, err := p.SearchWithResult(keyword, ext)
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, 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 实际的搜索实现 // doSearch 实际的搜索实现

View File

@@ -92,10 +92,18 @@ func startCacheCleaner() {
} }
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果(兼容性方法)
func (p *XuexizhinanPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) { func (p *XuexizhinanPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键 result, err := p.SearchWithResult(keyword, ext)
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, 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 实际的搜索实现 // doSearch 实际的搜索实现

View File

@@ -2,6 +2,7 @@ package service
import ( import (
"context" "context"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sort" "sort"
@@ -21,6 +22,37 @@ import (
// 优先关键词列表 // 优先关键词列表
var priorityKeywords = []string{"合集", "系列", "全", "完", "最新", "附"} 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 ( var (
enhancedTwoLevelCache *cache.EnhancedTwoLevelCache 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 搜索服务 // SearchService 搜索服务
type SearchService struct { type SearchService struct {
pluginManager *plugin.PluginManager pluginManager *plugin.PluginManager
@@ -71,9 +199,91 @@ func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCach
return return
} }
// 创建缓存更新函数 // 🔧 设置全局序列化器,确保异步插件与主程序使用相同的序列化格式
cacheUpdater := func(key string, data []byte, ttl time.Duration) error { serializer := mainCache.GetSerializer()
return mainCache.Set(key, data, ttl) 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 { for _, p := range plugins {
// 检查插件是否实现了SetMainCacheUpdater方法 // 检查插件是否实现了SetMainCacheUpdater方法(修复后的签名,增加关键词参数)
if asyncPlugin, ok := p.(interface{ SetMainCacheUpdater(func(string, []byte, time.Duration) error) }); ok { if asyncPlugin, ok := p.(interface{ SetMainCacheUpdater(func(string, []model.SearchResult, time.Duration, bool, string) error) }); ok {
// 注入缓存更新函数 // 注入缓存更新函数
asyncPlugin.SetMainCacheUpdater(cacheUpdater) asyncPlugin.SetMainCacheUpdater(cacheUpdater)
} }
@@ -893,6 +1103,7 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
// 生成缓存键 // 生成缓存键
cacheKey := cache.GeneratePluginCacheKey(keyword, plugins) cacheKey := cache.GeneratePluginCacheKey(keyword, plugins)
// 如果未启用强制刷新,尝试从缓存获取结果 // 如果未启用强制刷新,尝试从缓存获取结果
if !forceRefresh && cacheInitialized && config.AppConfig.CacheEnabled { if !forceRefresh && cacheInitialized && config.AppConfig.CacheEnabled {
var data []byte var data []byte
@@ -906,19 +1117,30 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
// 如果磁盘缓存比内存缓存更新,会自动更新内存缓存并返回最新数据 // 如果磁盘缓存比内存缓存更新,会自动更新内存缓存并返回最新数据
data, hit, err = enhancedTwoLevelCache.Get(cacheKey) 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 { if err == nil && hit {
var results []model.SearchResult var results []model.SearchResult
if err := enhancedTwoLevelCache.GetSerializer().Deserialize(data, &results); err == nil { 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 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 { if s.pluginManager != nil {
allPlugins := s.pluginManager.GetPlugins() allPlugins := s.pluginManager.GetPlugins()
@@ -966,32 +1188,20 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
for _, p := range availablePlugins { for _, p := range availablePlugins {
plugin := p // 创建副本,避免闭包问题 plugin := p // 创建副本,避免闭包问题
tasks = append(tasks, func() interface{} { tasks = append(tasks, func() interface{} {
// 检查插件是否为异步插件 // 设置主缓存键和当前关键词
if asyncPlugin, ok := plugin.(interface { plugin.SetMainCacheKey(cacheKey)
AsyncSearch(keyword string, searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), mainCacheKey string, ext map[string]interface{}) ([]model.SearchResult, error) plugin.SetCurrentKeyword(keyword)
SetMainCacheKey(string)
}); ok { // 调用异步插件的AsyncSearch方法
// 先设置主缓存键 results, err := plugin.AsyncSearch(keyword, func(client *http.Client, kw string, extParams map[string]interface{}) ([]model.SearchResult, error) {
asyncPlugin.SetMainCacheKey(cacheKey) // 使用插件的Search方法作为搜索函数
return plugin.Search(kw, extParams)
// 是异步插件调用AsyncSearch方法并传递主缓存键和ext参数 }, cacheKey, ext)
results, err := asyncPlugin.AsyncSearch(keyword, func(client *http.Client, kw string, extParams map[string]interface{}) ([]model.SearchResult, error) {
// 这里使用插件的Search方法作为搜索函数传递ext参数 if err != nil {
return plugin.Search(kw, extParams) return nil
}, 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
} }
return results
}) })
} }
@@ -1007,61 +1217,33 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
} }
} }
// 异步缓存结果 // 🔧 恢复主程序缓存更新:确保最终合并结果被正确缓存
if cacheInitialized && config.AppConfig.CacheEnabled { 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 ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute
// 使用增强版缓存 // 使用增强版缓存,确保与异步插件使用相同的序列化器
if enhancedTwoLevelCache != nil { if enhancedTwoLevelCache != nil {
data, err := enhancedTwoLevelCache.GetSerializer().Serialize(res) data, err := enhancedTwoLevelCache.GetSerializer().Serialize(res)
if err != nil { if err != nil {
fmt.Printf("❌ [主程序] 缓存序列化失败: %s | 错误: %v\n", key, err)
return 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 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 获取插件管理器 // GetPluginManager 获取插件管理器
func (s *SearchService) GetPluginManager() *plugin.PluginManager { func (s *SearchService) GetPluginManager() *plugin.PluginManager {

View File

@@ -33,6 +33,9 @@ func NewEnhancedTwoLevelCache() (*EnhancedTwoLevelCache, error) {
// 创建序列化器 // 创建序列化器
serializer := NewGobSerializer() serializer := NewGobSerializer()
// 🔥 设置内存缓存的磁盘缓存引用用于LRU淘汰时的备份
memCache.SetDiskCacheReference(diskCache)
return &EnhancedTwoLevelCache{ return &EnhancedTwoLevelCache{
memory: memCache, memory: memCache,
disk: diskCache, disk: diskCache,
@@ -57,6 +60,40 @@ func (c *EnhancedTwoLevelCache) Set(key string, data []byte, ttl time.Duration)
return nil 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 获取缓存 // Get 获取缓存
func (c *EnhancedTwoLevelCache) Get(key string) ([]byte, bool, error) { func (c *EnhancedTwoLevelCache) Get(key string) ([]byte, bool, error) {

View File

@@ -32,6 +32,8 @@ type ShardedMemoryCache struct {
maxSize int64 maxSize int64
itemsPerShard int itemsPerShard int
sizePerShard int64 sizePerShard int64
diskCache *ShardedDiskCache // 🔥 新增:磁盘缓存引用
diskCacheMutex sync.RWMutex // 🔥 新增:磁盘缓存引用的保护锁
} }
// 创建新的分片内存缓存 // 创建新的分片内存缓存
@@ -198,23 +200,37 @@ func (c *ShardedMemoryCache) GetLastModified(key string) (time.Time, bool) {
return item.lastModified, true return item.lastModified, true
} }
// 从指定分片中驱逐最久未使用的项 // 从指定分片中驱逐最久未使用的项(带磁盘备份)
func (c *ShardedMemoryCache) evictFromShard(shard *memoryCacheShard) { func (c *ShardedMemoryCache) evictFromShard(shard *memoryCacheShard) {
var oldestKey string var oldestKey string
var oldestItem *shardedMemoryCacheItem
var oldestTime int64 = 9223372036854775807 // int64最大值 var oldestTime int64 = 9223372036854775807 // int64最大值
for k, v := range shard.items { for k, v := range shard.items {
lastUsed := atomic.LoadInt64(&v.lastUsed) lastUsed := atomic.LoadInt64(&v.lastUsed)
if lastUsed < oldestTime { if lastUsed < oldestTime {
oldestKey = k oldestKey = k
oldestItem = v
oldestTime = lastUsed oldestTime = lastUsed
} }
} }
// 如果找到了最久未使用的项,删除它 // 如果找到了最久未使用的项,删除它
if oldestKey != "" { if oldestKey != "" && oldestItem != nil {
item := shard.items[oldestKey] // 🔥 关键优化:淘汰前检查是否需要刷盘保护
atomic.AddInt64(&shard.currSize, -int64(item.size)) 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) delete(shard.items, oldestKey)
} }
} }
@@ -281,4 +297,18 @@ func (c *ShardedMemoryCache) StartCleanupTask() {
c.CleanExpired() 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
} }