Skip to content
This repository was archived by the owner on Nov 3, 2025. It is now read-only.

Commit 049d2df

Browse files
committed
feat: set cache ddl
1 parent 059e9cb commit 049d2df

2 files changed

Lines changed: 171 additions & 22 deletions

File tree

lib/cache.go

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,22 @@ import (
88
"helios/models"
99
)
1010

11+
// 缓存配置常量
12+
const (
13+
// 搜索缓存 TTL:10分钟
14+
SearchCacheTTL = 10 * time.Minute
15+
// 缓存清理间隔:5分钟
16+
CacheCleanupInterval = 5 * time.Minute
17+
// 最大缓存条目数量
18+
MaxCacheSize = 1000
19+
)
20+
1121
// CacheManager 缓存管理器
1222
type CacheManager struct {
13-
cache map[string]CachedSearchPage
14-
mutex sync.RWMutex
23+
cache map[string]CachedSearchPage
24+
mutex sync.RWMutex
25+
cleanupTimer *time.Timer
26+
lastCleanup time.Time
1527
}
1628

1729
// NewCacheManager 创建新的缓存管理器
@@ -31,8 +43,10 @@ func (cm *CacheManager) Get(key string) (*CachedSearchPage, bool) {
3143
return nil, false
3244
}
3345

