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