mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 11:29:30 +08:00
重构缓存实现,大幅优化并发性能
This commit is contained in:
71
README.md
71
README.md
@@ -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
|
||||||
|
|||||||
@@ -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())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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百分比
|
||||||
|
|||||||
@@ -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 # 项目说明
|
|
||||||
```
|
|
||||||
482
docs/2-API层设计.md
482
docs/2-API层设计.md
@@ -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. **参数统一处理**:对相同语义的不同形式参数进行统一处理,确保缓存一致性
|
|
||||||
587
docs/3-服务层设计.md
587
docs/3-服务层设计.md
@@ -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. **异步操作**:非关键路径使用异步处理,如缓存写入
|
|
||||||
605
docs/4-插件系统设计.md
605
docs/4-插件系统设计.md
@@ -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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
1393
docs/5-缓存系统设计.md
1393
docs/5-缓存系统设计.md
File diff suppressed because it is too large
Load Diff
584
docs/PanSou系统开发设计文档.md
Normal file
584
docs/PanSou系统开发设计文档.md
Normal 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系统的实际实现编写,包含了系统设计的核心信息。文档内容与代码实现保持同步,专注于已实现的功能特性。*
|
||||||
1679
docs/插件开发指南.md
1679
docs/插件开发指南.md
File diff suppressed because it is too large
Load Diff
3
main.go
3
main.go
@@ -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
32
model/plugin_result.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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 实际的搜索实现
|
||||||
|
|||||||
@@ -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 实际的搜索实现
|
||||||
|
|||||||
@@ -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 实际的搜索实现
|
||||||
|
|||||||
@@ -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 实际的搜索实现
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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 实际的搜索实现
|
||||||
|
|||||||
@@ -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 实际的搜索实现
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
37
util/cache/enhanced_two_level_cache.go
vendored
37
util/cache/enhanced_two_level_cache.go
vendored
@@ -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) {
|
||||||
|
|
||||||
|
|||||||
38
util/cache/sharded_memory_cache.go
vendored
38
util/cache/sharded_memory_cache.go
vendored
@@ -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
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user