34-
// 检查缓存是否过期(5分钟)
35-
if time.Since(cached.Timestamp) > 5*time.Minute {
46+
// 检查缓存是否过期
47+
if time.Now().After(cached.ExpiresAt) {
48+
// 异步删除过期缓存
49+
go cm.Delete(key)
3650
return nil, false
3751
}
3852

@@ -44,6 +58,15 @@ func (cm *CacheManager) Set(key string, data CachedSearchPage) {
4458
cm.mutex.Lock()
4559
defer cm.mutex.Unlock()
4660

61+
// 确保自动清理已启动
62+
cm.ensureAutoCleanupStarted()
63+
64+
// 惰性清理:每次写入时检查是否需要清理
65+
now := time.Now()
66+
if now.Sub(cm.lastCleanup) > CacheCleanupInterval {
67+
cm.performCacheCleanup()
68+
}
69+
4770
cm.cache[key] = data
4871
}
4972

@@ -76,12 +99,88 @@ func (cm *CacheManager) CleanupExpired() {
7699
cm.mutex.Lock()
77100
defer cm.mutex.Unlock()
78101

102+
cm.performCacheCleanup()
103+
}
104+
105+
// ensureAutoCleanupStarted 确保自动清理已启动(惰性初始化)
106+
func (cm *CacheManager) ensureAutoCleanupStarted() {
107+
if cm.cleanupTimer == nil {
108+
cm.startAutoCleanup()
109+
}
110+
}
111+
112+
// performCacheCleanup 智能清理过期的缓存条目
113+
func (cm *CacheManager) performCacheCleanup() {
79114
now := time.Now()
80-
for key, cached := range cm.cache {
81-
if now.Sub(cached.Timestamp) > 5*time.Minute {
82-
delete(cm.cache, key)
115+
var keysToDelete []string
116+
sizeLimitedDeleted := 0
117+
118+
// 1. 清理过期条目
119+
for key, entry := range cm.cache {
120+
if now.After(entry.ExpiresAt) {
121+
keysToDelete = append(keysToDelete, key)
83122
}
84123
}
124+
125+
expiredCount := len(keysToDelete)
126+
for _, key := range keysToDelete {
127+
delete(cm.cache, key)
128+
}
129+
130+
// 2. 如果缓存大小超限,清理最老的条目(LRU策略)
131+
if len(cm.cache) > MaxCacheSize {
132+
// 将缓存条目转换为切片进行排序
133+
type cacheEntry struct {
134+
key string
135+
expiresAt time.Time
136+
}
137+
138+
var entries []cacheEntry
139+
for key, cached := range cm.cache {
140+
entries = append(entries, cacheEntry{
141+
key: key,
142+
expiresAt: cached.ExpiresAt,
143+
})
144+
}
145+
146+
// 按照过期时间排序,最早过期的在前面
147+
for i := 0; i < len(entries)-1; i++ {
148+
for j := i + 1; j < len(entries); j++ {
149+
if entries[i].expiresAt.After(entries[j].expiresAt) {
150+
entries[i], entries[j] = entries[j], entries[i]
151+
}
152+
}
153+
}
154+
155+
toRemove := len(cm.cache) - MaxCacheSize
156+
for i := 0; i < toRemove && i < len(entries); i++ {
157+
delete(cm.cache, entries[i].key)
158+
sizeLimitedDeleted++
159+
}
160+
}
161+
162+
cm.lastCleanup = now
163+
164+
// 输出清理统计信息(可选)
165+
if expiredCount > 0 || sizeLimitedDeleted > 0 {
166+
fmt.Printf("缓存清理完成: 过期=%d, 大小限制=%d, 剩余=%d\n",
167+
expiredCount, sizeLimitedDeleted, len(cm.cache))
168+
}
169+
}
170+
171+
// startAutoCleanup 启动自动清理定时器
172+
func (cm *CacheManager) startAutoCleanup() {
173+
if cm.cleanupTimer != nil {
174+
return // 避免重复启动
175+
}
176+
177+
cm.cleanupTimer = time.AfterFunc(CacheCleanupInterval, func() {
178+
cm.CleanupExpired()
179+
// 重新设置定时器
180+
cm.cleanupTimer = time.AfterFunc(CacheCleanupInterval, func() {
181+
cm.CleanupExpired()
182+
})
183+
})
85184
}
86185

87186
// GetCacheKey 生成缓存键
@@ -94,7 +193,6 @@ var globalCacheManager = NewCacheManager()
94193

95194
// GetCachedSearchPage 获取缓存的搜索页面(全局函数)
96195
func GetCachedSearchPage(apiSiteKey, query string, page int) *CachedSearchPage {
97-
fmt.Println("GetCachedSearchPage", apiSiteKey, query, page)
98196
key := GetCacheKey(apiSiteKey, query, page)
99197
cached, exists := globalCacheManager.Get(key)
100198
if !exists {
@@ -105,13 +203,14 @@ func GetCachedSearchPage(apiSiteKey, query string, page int) *CachedSearchPage {
105203

106204
// SetCachedSearchPage 设置缓存的搜索页面(全局函数)
107205
func SetCachedSearchPage(apiSiteKey, query string, page int, status string, data []models.SearchResult, pageCount *int) {
108-
fmt.Println("SetCachedSearchPage", apiSiteKey, query, page, status, pageCount)
109206
key := GetCacheKey(apiSiteKey, query, page)
207+
now := time.Now()
110208
cached := CachedSearchPage{
111209
Status: status,
112210
Data: data,
113211
PageCount: pageCount,
114-
Timestamp: time.Now(),
212+
Timestamp: now,
213+
ExpiresAt: now.Add(SearchCacheTTL),
115214
}
116215
globalCacheManager.Set(key, cached)
117216
}

lib/search.go

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ type ApiSearchItem struct {
3030
TypeName *string `json:"type_name,omitempty"`
3131
}
3232

33+
type ApiSearchItem2 struct {
34+
VodID string `json:"vod_id"`
35+
VodName string `json:"vod_name"`
36+
VodPic string `json:"vod_pic"`
37+
VodRemarks *string `json:"vod_remarks,omitempty"`
38+
VodPlayURL *string `json:"vod_play_url,omitempty"`
39+
VodClass *string `json:"vod_class,omitempty"`
40+
VodYear *string `json:"vod_year,omitempty"`
41+
VodContent *string `json:"vod_content,omitempty"`
42+
VodDoubanID *int `json:"vod_douban_id,omitempty"`
43+
TypeName *string `json:"type_name,omitempty"`
44+
}
45+
3346
// models.SearchResult 搜索结果数据结构
3447

3548
// CachedSearchPage 缓存的搜索页面数据
@@ -38,6 +51,7 @@ type CachedSearchPage struct {
3851
Data []models.SearchResult `json:"data"`
3952
PageCount *int `json:"page_count,omitempty"`
4053
Timestamp time.Time `json:"timestamp"`
54+
ExpiresAt time.Time `json:"expires_at"` // 过期时间
4155
}
4256

4357
// 全局变量
@@ -102,40 +116,76 @@ func SearchWithCache(apiSite models.APISite, query string, page int, url string,
102116

103117
if resp.StatusCode == 403 {
104118
SetCachedSearchPage(apiSite.Key, query, page, "forbidden", []models.SearchResult{}, nil)
105-
fmt.Println("搜索请求失败", resp.StatusCode)
119+
// fmt.Println("搜索请求失败", resp.StatusCode)
106120
return []models.SearchResult{}, nil, nil
107121
}
108122

109123
if resp.StatusCode != http.StatusOK {
110-
fmt.Println("搜索请求失败", resp.StatusCode)
124+
// fmt.Println("搜索请求失败", resp.StatusCode)
111125
return []models.SearchResult{}, nil, fmt.Errorf("HTTP error: %d", resp.StatusCode)
112126
}
113127

114128
body, err := io.ReadAll(resp.Body)
115129
if err != nil {
116-
fmt.Println("搜索请求失败", err)
130+
// fmt.Println("搜索请求失败", err)
117131
return []models.SearchResult{}, nil, err
118132
}
119133

134+
// 尝试第一种数据结构 (ApiSearchItem with int VodID)
120135
var data struct {
121136
List []ApiSearchItem `json:"list"`
122137
PageCount int `json:"pagecount"`
123138
}
124139

140+
var data2 struct {
141+
List []ApiSearchItem2 `json:"list"`
142+
PageCount int `json:"pagecount"`
143+
}
144+
145+
var items []ApiSearchItem
125146
if err := json.Unmarshal(body, &data); err != nil {
126-
fmt.Println("搜索请求失败", err)
127-
return []models.SearchResult{}, nil, err
147+
// 如果第一种结构解析失败,尝试第二种结构 (ApiSearchItem2 with string VodID)
148+
if err2 := json.Unmarshal(body, &data2); err2 != nil {
149+
// fmt.Println("搜索请求失败,两种数据结构都解析失败", err, err2)
150+
fmt.Println(string(body))
151+
return []models.SearchResult{}, nil, err
152+
}
153+
// 将 ApiSearchItem2 转换为 ApiSearchItem
154+
items = make([]ApiSearchItem, len(data2.List))
155+
for i, item2 := range data2.List {
156+
// 将 string 类型的 VodID 转换为 int
157+
vodID, err := strconv.Atoi(item2.VodID)
158+
if err != nil {
159+
// 如果转换失败,跳过这个项目或使用默认值
160+
// fmt.Printf("警告: 无法转换 VodID %s 为整数,跳过该项目\n", item2.VodID)
161+
continue
162+
}
163+
items[i] = ApiSearchItem{
164+
VodID: vodID,
165+
VodName: item2.VodName,
166+
VodPic: item2.VodPic,
167+
VodRemarks: item2.VodRemarks,
168+
VodPlayURL: item2.VodPlayURL,
169+
VodClass: item2.VodClass,
170+
VodYear: item2.VodYear,
171+
VodContent: item2.VodContent,
172+
VodDoubanID: item2.VodDoubanID,
173+
TypeName: item2.TypeName,
174+
}
175+
}
176+
} else {
177+
items = data.List
128178
}
129179

130-
if len(data.List) == 0 {
180+
if len(items) == 0 {
131181
// 空结果不做负缓存要求
132-
fmt.Println("搜索请求失败", "空结果")
182+
// fmt.Println("搜索请求失败", "空结果")
133183
return []models.SearchResult{}, nil, nil
134184
}
135185

136-
// 处理结果数据
186+
// 统一处理结果数据
137187
var allResults []models.SearchResult
138-
for _, item := range data.List {
188+
for _, item := range items {
139189
var episodes []string
140190
var titles []string
141191

@@ -204,7 +254,7 @@ func SearchFromAPI(apiSite models.APISite, query string, maxPages int) ([]models
204254
// 使用新的缓存搜索函数处理第一页
205255
firstPageResults, pageCountFromFirst, err := SearchWithCache(apiSite, query, 1, apiURL, 8000)
206256
if err != nil {
207-
fmt.Println("搜索请求失败", err)
257+
// fmt.Println("搜索请求失败", err)
208258
return []models.SearchResult{}, err
209259
}
210260

@@ -343,7 +393,7 @@ func GetDetailFromAPI(apiSite models.APISite, id string) (*models.SearchResult,
343393

344394
req, err := http.NewRequestWithContext(ctx, "GET", detailURL, nil)
345395
if err != nil {
346-
fmt.Println("详情请求失败", err)
396+
// fmt.Println("详情请求失败", err)
347397
return nil, err
348398
}
349399

@@ -563,7 +613,7 @@ func FetchVideoDetail(options FetchVideoDetailOptions) (*models.SearchResult, er
563613
apiSites := config.GlobalConfig.APISites
564614
apiSite, exists := apiSites[options.Source]
565615
if !exists {
566-
fmt.Println("无效的API来源", options.Source)
616+
// fmt.Println("无效的API来源", options.Source)
567617
return nil, fmt.Errorf("无效的API来源")
568618
}
569619

0 commit comments

Comments
 (0)