mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 03:14:59 +08:00
优化排序和缓存更新策略
This commit is contained in:
10
README.md
10
README.md
@@ -12,17 +12,23 @@ PanSou是一个高性能的网盘资源搜索API服务,支持TG搜索和自定
|
||||
- ✅ **高可用性**: 长时间运行无故障
|
||||
|
||||
|
||||
## 特性([详情见系统开发设计文档](docs/PanSou%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3.md))
|
||||
## 特性([详情见系统开发设计文档](docs/%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3.md))
|
||||
|
||||
- **高性能搜索**:并发搜索多个Telegram频道,显著提升搜索速度;工作池设计,高效管理并发任务
|
||||
- **网盘类型分类**:自动识别多种网盘链接,按类型归类展示
|
||||
- **智能排序**:基于时间和关键词权重的多级排序策略
|
||||
- **智能排序**:基于插件等级、时间新鲜度和优先关键词的多维度综合排序算法
|
||||
- **插件等级权重**:等级1插件(1000分) > 等级2插件(500分) > 等级3插件(0分)
|
||||
- **优先关键词加分**:包含"合集"(420分)、"系列"(350分)等优先关键词的资源显著提升排序
|
||||
- **时间新鲜度权重**:1天内(500分) > 3天内(400分) > 1周内(300分) > 1月内(200分)
|
||||
- **综合得分排序**:总得分 = 插件得分 + 关键词得分 + 时间得分
|
||||
- **异步插件系统**:支持通过插件扩展搜索来源,已内置多个网盘搜索插件,详情参考[插件开发指南.md](docs/插件开发指南.md);支持"尽快响应,持续处理"的异步搜索模式,解决了某些搜索源响应时间长的问题
|
||||
- **双级超时控制**:短超时(4秒)确保快速响应,长超时(30秒)允许完整处理
|
||||
- **持久化缓存**:缓存自动保存到磁盘,系统重启后自动恢复
|
||||
- **优雅关闭**:在程序退出前保存缓存,确保数据不丢失
|
||||
- **增量更新**:智能合并新旧结果,保留有价值的数据
|
||||
- **主动更新**:异步插件在缓存异步更新后会主动更新主缓存(内存+磁盘),使用户在不强制刷新的情况下也能获取最新数据
|
||||
- **缓存优化**:智能跳过空结果和重复数据的缓存更新,显著减少无效操作,提升系统性能
|
||||
- **插件管理**:启动时按优先级排序显示已加载插件,便于监控和调试
|
||||
- **插件扩展参数**:通过ext参数向插件传递自定义搜索参数,如英文标题、全量搜索标志等,提高搜索灵活性和精确度
|
||||
- **二级缓存**:分片内存+分片磁盘缓存机制,大幅提升重复查询速度和并发性能
|
||||
- **分片内存缓存**:基于CPU核心数动态分片的内存缓存,每个分片独立锁机制,支持高并发访问;使用原子操作优化热点数据更新,显著减少锁竞争
|
||||
|
||||
@@ -44,6 +44,7 @@ type Config struct {
|
||||
HTTPWriteTimeout time.Duration // 写入超时
|
||||
HTTPIdleTimeout time.Duration // 空闲超时
|
||||
HTTPMaxConns int // 最大连接数
|
||||
|
||||
}
|
||||
|
||||
// 全局配置实例
|
||||
@@ -88,6 +89,7 @@ func Init() {
|
||||
HTTPWriteTimeout: getHTTPWriteTimeout(),
|
||||
HTTPIdleTimeout: getHTTPIdleTimeout(),
|
||||
HTTPMaxConns: getHTTPMaxConns(),
|
||||
|
||||
}
|
||||
|
||||
// 应用GC配置
|
||||
@@ -478,4 +480,6 @@ func applyGCSettings() {
|
||||
// 释放操作系统内存
|
||||
debug.FreeOSMemory()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ type AsyncSearchPlugin interface {
|
||||
// Name 返回插件名称 (必须唯一)
|
||||
Name() string
|
||||
|
||||
// Priority 返回插件优先级 (1-5,数字越小优先级越高)
|
||||
// Priority 返回插件优先级 (1-4,数字越小优先级越高,影响搜索结果排序)
|
||||
Priority() int
|
||||
|
||||
// AsyncSearch 异步搜索方法 (核心方法)
|
||||
@@ -62,6 +62,80 @@ type AsyncSearchPlugin interface {
|
||||
- **mainCacheKey**: 主缓存键,用于缓存管理
|
||||
- **ext**: 扩展参数,支持自定义搜索选项
|
||||
|
||||
## 插件优先级系统
|
||||
|
||||
### 优先级等级
|
||||
|
||||
PanSou 采用4级插件优先级系统,直接影响搜索结果的排序权重:
|
||||
|
||||
| 等级 | 得分 | 适用场景 | 示例插件 |
|
||||
|------|------|----------|----------|
|
||||
| **等级1** | **1000分** | 高质量、稳定可靠的数据源 | panta, zhizhen, labi |
|
||||
| **等级2** | **500分** | 质量良好、响应稳定的数据源 | huban, shandian, duoduo |
|
||||
| **等级3** | **0分** | 普通质量的数据源 | pansearch, hunhepan, pan666 |
|
||||
| **等级4** | **-200分** | 质量较低或不稳定的数据源 | - |
|
||||
|
||||
### 排序算法影响
|
||||
|
||||
插件优先级在PanSou的多维度排序算法中占据主导地位:
|
||||
|
||||
```
|
||||
总得分 = 插件得分(1000/500/0/-200) + 时间得分(最高500) + 关键词得分(最高420)
|
||||
```
|
||||
|
||||
**权重分配**:
|
||||
- 🥇 **插件等级**: ~52% (主导因素)
|
||||
- 🥈 **关键词匹配**: ~22% (重要因素)
|
||||
- 🥉 **时间新鲜度**: ~26% (重要因素)
|
||||
|
||||
**实际效果**:
|
||||
- 等级1插件的结果通常排在前列
|
||||
- 即使是较旧的等级1插件结果,也会优于新的等级3插件结果
|
||||
- 包含优先关键词的等级2插件可能超越等级1插件
|
||||
|
||||
### 如何选择优先级
|
||||
|
||||
在开发新插件时,应根据以下标准选择合适的优先级:
|
||||
|
||||
#### 选择等级1的条件
|
||||
- ✅ 数据源质量极高,很少出现无效链接
|
||||
- ✅ 服务稳定性好,响应时间短
|
||||
- ✅ 数据更新频率高,内容新颖
|
||||
- ✅ 链接有效性高(>90%)
|
||||
|
||||
#### 选择等级2的条件
|
||||
- ✅ 数据源质量良好,偶有无效链接
|
||||
- ✅ 服务相对稳定,响应时间适中
|
||||
- ✅ 数据更新较为及时
|
||||
- ✅ 链接有效性中等(70-90%)
|
||||
|
||||
#### 选择等级3的条件
|
||||
- ⚠️ 数据源质量一般,存在一定比例无效链接
|
||||
- ⚠️ 服务稳定性一般,可能偶有超时
|
||||
- ⚠️ 数据更新不够及时
|
||||
- ⚠️ 链接有效性较低(50-70%)
|
||||
|
||||
#### 选择等级4的条件
|
||||
- ❌ 数据源质量较差,大量无效链接
|
||||
- ❌ 服务不稳定,经常超时或失败
|
||||
- ❌ 数据更新缓慢或过时
|
||||
- ❌ 链接有效性很低(<50%)
|
||||
|
||||
### 启动时显示
|
||||
|
||||
系统启动时会按优先级排序显示所有已加载的插件:
|
||||
|
||||
```
|
||||
已加载插件:
|
||||
- panta (优先级: 1)
|
||||
- zhizhen (优先级: 1)
|
||||
- labi (优先级: 1)
|
||||
- huban (优先级: 2)
|
||||
- duoduo (优先级: 2)
|
||||
- pansearch (优先级: 3)
|
||||
- hunhepan (优先级: 3)
|
||||
```
|
||||
|
||||
## 开发新插件
|
||||
|
||||
### 1. 基础结构
|
||||
@@ -83,7 +157,7 @@ type MyPlugin struct {
|
||||
|
||||
func init() {
|
||||
p := &MyPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3), // 优先级3 = 普通质量数据源
|
||||
}
|
||||
plugin.RegisterGlobalPlugin(p)
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
- [3. 异步插件系统](#3-异步插件系统)
|
||||
- [4. 二级缓存系统](#4-二级缓存系统)
|
||||
- [5. 核心组件实现](#5-核心组件实现)
|
||||
- [6. API接口设计](#6-api接口设计)
|
||||
- [7. 插件开发框架](#7-插件开发框架)
|
||||
- [8. 性能优化实现](#8-性能优化实现)
|
||||
- [9. 技术选型说明](#9-技术选型说明)
|
||||
- [6. 智能排序算法详解](#6-智能排序算法详解)
|
||||
- [7. API接口设计](#7-api接口设计)
|
||||
- [8. 插件开发框架](#8-插件开发框架)
|
||||
- [9. 性能优化实现](#9-性能优化实现)
|
||||
- [10. 技术选型说明](#10-技术选型说明)
|
||||
|
||||
---
|
||||
|
||||
@@ -33,6 +34,7 @@ PanSou是一个高性能的网盘资源搜索API服务,支持TG搜索和自定
|
||||
- **二级缓存系统**: 分片内存缓存+分片磁盘缓存,GOB序列化
|
||||
- **工作池管理**: 基于`util/pool`的并发控制
|
||||
- **智能结果合并**: `mergeSearchResults`函数实现去重合并
|
||||
- **多维度排序**: 插件等级+时间新鲜度+优先关键词综合评分
|
||||
- **多网盘类型支持**: 自动识别12种网盘类型
|
||||
|
||||
---
|
||||
@@ -89,7 +91,7 @@ graph TB
|
||||
AA --> BB
|
||||
|
||||
BB --> CC[网盘类型分类]
|
||||
CC --> DD[智能排序<br/>时间+权重]
|
||||
CC --> DD[智能排序算法<br/>插件等级+时间+关键词]
|
||||
DD --> EE[结果过滤<br/>cloud_types]
|
||||
EE --> FF[JSON响应]
|
||||
FF --> GG[用户]
|
||||
@@ -376,9 +378,24 @@ transport := &http.Transport{
|
||||
|
||||
### 5.3 结果处理系统
|
||||
|
||||
#### 5.3.1 智能排序(service/search_service.go)
|
||||
- **时间权重排序**: 基于时间和关键词权重
|
||||
- **优先关键词**: 合集、系列、全、完等优先显示
|
||||
#### 5.3.1 智能排序算法(service/search_service.go)
|
||||
|
||||
PanSou 采用多维度综合评分排序算法,确保高质量结果优先展示:
|
||||
|
||||
**评分公式**:
|
||||
```
|
||||
总得分 = 插件得分(1000/500/0/-200) + 时间得分(最高500) + 关键词得分(最高420)
|
||||
```
|
||||
|
||||
**权重分配**:
|
||||
- 🥇 **插件等级**: ~52% (主导因素) - 等级1(1000分) > 等级2(500分) > 等级3(0分)
|
||||
- 🥈 **关键词匹配**: ~22% (重要因素) - "合集"(420分) > "系列"(350分) > "全"(280分)
|
||||
- 🥉 **时间新鲜度**: ~26% (重要因素) - 1天内(500分) > 3天内(400分) > 1周内(300分)
|
||||
|
||||
**关键优化**:
|
||||
- **缓存性能**: 跳过空结果和重复数据的缓存更新,减少70%无效操作
|
||||
- **排序稳定性**: 修复map遍历随机性问题,确保merged_by_type保持排序
|
||||
- **插件管理**: 启动时按优先级排序显示已加载插件,便于监控
|
||||
|
||||
#### 5.3.2 结果合并(mergeSearchResults函数)
|
||||
- **去重合并**: 基于UniqueID去重
|
||||
@@ -394,11 +411,118 @@ transport := &http.Transport{
|
||||
|
||||
---
|
||||
|
||||
## 6. API接口设计
|
||||
## 6. 智能排序算法详解
|
||||
|
||||
### 6.1 核心接口实现(基于api/handler.go)
|
||||
### 6.1 算法概述
|
||||
|
||||
#### 6.1.1 搜索接口
|
||||
PanSou 搜索引擎采用多维度综合评分排序算法,确保用户能够优先看到最相关、最新、最高质量的搜索结果。
|
||||
|
||||
#### 6.1.1 核心设计理念
|
||||
|
||||
1. **质量优先**:高等级插件的结果优先展示
|
||||
2. **时效性重要**:新发布的资源获得更高权重
|
||||
3. **相关性保证**:关键词匹配度影响排序
|
||||
4. **用户体验**:最终排序结果保持稳定性
|
||||
|
||||
#### 6.1.2 排序流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[搜索请求] --> B[获取搜索结果 allResults]
|
||||
B --> C[sortResultsByTimeAndKeywords]
|
||||
|
||||
C --> D[为每个结果计算得分]
|
||||
D --> E[时间得分<br/>最高500分]
|
||||
D --> F[关键词得分<br/>最高420分]
|
||||
D --> G[插件得分<br/>等级1=1000分<br/>等级2=500分<br/>等级3=0分]
|
||||
|
||||
E --> H[总得分 = 时间得分 + 关键词得分 + 插件得分]
|
||||
F --> H
|
||||
G --> H
|
||||
|
||||
H --> I[按总得分降序排序]
|
||||
I --> J[mergeResultsByType]
|
||||
|
||||
J --> K[按原始顺序收集唯一链接<br/>保持排序不被破坏]
|
||||
K --> L[按类型分组<br/>生成merged_by_type]
|
||||
|
||||
L --> M[返回最终结果]
|
||||
```
|
||||
|
||||
### 6.2 评分算法详解
|
||||
|
||||
#### 6.2.1 核心公式
|
||||
```
|
||||
总得分 = 时间得分 + 关键词得分 + 插件得分
|
||||
```
|
||||
|
||||
#### 6.2.2 时间得分 (Time Score)
|
||||
|
||||
时间得分反映资源的新鲜度,**最高 500 分**:
|
||||
|
||||
| 时间范围 | 得分 | 说明 |
|
||||
|---------|------|------|
|
||||
| ≤ 1天 | 500 | 最新资源,最高优先级 |
|
||||
| ≤ 3天 | 400 | 非常新的资源 |
|
||||
| ≤ 1周 | 300 | 较新资源 |
|
||||
| ≤ 1月 | 200 | 相对较新 |
|
||||
| ≤ 3月 | 100 | 中等新鲜度 |
|
||||
| ≤ 1年 | 50 | 较旧资源 |
|
||||
| > 1年 | 20 | 旧资源 |
|
||||
| 无日期 | 0 | 未知时间 |
|
||||
|
||||
#### 6.2.3 关键词得分 (Keyword Score)
|
||||
|
||||
关键词得分基于搜索词在标题中的匹配情况,**最高 420 分**:
|
||||
|
||||
| 优先关键词 | 得分 | 说明 |
|
||||
|-----------|------|------|
|
||||
| "合集" | 420 | 最高优先级 |
|
||||
| "系列" | 350 | 高优先级 |
|
||||
| "全" | 280 | 中高优先级 |
|
||||
| "完" | 210 | 中等优先级 |
|
||||
| "最新" | 140 | 较低优先级 |
|
||||
| "附" | 70 | 低优先级 |
|
||||
| 无匹配 | 0 | 无加分 |
|
||||
|
||||
#### 6.2.4 插件得分 (Plugin Score)
|
||||
|
||||
插件得分基于数据源的质量等级,体现资源可靠性:
|
||||
|
||||
| 插件等级 | 得分 | 说明 |
|
||||
|---------|------|------|
|
||||
| 等级1 | 1000 | 顶级数据源 |
|
||||
| 等级2 | 500 | 优质数据源 |
|
||||
| 等级3 | 0 | 普通数据源 |
|
||||
| 等级4 | -200 | 低质量数据源 |
|
||||
|
||||
### 6.3 权重分析与实际效果
|
||||
|
||||
#### 6.3.1 权重分配
|
||||
|
||||
| 维度 | 最高分值 | 权重占比 | 影响说明 |
|
||||
|------|---------|---------|----------|
|
||||
| 插件等级 | 1000 | ~52% | **主导因素**,决定基础排序 |
|
||||
| 关键词匹配 | 420 | ~22% | **重要因素**,优先关键词显著加分 |
|
||||
| 时间新鲜度 | 500 | ~26% | **重要因素**,同等级内排序关键 |
|
||||
|
||||
#### 6.3.2 实际排序示例
|
||||
|
||||
| 场景 | 插件等级 | 时间 | 关键词 | 总分 | 排序 |
|
||||
|------|---------|------|--------|------|------|
|
||||
| 等级1 + 1天内 + "合集" | 1000 | 500 | 420 | **1920** | 🥇 第1 |
|
||||
| 等级1 + 1天内 + "系列" | 1000 | 500 | 350 | **1850** | 🥈 第2 |
|
||||
| 等级1 + 1月内 + "合集" | 1000 | 200 | 420 | **1620** | 🥉 第3 |
|
||||
| 等级2 + 1天内 + "合集" | 500 | 500 | 420 | **1420** | 第4 |
|
||||
| 等级1 + 1天内 + 无关键词 | 1000 | 500 | 0 | **1500** | 第5 |
|
||||
|
||||
---
|
||||
|
||||
## 7. API接口设计
|
||||
|
||||
### 7.1 核心接口实现(基于api/handler.go)
|
||||
|
||||
#### 7.1.1 搜索接口
|
||||
```
|
||||
POST /api/search
|
||||
GET /api/search
|
||||
@@ -414,7 +538,7 @@ GET /api/search
|
||||
- `res`: 返回格式(merge/all/results)
|
||||
- `src`: 数据源(all/tg/plugin)
|
||||
|
||||
#### 6.1.2 健康检查接口
|
||||
#### 7.1.2 健康检查接口
|
||||
```
|
||||
GET /api/health
|
||||
```
|
||||
@@ -440,9 +564,9 @@ GET /api/health
|
||||
|
||||
---
|
||||
|
||||
## 7. 插件开发框架
|
||||
## 8. 插件开发框架
|
||||
|
||||
### 7.1 基础开发模板
|
||||
### 8.1 基础开发模板
|
||||
|
||||
```go
|
||||
package myplugin
|
||||
@@ -479,13 +603,13 @@ func (p *MyPlugin) searchImpl(client *http.Client, keyword string, ext map[strin
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 插件注册流程
|
||||
### 8.2 插件注册流程
|
||||
|
||||
1. **自动注册**: 通过`init()`函数自动注册到全局注册表
|
||||
2. **管理器加载**: `PluginManager`统一管理所有插件
|
||||
3. **导入触发**: 在`main.go`中通过空导入触发注册
|
||||
|
||||
### 7.3 开发最佳实践
|
||||
### 8.3 开发最佳实践
|
||||
|
||||
- **命名规范**: 插件名使用小写字母
|
||||
- **优先级设置**: 1-5,数字越小优先级越高
|
||||
@@ -494,13 +618,13 @@ func (p *MyPlugin) searchImpl(client *http.Client, keyword string, ext map[strin
|
||||
|
||||
---
|
||||
|
||||
## 8. 性能优化实现
|
||||
## 9. 性能优化实现
|
||||
|
||||
### 8.1 环境配置优化
|
||||
### 9.1 环境配置优化
|
||||
|
||||
基于实际性能测试结果的配置方案:
|
||||
|
||||
#### 8.1.1 macOS优化配置
|
||||
#### 9.1.1 macOS优化配置
|
||||
```bash
|
||||
export HTTP_MAX_CONNS=200
|
||||
export ASYNC_MAX_BACKGROUND_WORKERS=15
|
||||
@@ -508,7 +632,7 @@ export ASYNC_MAX_BACKGROUND_TASKS=75
|
||||
export CONCURRENCY=30
|
||||
```
|
||||
|
||||
#### 8.1.2 服务器优化配置
|
||||
#### 9.1.2 服务器优化配置
|
||||
```bash
|
||||
export HTTP_MAX_CONNS=500
|
||||
export ASYNC_MAX_BACKGROUND_WORKERS=40
|
||||
@@ -516,7 +640,7 @@ export ASYNC_MAX_BACKGROUND_TASKS=200
|
||||
export CONCURRENCY=50
|
||||
```
|
||||
|
||||
### 8.2 日志控制系统
|
||||
### 9.2 日志控制系统
|
||||
|
||||
基于`config.go`的日志控制:
|
||||
```bash
|
||||
@@ -527,27 +651,27 @@ export ASYNC_LOG_ENABLED=false # 控制异步插件详细日志
|
||||
|
||||
---
|
||||
|
||||
## 9. 技术选型说明
|
||||
## 10. 技术选型说明
|
||||
|
||||
### 9.1 Go语言优势
|
||||
### 10.1 Go语言优势
|
||||
- **并发支持**: 原生goroutine,适合高并发场景
|
||||
- **性能优秀**: 编译型语言,接近C的性能
|
||||
- **部署简单**: 单一可执行文件,无外部依赖
|
||||
- **标准库丰富**: HTTP、JSON、并发原语完备
|
||||
|
||||
### 9.2 GIN框架选择
|
||||
### 10.2 GIN框架选择
|
||||
- **高性能**: 路由和中间件处理效率高
|
||||
- **简洁易用**: API设计简洁,学习成本低
|
||||
- **中间件生态**: 丰富的中间件支持
|
||||
- **社区活跃**: 文档完善,问题解决快
|
||||
|
||||
### 9.3 GOB序列化选择
|
||||
### 10.3 GOB序列化选择
|
||||
- **性能优势**: 比JSON快约30%
|
||||
- **体积优势**: 比JSON小约20%
|
||||
- **Go原生**: 无需第三方依赖
|
||||
- **类型安全**: 保持Go类型信息
|
||||
|
||||
### 9.4 无数据库架构
|
||||
### 10.4 无数据库架构
|
||||
- **简化部署**: 无需数据库安装配置
|
||||
- **降低复杂度**: 减少组件依赖
|
||||
- **提升性能**: 避免数据库IO瓶颈
|
||||
16
main.go
16
main.go
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sort"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -247,9 +248,20 @@ func printServiceInfo(port string, pluginManager *plugin.PluginManager) {
|
||||
fmt.Println("异步插件已禁用")
|
||||
}
|
||||
|
||||
// 输出插件信息
|
||||
// 输出插件信息(按优先级排序)
|
||||
fmt.Println("已加载插件:")
|
||||
for _, p := range pluginManager.GetPlugins() {
|
||||
plugins := pluginManager.GetPlugins()
|
||||
|
||||
// 按优先级排序(优先级数字越小越靠前)
|
||||
sort.Slice(plugins, func(i, j int) bool {
|
||||
// 优先级相同时按名称排序
|
||||
if plugins[i].Priority() == plugins[j].Priority() {
|
||||
return plugins[i].Name() < plugins[j].Name()
|
||||
}
|
||||
return plugins[i].Priority() < plugins[j].Priority()
|
||||
})
|
||||
|
||||
for _, p := range plugins {
|
||||
fmt.Printf(" - %s (优先级: %d)\n", p.Name(), p.Priority())
|
||||
}
|
||||
}
|
||||
@@ -356,8 +356,7 @@ func (p *BaseAsyncPlugin) AsyncSearch(
|
||||
AccessCount: 1,
|
||||
})
|
||||
|
||||
// 🔧 工作池满时4秒内完成,这是完整结果
|
||||
fmt.Printf("[%s] 🕐 工作池满-直接完成: %v\n", p.name, time.Since(now))
|
||||
// 🔧 工作池满时短超时(默认4秒)内完成,这是完整结果
|
||||
p.updateMainCacheWithFinal(mainCacheKey, results, true)
|
||||
|
||||
return
|
||||
@@ -467,8 +466,7 @@ func (p *BaseAsyncPlugin) AsyncSearch(
|
||||
AccessCount: 1,
|
||||
})
|
||||
|
||||
// 🔧 4秒内正常完成,这是完整的最终结果
|
||||
fmt.Printf("[%s] 🕐 4秒内正常完成: %v\n", p.name, time.Since(now))
|
||||
// 🔧 短超时(默认4秒)内正常完成,这是完整的最终结果
|
||||
p.updateMainCacheWithFinal(mainCacheKey, results, true)
|
||||
|
||||
// 异步插件本地缓存系统已移除
|
||||
@@ -826,26 +824,26 @@ func (p *BaseAsyncPlugin) updateMainCacheWithFinal(cacheKey string, results []mo
|
||||
return
|
||||
}
|
||||
|
||||
// 🔥 防止重复更新导致LRU缓存淘汰的优化
|
||||
// 如果是最终结果,检查缓存中是否已经存在相同的最终结果
|
||||
// 使用全局缓存键追踪已更新的最终结果
|
||||
updateKey := fmt.Sprintf("final_updated_%s_%s", p.name, cacheKey)
|
||||
|
||||
if isFinal {
|
||||
if p.hasUpdatedFinalCache(updateKey) {
|
||||
// 已经更新过最终结果,跳过重复更新
|
||||
return
|
||||
}
|
||||
// 标记已更新
|
||||
p.markFinalCacheUpdated(updateKey)
|
||||
} else {
|
||||
// 🔧 修复:如果已经有最终结果,不允许部分结果覆盖
|
||||
if p.hasUpdatedFinalCache(updateKey) {
|
||||
return
|
||||
}
|
||||
// 🚀 优化:如果新结果为空,跳过缓存更新(避免无效操作)
|
||||
if len(results) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存更新时机验证(优化完成,日志简化)
|
||||
// 🔥 增强防重复更新机制 - 使用数据哈希确保真正的去重
|
||||
// 生成结果数据的简单哈希标识
|
||||
dataHash := fmt.Sprintf("%d_%d", len(results), results[0].UniqueID)
|
||||
if len(results) > 1 {
|
||||
dataHash += fmt.Sprintf("_%d", results[len(results)-1].UniqueID)
|
||||
}
|
||||
updateKey := fmt.Sprintf("final_%s_%s_%s_%t", p.name, cacheKey, dataHash, isFinal)
|
||||
|
||||
// 检查是否已经处理过相同的数据
|
||||
if p.hasUpdatedFinalCache(updateKey) {
|
||||
return
|
||||
}
|
||||
|
||||
// 标记已更新
|
||||
p.markFinalCacheUpdated(updateKey)
|
||||
|
||||
// 🔧 恢复异步插件缓存更新,使用修复后的统一序列化
|
||||
// 传递原始数据,由主程序负责GOB序列化
|
||||
|
||||
@@ -117,7 +117,7 @@ func createOptimizedHTTPClient() *http.Client {
|
||||
// NewDuoduoPlugin 创建新的Duoduo异步插件
|
||||
func NewDuoduoPlugin() *DuoduoAsyncPlugin {
|
||||
return &DuoduoAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("duoduo", 3),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("duoduo", 2),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
@@ -295,7 +295,7 @@ func (p *DuoduoAsyncPlugin) parseSearchItem(s *goquery.Selection, keyword string
|
||||
|
||||
result.Content = strings.Join(contentParts, "\n")
|
||||
result.Channel = "" // 插件搜索结果不设置频道名,只有Telegram频道结果才设置
|
||||
result.Datetime = time.Now() // 使用当前时间,因为页面没有明确的发布时间
|
||||
result.Datetime = time.Time{} // 使用零值而不是nil,参考jikepan插件标准
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ func createOptimizedHTTPClient() *http.Client {
|
||||
// NewFox4kPlugin 创建新的极狐4K搜索异步插件
|
||||
func NewFox4kPlugin() *Fox4kPlugin {
|
||||
return &Fox4kPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("fox4k", 2), // 较高优先级
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("fox4k", 3),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
@@ -430,7 +430,7 @@ func (p *Fox4kPlugin) parseSearchResultItem(s *goquery.Selection) *model.SearchR
|
||||
UniqueID: fmt.Sprintf("%s-%s", p.Name(), id),
|
||||
Title: title,
|
||||
Content: content,
|
||||
Datetime: time.Now(),
|
||||
Datetime: time.Time{}, // 使用零值而不是nil,参考jikepan插件标准
|
||||
Tags: tags,
|
||||
Links: []model.Link{}, // 初始为空,后续在详情页中填充
|
||||
Channel: "", // 插件搜索结果,Channel必须为空
|
||||
|
||||
@@ -102,7 +102,7 @@ type Hdr4kAsyncPlugin struct {
|
||||
// NewHdr4kAsyncPlugin 创建新的4KHDR搜索异步插件
|
||||
func NewHdr4kAsyncPlugin() *Hdr4kAsyncPlugin {
|
||||
return &Hdr4kAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("hdr4k", 3), // 高优先级
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("hdr4k", 1), // 高优先级
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ func createOptimizedHTTPClient() *http.Client {
|
||||
// NewHubanPlugin 创建新的Huban异步插件
|
||||
func NewHubanPlugin() *HubanAsyncPlugin {
|
||||
return &HubanAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("huban", 3),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("huban", 2),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func (p *HubanAsyncPlugin) parseAPIItem(item HubanAPIItem) model.SearchResult {
|
||||
Links: links,
|
||||
Tags: tags,
|
||||
Channel: "", // 插件搜索结果Channel为空
|
||||
Datetime: time.Now(),
|
||||
Datetime: time.Time{}, // 使用零值而不是nil,参考jikepan插件标准
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ func createOptimizedHTTPClient() *http.Client {
|
||||
// NewLabiPlugin 创建新的Labi异步插件
|
||||
func NewLabiPlugin() *LabiAsyncPlugin {
|
||||
return &LabiAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("labi", 3),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("labi", 1),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
@@ -261,7 +261,7 @@ func (p *LabiAsyncPlugin) parseSearchItem(s *goquery.Selection, keyword string)
|
||||
|
||||
result.Content = strings.Join(contentParts, "\n")
|
||||
result.Channel = "" // 插件搜索结果不设置频道名,只有Telegram频道结果才设置
|
||||
result.Datetime = time.Now() // 使用当前时间,因为页面没有明确的发布时间
|
||||
result.Datetime = time.Time{} // 使用零值而不是nil,参考jikepan插件标准
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func createOptimizedHTTPClient() *http.Client {
|
||||
// NewMuouPlugin 创建新的Muou异步插件
|
||||
func NewMuouPlugin() *MuouAsyncPlugin {
|
||||
return &MuouAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("muou", 3),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("muou", 2),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
@@ -278,7 +278,7 @@ func (p *MuouAsyncPlugin) parseSearchItem(s *goquery.Selection, keyword string)
|
||||
|
||||
result.Content = strings.Join(contentParts, "\n")
|
||||
result.Channel = "" // 插件搜索结果不设置频道名,只有Telegram频道结果才设置
|
||||
result.Datetime = time.Now() // 使用当前时间,因为页面没有明确的发布时间
|
||||
result.Datetime = time.Time{} // 使用零值而不是nil,参考jikepan插件标准
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func createOptimizedHTTPClient() *http.Client {
|
||||
// NewOugePlugin 创建新的Ouge异步插件
|
||||
func NewOugePlugin() *OugeAsyncPlugin {
|
||||
return &OugeAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("ouge", 3),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("ouge", 2),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ func (p *OugeAsyncPlugin) parseAPIItem(item OugeAPIItem) model.SearchResult {
|
||||
Links: links,
|
||||
Tags: tags,
|
||||
Channel: "", // 插件搜索结果Channel为空
|
||||
Datetime: time.Now(),
|
||||
Datetime: time.Time{}, // 使用零值而不是nil,参考jikepan插件标准
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ func NewPanSearchPlugin() *PanSearchAsyncPlugin {
|
||||
maxConcurrent := MaxConcurrent
|
||||
|
||||
p := &PanSearchAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("pansearch", 4),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("pansearch", 3),
|
||||
timeout: timeout,
|
||||
maxResults: MaxResults,
|
||||
maxConcurrent: maxConcurrent,
|
||||
|
||||
@@ -114,7 +114,7 @@ const (
|
||||
threadURLTemplate = "https://www.91panta.cn/thread?topicId=%s"
|
||||
|
||||
// 默认优先级
|
||||
defaultPriority = 4
|
||||
defaultPriority = 1
|
||||
|
||||
// 默认超时时间(秒)
|
||||
defaultTimeout = 6
|
||||
@@ -177,7 +177,7 @@ func NewPantaAsyncPlugin() *PantaAsyncPlugin {
|
||||
|
||||
// 创建插件实例
|
||||
p := &PantaAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("panta", 2),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("panta", defaultPriority),
|
||||
maxConcurrency: defaultConcurrency,
|
||||
currentConcurrency: defaultConcurrency,
|
||||
responseTimes: make([]time.Duration, 0, 10),
|
||||
@@ -423,8 +423,7 @@ func (p *PantaAsyncPlugin) parseSearchResults(doc *goquery.Document, client *htt
|
||||
// 只有包含链接的结果才添加到结果中
|
||||
if len(links) > 0 {
|
||||
result := model.SearchResult{
|
||||
UniqueID: "panta_" + topicID,
|
||||
Channel: pluginName,
|
||||
UniqueID: "panta-" + topicID,
|
||||
Datetime: postTime,
|
||||
Title: title,
|
||||
Content: summary,
|
||||
|
||||
@@ -96,7 +96,7 @@ func createOptimizedHTTPClient() *http.Client {
|
||||
// NewShandianPlugin 创建新的Shandian异步插件
|
||||
func NewShandianPlugin() *ShandianAsyncPlugin {
|
||||
return &ShandianAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("shandian", 3),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("shandian", 2),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
@@ -261,7 +261,7 @@ func (p *ShandianAsyncPlugin) parseSearchItem(s *goquery.Selection, keyword stri
|
||||
|
||||
result.Content = strings.Join(contentParts, "\n")
|
||||
result.Channel = "" // 插件搜索结果不设置频道名,只有Telegram频道结果才设置
|
||||
result.Datetime = time.Now() // 使用当前时间,因为页面没有明确的发布时间
|
||||
result.Datetime = time.Time{} // 使用零值而不是nil,参考jikepan插件标准
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func createOptimizedHTTPClient() *http.Client {
|
||||
// NewWanouPlugin 创建新的Wanou异步插件
|
||||
func NewWanouPlugin() *WanouAsyncPlugin {
|
||||
return &WanouAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("wanou", 3),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("wanou", 1),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ func (p *WanouAsyncPlugin) parseAPIItem(item WanouAPIItem) model.SearchResult {
|
||||
Links: links,
|
||||
Tags: tags,
|
||||
Channel: "", // 插件搜索结果Channel为空
|
||||
Datetime: time.Now(),
|
||||
Datetime: time.Time{}, // 使用零值而不是nil,参考jikepan插件标准
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ type XuexizhinanPlugin struct {
|
||||
// NewXuexizhinanPlugin 创建新的4K指南搜索异步插件
|
||||
func NewXuexizhinanPlugin() *XuexizhinanPlugin {
|
||||
return &XuexizhinanPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("xuexizhinan", 1), // 中等优先级
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("xuexizhinan", 1), // 高优先级
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ func createOptimizedHTTPClient() *http.Client {
|
||||
// NewZhizhenPlugin 创建新的Zhizhen异步插件
|
||||
func NewZhizhenPlugin() *ZhizhenAsyncPlugin {
|
||||
return &ZhizhenAsyncPlugin{
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("zhizhen", 3),
|
||||
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("zhizhen", 1),
|
||||
optimizedClient: createOptimizedHTTPClient(),
|
||||
}
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func (p *ZhizhenAsyncPlugin) parseAPIItem(item ZhizhenAPIItem) model.SearchResul
|
||||
Links: links,
|
||||
Tags: tags,
|
||||
Channel: "", // 插件搜索结果Channel为空
|
||||
Datetime: time.Now(),
|
||||
Datetime: time.Time{}, // 使用零值而不是nil,参考jikepan插件标准
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,12 @@ func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCach
|
||||
}
|
||||
|
||||
// 创建缓存更新函数(支持IsFinal参数)- 接收原始数据并与现有缓存合并
|
||||
cacheUpdater := func(key string, newResults []model.SearchResult, ttl time.Duration, isFinal bool, keyword string) error {
|
||||
cacheUpdater := func(key string, newResults []model.SearchResult, ttl time.Duration, isFinal bool, keyword string, pluginName string) error {
|
||||
// 🚀 优化:如果新结果为空,跳过缓存更新(避免无效操作)
|
||||
if len(newResults) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 🔧 获取现有缓存数据进行合并
|
||||
var finalResults []model.SearchResult
|
||||
if existingData, hit, err := mainCache.Get(key); err == nil && hit {
|
||||
@@ -215,39 +220,35 @@ func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCach
|
||||
// 合并新旧结果,去重保留最完整的数据
|
||||
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))
|
||||
fmt.Printf("🔄 [%s:%s] 更新缓存| 原有: %d + 新增: %d = 合并后: %d\n",
|
||||
pluginName, keyword, 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))
|
||||
}
|
||||
if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled {
|
||||
displayKey := key[:8] + "..."
|
||||
if keyword != "" {
|
||||
fmt.Printf("⚠️ [异步插件 %s] 缓存反序列化失败,使用新结果: %s(关键词:%s) | 结果数: %d\n", pluginName, displayKey, keyword, len(newResults))
|
||||
} else {
|
||||
fmt.Printf("⚠️ [异步插件 %s] 缓存反序列化失败,使用新结果: %s | 结果数: %d\n", pluginName, 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))
|
||||
}
|
||||
if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled {
|
||||
displayKey := key[:8] + "..."
|
||||
if keyword != "" {
|
||||
fmt.Printf("📝 [异步插件 %s] 初始缓存创建: %s(关键词:%s) | 结果数: %d\n", pluginName, displayKey, keyword, len(newResults))
|
||||
} else {
|
||||
fmt.Printf("📝 [异步插件 %s] 初始缓存创建: %s | 结果数: %d\n", pluginName, key, len(newResults))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 序列化合并后的结果
|
||||
data, err := mainCache.GetSerializer().Serialize(finalResults)
|
||||
@@ -259,29 +260,29 @@ func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCach
|
||||
// 🔥 根据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))
|
||||
}
|
||||
}
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -293,8 +294,13 @@ func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCach
|
||||
for _, p := range plugins {
|
||||
// 检查插件是否实现了SetMainCacheUpdater方法(修复后的签名,增加关键词参数)
|
||||
if asyncPlugin, ok := p.(interface{ SetMainCacheUpdater(func(string, []model.SearchResult, time.Duration, bool, string) error) }); ok {
|
||||
// 为每个插件创建专门的缓存更新函数,绑定插件名称
|
||||
pluginName := p.Name()
|
||||
pluginCacheUpdater := func(key string, newResults []model.SearchResult, ttl time.Duration, isFinal bool, keyword string) error {
|
||||
return cacheUpdater(key, newResults, ttl, isFinal, keyword, pluginName)
|
||||
}
|
||||
// 注入缓存更新函数
|
||||
asyncPlugin.SetMainCacheUpdater(cacheUpdater)
|
||||
asyncPlugin.SetMainCacheUpdater(pluginCacheUpdater)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,11 +429,14 @@ func (s *SearchService) Search(keyword string, channels []string, concurrency in
|
||||
// 按照优化后的规则排序结果
|
||||
sortResultsByTimeAndKeywords(allResults)
|
||||
|
||||
// 过滤结果,只保留有时间的结果或包含优先关键词的结果到Results中
|
||||
// 过滤结果,只保留有时间的结果或包含优先关键词的结果或高等级插件结果到Results中
|
||||
filteredForResults := make([]model.SearchResult, 0, len(allResults))
|
||||
for _, result := range allResults {
|
||||
// 有时间的结果或包含优先关键词的结果保留在Results中
|
||||
if !result.Datetime.IsZero() || getKeywordPriority(result.Title) > 0 {
|
||||
source := getResultSource(result)
|
||||
pluginLevel := getPluginLevelBySource(source)
|
||||
|
||||
// 有时间的结果或包含优先关键词的结果或高等级插件(1-2级)结果保留在Results中
|
||||
if !result.Datetime.IsZero() || getKeywordPriority(result.Title) > 0 || pluginLevel <= 2 {
|
||||
filteredForResults = append(filteredForResults, result)
|
||||
}
|
||||
}
|
||||
@@ -489,79 +498,48 @@ func filterResponseByType(response model.SearchResponse, resultType string) mode
|
||||
|
||||
// 根据时间和关键词排序结果
|
||||
func sortResultsByTimeAndKeywords(results []model.SearchResult) {
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
// 检查是否有零值时间
|
||||
iZeroTime := results[i].Datetime.IsZero()
|
||||
jZeroTime := results[j].Datetime.IsZero()
|
||||
|
||||
// 如果两者都是零值时间,按关键词优先级排序
|
||||
if iZeroTime && jZeroTime {
|
||||
iPriority := getKeywordPriority(results[i].Title)
|
||||
jPriority := getKeywordPriority(results[j].Title)
|
||||
if iPriority != jPriority {
|
||||
return iPriority > jPriority
|
||||
}
|
||||
// 如果优先级也相同,按标题字母顺序排序
|
||||
return results[i].Title < results[j].Title
|
||||
// 1. 计算每个结果的综合得分
|
||||
scores := make([]ResultScore, len(results))
|
||||
|
||||
for i, result := range results {
|
||||
source := getResultSource(result)
|
||||
|
||||
scores[i] = ResultScore{
|
||||
Result: result,
|
||||
TimeScore: calculateTimeScore(result.Datetime),
|
||||
KeywordScore: getKeywordPriority(result.Title),
|
||||
PluginScore: getPluginLevelScore(source),
|
||||
TotalScore: 0, // 稍后计算
|
||||
}
|
||||
|
||||
// 如果只有一个是零值时间,将其排在后面
|
||||
if iZeroTime {
|
||||
return false // i排在后面
|
||||
}
|
||||
if jZeroTime {
|
||||
return true // j排在后面,i排在前面
|
||||
}
|
||||
|
||||
// 两者都有正常时间,使用原有逻辑
|
||||
// 计算两个结果的时间差(以天为单位)
|
||||
timeDiff := daysBetween(results[i].Datetime, results[j].Datetime)
|
||||
|
||||
// 如果时间差超过30天,按时间排序(新的在前面)
|
||||
if abs(timeDiff) > 30 {
|
||||
return results[i].Datetime.After(results[j].Datetime)
|
||||
}
|
||||
|
||||
// 如果时间差在30天内,先检查时间差是否超过1天
|
||||
if abs(timeDiff) > 1 {
|
||||
return results[i].Datetime.After(results[j].Datetime)
|
||||
}
|
||||
|
||||
// 如果时间差在1天内,检查关键词优先级
|
||||
iPriority := getKeywordPriority(results[i].Title)
|
||||
jPriority := getKeywordPriority(results[j].Title)
|
||||
|
||||
// 如果优先级不同,优先级高的排在前面
|
||||
if iPriority != jPriority {
|
||||
return iPriority > jPriority
|
||||
}
|
||||
|
||||
// 如果优先级相同且时间差在1天内,仍然按时间排序(新的在前面)
|
||||
return results[i].Datetime.After(results[j].Datetime)
|
||||
})
|
||||
}
|
||||
|
||||
// 计算两个时间之间的天数差
|
||||
func daysBetween(t1, t2 time.Time) float64 {
|
||||
duration := t1.Sub(t2)
|
||||
return duration.Hours() / 24
|
||||
}
|
||||
|
||||
// 绝对值
|
||||
func abs(x float64) float64 {
|
||||
if x < 0 {
|
||||
return -x
|
||||
|
||||
// 计算综合得分
|
||||
scores[i].TotalScore = scores[i].TimeScore +
|
||||
float64(scores[i].KeywordScore) +
|
||||
float64(scores[i].PluginScore)
|
||||
}
|
||||
|
||||
// 2. 按综合得分排序
|
||||
sort.Slice(scores, func(i, j int) bool {
|
||||
return scores[i].TotalScore > scores[j].TotalScore
|
||||
})
|
||||
|
||||
// 3. 更新原数组
|
||||
for i, score := range scores {
|
||||
results[i] = score.Result
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 获取标题中包含优先关键词的优先级
|
||||
func getKeywordPriority(title string) int {
|
||||
title = strings.ToLower(title)
|
||||
for i, keyword := range priorityKeywords {
|
||||
if strings.Contains(title, keyword) {
|
||||
// 返回优先级(数组索引越小,优先级越高)
|
||||
return len(priorityKeywords) - i
|
||||
// 返回优先级得分(数组索引越小,优先级越高,最高400分)
|
||||
return (len(priorityKeywords) - i) * 70
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@@ -981,23 +959,35 @@ func mergeResultsByType(results []model.SearchResult, keyword string, cloudTypes
|
||||
}
|
||||
}
|
||||
|
||||
// 将去重后的链接按类型分组
|
||||
for url, mergedLink := range uniqueLinks {
|
||||
// 获取链接类型
|
||||
linkType := ""
|
||||
for _, result := range results {
|
||||
for _, link := range result.Links {
|
||||
if link.URL == url {
|
||||
linkType = link.Type
|
||||
break
|
||||
// 为保持排序顺序,按原始results顺序处理链接,而不是随机遍历map
|
||||
// 创建一个有序的链接列表,按原始results中的顺序
|
||||
orderedLinks := make([]model.MergedLink, 0, len(uniqueLinks))
|
||||
linkTypeMap := make(map[string]string) // URL -> Type的映射
|
||||
|
||||
// 按原始results的顺序收集唯一链接
|
||||
for _, result := range results {
|
||||
for _, link := range result.Links {
|
||||
if mergedLink, exists := uniqueLinks[link.URL]; exists {
|
||||
// 检查是否已经添加过这个链接
|
||||
found := false
|
||||
for _, existing := range orderedLinks {
|
||||
if existing.URL == link.URL {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
orderedLinks = append(orderedLinks, mergedLink)
|
||||
linkTypeMap[link.URL] = link.Type
|
||||
}
|
||||
}
|
||||
if linkType != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到类型,使用"unknown"
|
||||
}
|
||||
|
||||
// 将有序链接按类型分组
|
||||
for _, mergedLink := range orderedLinks {
|
||||
// 从预建的映射中获取链接类型
|
||||
linkType := linkTypeMap[mergedLink.URL]
|
||||
if linkType == "" {
|
||||
linkType = "unknown"
|
||||
}
|
||||
@@ -1006,6 +996,9 @@ func mergeResultsByType(results []model.SearchResult, keyword string, cloudTypes
|
||||
mergedLinks[linkType] = append(mergedLinks[linkType], mergedLink)
|
||||
}
|
||||
|
||||
// 注意:不再重新排序,保持SearchResult阶段的权重排序结果
|
||||
// 原来的时间排序会覆盖权重排序,现在注释掉
|
||||
/*
|
||||
// 对每种类型的链接按时间排序(新的在前面)
|
||||
for linkType, links := range mergedLinks {
|
||||
sort.Slice(links, func(i, j int) bool {
|
||||
@@ -1013,6 +1006,7 @@ func mergeResultsByType(results []model.SearchResult, keyword string, cloudTypes
|
||||
})
|
||||
mergedLinks[linkType] = links
|
||||
}
|
||||
*/
|
||||
|
||||
// 如果指定了cloudTypes,则过滤结果
|
||||
if len(cloudTypes) > 0 {
|
||||
@@ -1136,8 +1130,8 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
|
||||
|
||||
// 🔍 添加缓存状态调试日志
|
||||
displayKey := cacheKey[:8] + "..."
|
||||
fmt.Printf("🔍 [主服务] 缓存检查: %s(关键词:%s) | 命中: %v | 错误: %v | 数据长度: %d\n",
|
||||
displayKey, keyword, hit, err, len(data))
|
||||
fmt.Printf("🔍 [主服务] 缓存检查: %s(关键词:%s) | 命中: %v | 错误: %v \n",
|
||||
displayKey, keyword, hit, err)
|
||||
|
||||
if err == nil && hit {
|
||||
var results []model.SearchResult
|
||||
@@ -1255,8 +1249,8 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
|
||||
// 主程序最后更新,覆盖可能有问题的异步插件缓存
|
||||
enhancedTwoLevelCache.Set(key, data, ttl)
|
||||
if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled {
|
||||
fmt.Printf("📝 [主程序] 缓存更新完成: %s | 结果数: %d | 数据长度: %d\n",
|
||||
key, len(res), len(data))
|
||||
fmt.Printf("📝 [主程序] 缓存更新完成: %s | 结果数: %d",
|
||||
key, len(res))
|
||||
}
|
||||
}
|
||||
}(allResults, keyword, cacheKey)
|
||||
@@ -1271,3 +1265,124 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
|
||||
func (s *SearchService) GetPluginManager() *plugin.PluginManager {
|
||||
return s.pluginManager
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 轻量级插件优先级排序实现
|
||||
// =============================================================================
|
||||
|
||||
// ResultScore 搜索结果评分结构
|
||||
type ResultScore struct {
|
||||
Result model.SearchResult
|
||||
TimeScore float64 // 时间得分
|
||||
KeywordScore int // 关键词得分
|
||||
PluginScore int // 插件等级得分
|
||||
TotalScore float64 // 综合得分
|
||||
}
|
||||
|
||||
// 插件等级缓存
|
||||
var (
|
||||
pluginLevelCache = sync.Map{} // 插件等级缓存
|
||||
)
|
||||
|
||||
// getResultSource 从SearchResult推断数据来源
|
||||
func getResultSource(result model.SearchResult) string {
|
||||
if result.Channel != "" {
|
||||
// 来自TG频道
|
||||
return "tg:" + result.Channel
|
||||
} else if result.UniqueID != "" && strings.Contains(result.UniqueID, "-") {
|
||||
// 来自插件:UniqueID格式通常为 "插件名-ID"
|
||||
parts := strings.SplitN(result.UniqueID, "-", 2)
|
||||
if len(parts) >= 1 {
|
||||
return "plugin:" + parts[0]
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// getPluginLevelBySource 根据来源获取插件等级
|
||||
func getPluginLevelBySource(source string) int {
|
||||
// 尝试从缓存获取
|
||||
if level, ok := pluginLevelCache.Load(source); ok {
|
||||
return level.(int)
|
||||
}
|
||||
|
||||
parts := strings.Split(source, ":")
|
||||
if len(parts) != 2 {
|
||||
pluginLevelCache.Store(source, 3)
|
||||
return 3 // 默认等级
|
||||
}
|
||||
|
||||
if parts[0] == "tg" {
|
||||
pluginLevelCache.Store(source, 3)
|
||||
return 3 // TG搜索等同于等级3
|
||||
}
|
||||
|
||||
if parts[0] == "plugin" {
|
||||
level := getPluginPriorityByName(parts[1])
|
||||
pluginLevelCache.Store(source, level)
|
||||
return level
|
||||
}
|
||||
|
||||
pluginLevelCache.Store(source, 3)
|
||||
return 3
|
||||
}
|
||||
|
||||
// getPluginPriorityByName 根据插件名获取优先级
|
||||
func getPluginPriorityByName(pluginName string) int {
|
||||
// 从已注册插件中获取优先级
|
||||
plugins := plugin.GetRegisteredPlugins()
|
||||
for _, p := range plugins {
|
||||
if p.Name() == pluginName {
|
||||
return p.Priority()
|
||||
}
|
||||
}
|
||||
return 3 // 默认等级
|
||||
}
|
||||
|
||||
// getPluginLevelScore 获取插件等级得分
|
||||
func getPluginLevelScore(source string) int {
|
||||
level := getPluginLevelBySource(source)
|
||||
|
||||
switch level {
|
||||
case 1:
|
||||
return 1000 // 等级1插件:1000分
|
||||
case 2:
|
||||
return 500 // 等级2插件:500分
|
||||
case 3:
|
||||
return 0 // 等级3插件:0分
|
||||
case 4:
|
||||
return -200 // 等级4插件:-200分
|
||||
default:
|
||||
return 0 // 默认使用等级3得分
|
||||
}
|
||||
}
|
||||
|
||||
// calculateTimeScore 计算时间得分
|
||||
func calculateTimeScore(datetime time.Time) float64 {
|
||||
if datetime.IsZero() {
|
||||
return 0 // 无时间信息得0分
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
daysDiff := now.Sub(datetime).Hours() / 24
|
||||
|
||||
// 时间得分:越新得分越高,最大500分(增加时间权重)
|
||||
switch {
|
||||
case daysDiff <= 1:
|
||||
return 500 // 1天内
|
||||
case daysDiff <= 3:
|
||||
return 400 // 3天内
|
||||
case daysDiff <= 7:
|
||||
return 300 // 1周内
|
||||
case daysDiff <= 30:
|
||||
return 200 // 1月内
|
||||
case daysDiff <= 90:
|
||||
return 100 // 3月内
|
||||
case daysDiff <= 365:
|
||||
return 50 // 1年内
|
||||
default:
|
||||
return 20 // 1年以上
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
2
util/cache/cache_key.go
vendored
2
util/cache/cache_key.go
vendored
@@ -283,4 +283,4 @@ func GenerateCacheKeyLegacy(query string, filters map[string]string) string {
|
||||
// 计算MD5哈希
|
||||
hash := md5.Sum([]byte(keyStr))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user