mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 19:37:33 +08:00
update: cache
This commit is contained in:
@@ -49,6 +49,7 @@ type ResourceRepository interface {
|
|||||||
DeleteRelatedResources(ckID uint) (int64, error)
|
DeleteRelatedResources(ckID uint) (int64, error)
|
||||||
CountResourcesByCkID(ckID uint) (int64, error)
|
CountResourcesByCkID(ckID uint) (int64, error)
|
||||||
FindByKey(key string) ([]entity.Resource, error)
|
FindByKey(key string) ([]entity.Resource, error)
|
||||||
|
GetHotResources(limit int) ([]entity.Resource, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceRepositoryImpl Resource的Repository实现
|
// ResourceRepositoryImpl Resource的Repository实现
|
||||||
@@ -355,12 +356,37 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
|||||||
// 计算偏移量
|
// 计算偏移量
|
||||||
offset := (page - 1) * pageSize
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
// 获取分页数据,按更新时间倒序
|
// 处理排序参数
|
||||||
|
orderBy := "updated_at"
|
||||||
|
orderDir := "DESC"
|
||||||
|
|
||||||
|
if orderByVal, ok := params["order_by"].(string); ok && orderByVal != "" {
|
||||||
|
// 验证排序字段,防止SQL注入
|
||||||
|
validOrderByFields := map[string]bool{
|
||||||
|
"created_at": true,
|
||||||
|
"updated_at": true,
|
||||||
|
"view_count": true,
|
||||||
|
"title": true,
|
||||||
|
"id": true,
|
||||||
|
}
|
||||||
|
if validOrderByFields[orderByVal] {
|
||||||
|
orderBy = orderByVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if orderDirVal, ok := params["order_dir"].(string); ok && orderDirVal != "" {
|
||||||
|
// 验证排序方向
|
||||||
|
if orderDirVal == "ASC" || orderDirVal == "DESC" {
|
||||||
|
orderDir = orderDirVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分页数据,应用排序
|
||||||
queryStart := utils.GetCurrentTime()
|
queryStart := utils.GetCurrentTime()
|
||||||
err := db.Order("updated_at DESC").Offset(offset).Limit(pageSize).Find(&resources).Error
|
err := db.Order(fmt.Sprintf("%s %s", orderBy, orderDir)).Offset(offset).Limit(pageSize).Find(&resources).Error
|
||||||
queryDuration := time.Since(queryStart)
|
queryDuration := time.Since(queryStart)
|
||||||
totalDuration := time.Since(startTime)
|
totalDuration := time.Since(startTime)
|
||||||
utils.Debug("SearchWithFilters完成: 总数=%d, 当前页数据量=%d, 查询耗时=%v, 总耗时=%v", total, len(resources), queryDuration, totalDuration)
|
utils.Debug("SearchWithFilters完成: 总数=%d, 当前页数据量=%d, 排序=%s %s, 查询耗时=%v, 总耗时=%v", total, len(resources), orderBy, orderDir, queryDuration, totalDuration)
|
||||||
return resources, total, err
|
return resources, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,3 +750,41 @@ func (r *ResourceRepositoryImpl) FindByKey(key string) ([]entity.Resource, error
|
|||||||
Find(&resources).Error
|
Find(&resources).Error
|
||||||
return resources, err
|
return resources, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHotResources 获取热门资源(按查看次数排序,去重,限制数量)
|
||||||
|
func (r *ResourceRepositoryImpl) GetHotResources(limit int) ([]entity.Resource, error) {
|
||||||
|
var resources []entity.Resource
|
||||||
|
|
||||||
|
// 按key分组,获取每个key中查看次数最高的资源,然后按查看次数排序
|
||||||
|
err := r.db.Table("resources").
|
||||||
|
Select(`
|
||||||
|
resources.*,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY key ORDER BY view_count DESC) as rn
|
||||||
|
`).
|
||||||
|
Where("is_public = ? AND view_count > 0", true).
|
||||||
|
Preload("Category").
|
||||||
|
Preload("Pan").
|
||||||
|
Preload("Tags").
|
||||||
|
Order("view_count DESC").
|
||||||
|
Limit(limit * 2). // 获取更多数据以确保去重后有足够的结果
|
||||||
|
Find(&resources).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按key去重,保留每个key的第一个(即查看次数最高的)
|
||||||
|
seenKeys := make(map[string]bool)
|
||||||
|
var hotResources []entity.Resource
|
||||||
|
for _, resource := range resources {
|
||||||
|
if !seenKeys[resource.Key] {
|
||||||
|
seenKeys[resource.Key] = true
|
||||||
|
hotResources = append(hotResources, resource)
|
||||||
|
if len(hotResources) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hotResources, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
pan "github.com/ctwj/urldb/common"
|
pan "github.com/ctwj/urldb/common"
|
||||||
commonutils "github.com/ctwj/urldb/common/utils"
|
commonutils "github.com/ctwj/urldb/common/utils"
|
||||||
@@ -902,6 +903,118 @@ func transferSingleResource(resource *entity.Resource, account entity.Cks, facto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHotResources 获取热门资源
|
||||||
|
func GetHotResources(c *gin.Context) {
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||||
|
|
||||||
|
utils.Info("获取热门资源请求 - limit: %d", limit)
|
||||||
|
|
||||||
|
// 限制最大请求数量
|
||||||
|
if limit > 20 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用公共缓存机制
|
||||||
|
cacheKey := fmt.Sprintf("hot_resources_%d", limit)
|
||||||
|
ttl := time.Hour // 1小时缓存
|
||||||
|
cacheManager := utils.GetHotResourcesCache()
|
||||||
|
|
||||||
|
// 尝试从缓存获取
|
||||||
|
if cachedData, found := cacheManager.Get(cacheKey, ttl); found {
|
||||||
|
utils.Info("使用热门资源缓存 - key: %s", cacheKey)
|
||||||
|
c.Header("Cache-Control", "public, max-age=3600")
|
||||||
|
c.Header("ETag", fmt.Sprintf("hot-resources-%d", len(cachedData.([]gin.H))))
|
||||||
|
|
||||||
|
// 转换为正确的类型
|
||||||
|
if data, ok := cachedData.([]gin.H); ok {
|
||||||
|
SuccessResponse(c, gin.H{
|
||||||
|
"data": data,
|
||||||
|
"total": len(data),
|
||||||
|
"limit": limit,
|
||||||
|
"cached": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存未命中,从数据库获取
|
||||||
|
resources, err := repoManager.ResourceRepository.GetHotResources(limit)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("获取热门资源失败: %v", err)
|
||||||
|
ErrorResponse(c, "获取热门资源失败", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取违禁词配置
|
||||||
|
cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) {
|
||||||
|
return repoManager.SystemConfigRepository.GetConfigValue(entity.ConfigKeyForbiddenWords)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("获取违禁词配置失败: %v", err)
|
||||||
|
cleanWords = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理违禁词并转换为响应格式
|
||||||
|
var resourceResponses []gin.H
|
||||||
|
for _, resource := range resources {
|
||||||
|
// 检查违禁词
|
||||||
|
forbiddenInfo := utils.CheckResourceForbiddenWords(resource.Title, resource.Description, cleanWords)
|
||||||
|
|
||||||
|
resourceResponse := gin.H{
|
||||||
|
"id": resource.ID,
|
||||||
|
"key": resource.Key,
|
||||||
|
"title": forbiddenInfo.ProcessedTitle,
|
||||||
|
"url": resource.URL,
|
||||||
|
"description": forbiddenInfo.ProcessedDesc,
|
||||||
|
"pan_id": resource.PanID,
|
||||||
|
"view_count": resource.ViewCount,
|
||||||
|
"created_at": resource.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
"updated_at": resource.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
"cover": resource.Cover,
|
||||||
|
"author": resource.Author,
|
||||||
|
"file_size": resource.FileSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加违禁词标记
|
||||||
|
resourceResponse["has_forbidden_words"] = forbiddenInfo.HasForbiddenWords
|
||||||
|
resourceResponse["forbidden_words"] = forbiddenInfo.ForbiddenWords
|
||||||
|
|
||||||
|
// 添加标签信息
|
||||||
|
var tagResponses []gin.H
|
||||||
|
if len(resource.Tags) > 0 {
|
||||||
|
for _, tag := range resource.Tags {
|
||||||
|
tagResponse := gin.H{
|
||||||
|
"id": tag.ID,
|
||||||
|
"name": tag.Name,
|
||||||
|
"description": tag.Description,
|
||||||
|
}
|
||||||
|
tagResponses = append(tagResponses, tagResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resourceResponse["tags"] = tagResponses
|
||||||
|
|
||||||
|
resourceResponses = append(resourceResponses, resourceResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储到缓存
|
||||||
|
cacheManager.Set(cacheKey, resourceResponses)
|
||||||
|
utils.Info("热门资源已缓存 - key: %s, count: %d", cacheKey, len(resourceResponses))
|
||||||
|
|
||||||
|
// 设置缓存头
|
||||||
|
c.Header("Cache-Control", "public, max-age=3600")
|
||||||
|
c.Header("ETag", fmt.Sprintf("hot-resources-%d", len(resourceResponses)))
|
||||||
|
|
||||||
|
SuccessResponse(c, gin.H{
|
||||||
|
"data": resourceResponses,
|
||||||
|
"total": len(resourceResponses),
|
||||||
|
"limit": limit,
|
||||||
|
"cached": false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// GetRelatedResources 获取相关资源
|
// GetRelatedResources 获取相关资源
|
||||||
func GetRelatedResources(c *gin.Context) {
|
func GetRelatedResources(c *gin.Context) {
|
||||||
// 获取查询参数
|
// 获取查询参数
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -233,6 +233,7 @@ func main() {
|
|||||||
|
|
||||||
// 资源管理
|
// 资源管理
|
||||||
api.GET("/resources", handlers.GetResources)
|
api.GET("/resources", handlers.GetResources)
|
||||||
|
api.GET("/resources/hot", handlers.GetHotResources)
|
||||||
api.POST("/resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateResource)
|
api.POST("/resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateResource)
|
||||||
api.PUT("/resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateResource)
|
api.PUT("/resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateResource)
|
||||||
api.DELETE("/resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteResource)
|
api.DELETE("/resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteResource)
|
||||||
|
|||||||
96
scheduler/cache_cleaner.go
Normal file
96
scheduler/cache_cleaner.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package scheduler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"github.com/ctwj/urldb/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CacheCleaner 缓存清理调度器
|
||||||
|
type CacheCleaner struct {
|
||||||
|
baseScheduler *BaseScheduler
|
||||||
|
running bool
|
||||||
|
ticker *time.Ticker
|
||||||
|
stopChan chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCacheCleaner 创建缓存清理调度器
|
||||||
|
func NewCacheCleaner(baseScheduler *BaseScheduler) *CacheCleaner {
|
||||||
|
return &CacheCleaner{
|
||||||
|
baseScheduler: baseScheduler,
|
||||||
|
running: false,
|
||||||
|
ticker: time.NewTicker(time.Hour), // 每小时执行一次
|
||||||
|
stopChan: make(chan bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start 启动缓存清理任务
|
||||||
|
func (cc *CacheCleaner) Start() {
|
||||||
|
if cc.running {
|
||||||
|
utils.Warn("缓存清理任务已在运行中")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.running = true
|
||||||
|
utils.Info("启动缓存清理任务")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cc.ticker.C:
|
||||||
|
cc.cleanCache()
|
||||||
|
case <-cc.stopChan:
|
||||||
|
cc.running = false
|
||||||
|
utils.Info("缓存清理任务已停止")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止缓存清理任务
|
||||||
|
func (cc *CacheCleaner) Stop() {
|
||||||
|
if !cc.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
close(cc.stopChan)
|
||||||
|
cc.ticker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanCache 执行缓存清理
|
||||||
|
func (cc *CacheCleaner) cleanCache() {
|
||||||
|
utils.Debug("开始清理过期缓存")
|
||||||
|
|
||||||
|
// 清理过期缓存(1小时TTL)
|
||||||
|
utils.CleanAllExpiredCaches(time.Hour)
|
||||||
|
utils.Debug("定期清理过期缓存完成")
|
||||||
|
|
||||||
|
// 可以在这里添加其他缓存清理逻辑,比如:
|
||||||
|
// - 清理特定模式的缓存
|
||||||
|
// - 记录缓存统计信息
|
||||||
|
cc.logCacheStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// logCacheStats 记录缓存统计信息
|
||||||
|
func (cc *CacheCleaner) logCacheStats() {
|
||||||
|
hotCacheSize := utils.GetHotResourcesCache().Size()
|
||||||
|
relatedCacheSize := utils.GetRelatedResourcesCache().Size()
|
||||||
|
systemConfigSize := utils.GetSystemConfigCache().Size()
|
||||||
|
categoriesSize := utils.GetCategoriesCache().Size()
|
||||||
|
tagsSize := utils.GetTagsCache().Size()
|
||||||
|
|
||||||
|
totalSize := hotCacheSize + relatedCacheSize + systemConfigSize + categoriesSize + tagsSize
|
||||||
|
|
||||||
|
utils.Debug("缓存统计 - 热门资源: %d, 相关资源: %d, 系统配置: %d, 分类: %d, 标签: %d, 总计: %d",
|
||||||
|
hotCacheSize, relatedCacheSize, systemConfigSize, categoriesSize, tagsSize, totalSize)
|
||||||
|
|
||||||
|
// 如果缓存过多,可以记录警告
|
||||||
|
if totalSize > 1000 {
|
||||||
|
utils.Warn("缓存项数量过多: %d,建议检查缓存策略", totalSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRunning 检查是否正在运行
|
||||||
|
func (cc *CacheCleaner) IsRunning() bool {
|
||||||
|
return cc.running
|
||||||
|
}
|
||||||
194
utils/cache.go
Normal file
194
utils/cache.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CacheData 缓存数据结构
|
||||||
|
type CacheData struct {
|
||||||
|
Data interface{}
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheManager 通用缓存管理器
|
||||||
|
type CacheManager struct {
|
||||||
|
cache map[string]*CacheData
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCacheManager 创建缓存管理器
|
||||||
|
func NewCacheManager() *CacheManager {
|
||||||
|
return &CacheManager{
|
||||||
|
cache: make(map[string]*CacheData),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 设置缓存
|
||||||
|
func (cm *CacheManager) Set(key string, data interface{}) {
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
cm.cache[key] = &CacheData{
|
||||||
|
Data: data,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取缓存
|
||||||
|
func (cm *CacheManager) Get(key string, ttl time.Duration) (interface{}, bool) {
|
||||||
|
cm.mutex.RLock()
|
||||||
|
defer cm.mutex.RUnlock()
|
||||||
|
|
||||||
|
if cachedData, exists := cm.cache[key]; exists {
|
||||||
|
if time.Since(cachedData.UpdatedAt) < ttl {
|
||||||
|
return cachedData.Data, true
|
||||||
|
}
|
||||||
|
// 缓存过期,删除
|
||||||
|
delete(cm.cache, key)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWithTTL 获取缓存并返回剩余TTL
|
||||||
|
func (cm *CacheManager) GetWithTTL(key string, ttl time.Duration) (interface{}, bool, time.Duration) {
|
||||||
|
cm.mutex.RLock()
|
||||||
|
defer cm.mutex.RUnlock()
|
||||||
|
|
||||||
|
if cachedData, exists := cm.cache[key]; exists {
|
||||||
|
elapsed := time.Since(cachedData.UpdatedAt)
|
||||||
|
if elapsed < ttl {
|
||||||
|
return cachedData.Data, true, ttl - elapsed
|
||||||
|
}
|
||||||
|
// 缓存过期,删除
|
||||||
|
delete(cm.cache, key)
|
||||||
|
}
|
||||||
|
return nil, false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除缓存
|
||||||
|
func (cm *CacheManager) Delete(key string) {
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
delete(cm.cache, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePattern 删除匹配模式的缓存
|
||||||
|
func (cm *CacheManager) DeletePattern(pattern string) {
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
|
||||||
|
for key := range cm.cache {
|
||||||
|
// 简单的字符串匹配,可以根据需要扩展为正则表达式
|
||||||
|
if len(pattern) > 0 && (key == pattern || (len(key) >= len(pattern) && key[:len(pattern)] == pattern)) {
|
||||||
|
delete(cm.cache, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清空所有缓存
|
||||||
|
func (cm *CacheManager) Clear() {
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
cm.cache = make(map[string]*CacheData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size 获取缓存项数量
|
||||||
|
func (cm *CacheManager) Size() int {
|
||||||
|
cm.mutex.RLock()
|
||||||
|
defer cm.mutex.RUnlock()
|
||||||
|
return len(cm.cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanExpired 清理过期缓存
|
||||||
|
func (cm *CacheManager) CleanExpired(ttl time.Duration) int {
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
|
||||||
|
cleaned := 0
|
||||||
|
now := time.Now()
|
||||||
|
for key, cachedData := range cm.cache {
|
||||||
|
if now.Sub(cachedData.UpdatedAt) >= ttl {
|
||||||
|
delete(cm.cache, key)
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeys 获取所有缓存键
|
||||||
|
func (cm *CacheManager) GetKeys() []string {
|
||||||
|
cm.mutex.RLock()
|
||||||
|
defer cm.mutex.RUnlock()
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(cm.cache))
|
||||||
|
for key := range cm.cache {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局缓存管理器实例
|
||||||
|
var (
|
||||||
|
// 热门资源缓存
|
||||||
|
HotResourcesCache = NewCacheManager()
|
||||||
|
|
||||||
|
// 相关资源缓存
|
||||||
|
RelatedResourcesCache = NewCacheManager()
|
||||||
|
|
||||||
|
// 系统配置缓存
|
||||||
|
SystemConfigCache = NewCacheManager()
|
||||||
|
|
||||||
|
// 分类缓存
|
||||||
|
CategoriesCache = NewCacheManager()
|
||||||
|
|
||||||
|
// 标签缓存
|
||||||
|
TagsCache = NewCacheManager()
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHotResourcesCache 获取热门资源缓存管理器
|
||||||
|
func GetHotResourcesCache() *CacheManager {
|
||||||
|
return HotResourcesCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRelatedResourcesCache 获取相关资源缓存管理器
|
||||||
|
func GetRelatedResourcesCache() *CacheManager {
|
||||||
|
return RelatedResourcesCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystemConfigCache 获取系统配置缓存管理器
|
||||||
|
func GetSystemConfigCache() *CacheManager {
|
||||||
|
return SystemConfigCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoriesCache 获取分类缓存管理器
|
||||||
|
func GetCategoriesCache() *CacheManager {
|
||||||
|
return CategoriesCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagsCache 获取标签缓存管理器
|
||||||
|
func GetTagsCache() *CacheManager {
|
||||||
|
return TagsCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAllCaches 清空所有全局缓存
|
||||||
|
func ClearAllCaches() {
|
||||||
|
HotResourcesCache.Clear()
|
||||||
|
RelatedResourcesCache.Clear()
|
||||||
|
SystemConfigCache.Clear()
|
||||||
|
CategoriesCache.Clear()
|
||||||
|
TagsCache.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanAllExpiredCaches 清理所有过期缓存
|
||||||
|
func CleanAllExpiredCaches(ttl time.Duration) {
|
||||||
|
totalCleaned := 0
|
||||||
|
totalCleaned += HotResourcesCache.CleanExpired(ttl)
|
||||||
|
totalCleaned += RelatedResourcesCache.CleanExpired(ttl)
|
||||||
|
totalCleaned += SystemConfigCache.CleanExpired(ttl)
|
||||||
|
totalCleaned += CategoriesCache.CleanExpired(ttl)
|
||||||
|
totalCleaned += TagsCache.CleanExpired(ttl)
|
||||||
|
|
||||||
|
if totalCleaned > 0 {
|
||||||
|
Info("清理过期缓存完成,共清理 %d 个缓存项", totalCleaned)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ export const parseApiResponse = <T>(response: any): T => {
|
|||||||
|
|
||||||
export const useResourceApi = () => {
|
export const useResourceApi = () => {
|
||||||
const getResources = (params?: any) => useApiFetch('/resources', { params }).then(parseApiResponse)
|
const getResources = (params?: any) => useApiFetch('/resources', { params }).then(parseApiResponse)
|
||||||
|
const getHotResources = (params?: any) => useApiFetch('/resources/hot', { params }).then(parseApiResponse)
|
||||||
const getResource = (id: number) => useApiFetch(`/resources/${id}`).then(parseApiResponse)
|
const getResource = (id: number) => useApiFetch(`/resources/${id}`).then(parseApiResponse)
|
||||||
const getResourcesByKey = (key: string) => useApiFetch(`/resources/key/${key}`).then(parseApiResponse)
|
const getResourcesByKey = (key: string) => useApiFetch(`/resources/key/${key}`).then(parseApiResponse)
|
||||||
const createResource = (data: any) => useApiFetch('/resources', { method: 'POST', body: data }).then(parseApiResponse)
|
const createResource = (data: any) => useApiFetch('/resources', { method: 'POST', body: data }).then(parseApiResponse)
|
||||||
@@ -62,7 +63,7 @@ export const useResourceApi = () => {
|
|||||||
const getResourceLink = (id: number) => useApiFetch(`/resources/${id}/link`).then(parseApiResponse)
|
const getResourceLink = (id: number) => useApiFetch(`/resources/${id}/link`).then(parseApiResponse)
|
||||||
// 新增:获取相关资源
|
// 新增:获取相关资源
|
||||||
const getRelatedResources = (params?: any) => useApiFetch('/resources/related', { params }).then(parseApiResponse)
|
const getRelatedResources = (params?: any) => useApiFetch('/resources/related', { params }).then(parseApiResponse)
|
||||||
return { getResources, getResource, getResourcesByKey, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources, getResourceLink, getRelatedResources }
|
return { getResources, getHotResources, getResource, getResourcesByKey, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources, getResourceLink, getRelatedResources }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthApi = () => {
|
export const useAuthApi = () => {
|
||||||
|
|||||||
@@ -127,9 +127,12 @@ export const useSeo = () => {
|
|||||||
dynamicKeywords = `${searchKeyword},${meta.keywords}`
|
dynamicKeywords = `${searchKeyword},${meta.keywords}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成动态OG图片URL
|
// 生成动态OG图片URL,支持自定义OG图片
|
||||||
const theme = searchKeyword ? 'blue' : platformId ? 'green' : 'default'
|
let ogImageUrl = customMeta?.ogImage
|
||||||
const ogImageUrl = generateOgImageUrl(title, dynamicDescription, theme)
|
if (!ogImageUrl) {
|
||||||
|
const theme = searchKeyword ? 'blue' : platformId ? 'green' : 'default'
|
||||||
|
ogImageUrl = generateOgImageUrl(title, dynamicDescription, theme)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export default defineNuxtConfig({
|
|||||||
{ name: 'theme-color', content: '#3b82f6' },
|
{ name: 'theme-color', content: '#3b82f6' },
|
||||||
{ property: 'og:site_name', content: '老九网盘资源数据库' },
|
{ property: 'og:site_name', content: '老九网盘资源数据库' },
|
||||||
{ property: 'og:type', content: 'website' },
|
{ property: 'og:type', content: 'website' },
|
||||||
|
{ property: 'og:image', content: '/assets/images/og.webp' },
|
||||||
{ name: 'twitter:card', content: 'summary_large_image' }
|
{ name: 'twitter:card', content: 'summary_large_image' }
|
||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
|
|||||||
@@ -422,7 +422,8 @@ const updatePageSeo = () => {
|
|||||||
// 使用动态计算的标题,而不是默认的"首页"
|
// 使用动态计算的标题,而不是默认的"首页"
|
||||||
setPageSeo(pageTitle.value, {
|
setPageSeo(pageTitle.value, {
|
||||||
description: pageDescription.value,
|
description: pageDescription.value,
|
||||||
keywords: pageKeywords.value
|
keywords: pageKeywords.value,
|
||||||
|
ogImage: '/assets/images/og.webp' // 使用默认的OG图片
|
||||||
})
|
})
|
||||||
|
|
||||||
// 设置HTML属性和canonical链接
|
// 设置HTML属性和canonical链接
|
||||||
@@ -444,6 +445,12 @@ const updatePageSeo = () => {
|
|||||||
href: canonicalUrl
|
href: canonicalUrl
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
property: 'og:image',
|
||||||
|
content: '/assets/images/og.webp'
|
||||||
|
}
|
||||||
|
],
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
@@ -452,7 +459,8 @@ const updatePageSeo = () => {
|
|||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": (seoSystemConfig.value && seoSystemConfig.value.site_title) || '老九网盘资源数据库',
|
"name": (seoSystemConfig.value && seoSystemConfig.value.site_title) || '老九网盘资源数据库',
|
||||||
"description": pageDescription.value,
|
"description": pageDescription.value,
|
||||||
"url": canonicalUrl
|
"url": canonicalUrl,
|
||||||
|
"image": '/assets/images/og.webp'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -234,11 +234,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="displayRelatedResources.length > 0" class="space-y-3">
|
<div v-else-if="displayRelatedResources.length > 0" class="space-y-3">
|
||||||
<div
|
<a
|
||||||
v-for="(resource, index) in displayRelatedResources"
|
v-for="(resource, index) in displayRelatedResources"
|
||||||
:key="resource.id"
|
:key="resource.id"
|
||||||
class="group cursor-pointer"
|
:href="`/r/${resource.key}`"
|
||||||
@click="navigateToResource(resource.key)"
|
class="group block cursor-pointer"
|
||||||
|
@click.prevent="navigateToResource(resource.key)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
|
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
|
||||||
<!-- 序号 -->
|
<!-- 序号 -->
|
||||||
@@ -263,7 +264,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">
|
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
@@ -282,73 +283,58 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- 热门资源列表 -->
|
<!-- 热门资源列表 -->
|
||||||
<div v-if="hotResourcesLoading" class="space-y-4">
|
<div v-if="hotResourcesLoading" class="space-y-3">
|
||||||
<div v-for="i in 5" :key="i" class="animate-pulse">
|
<div v-for="i in 10" :key="i" class="animate-pulse">
|
||||||
<div class="flex gap-3">
|
<div class="flex items-center gap-3 p-2 rounded-lg">
|
||||||
<div class="w-16 h-20 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
<div class="w-5 h-5 bg-gray-200 dark:bg-gray-700 rounded-full flex-shrink-0"></div>
|
||||||
<div class="flex-1 space-y-2">
|
<div class="flex-1 space-y-2">
|
||||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||||
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||||
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="hotResources.length > 0" class="space-y-4">
|
<div v-else-if="hotResources.length > 0" class="space-y-3">
|
||||||
<div
|
<a
|
||||||
v-for="(resource, index) in hotResources"
|
v-for="(resource, index) in hotResources"
|
||||||
:key="resource.id"
|
:key="resource.id"
|
||||||
class="group cursor-pointer"
|
:href="`/r/${resource.key}`"
|
||||||
@click="navigateToResource(resource.key)"
|
class="group block cursor-pointer"
|
||||||
|
@click.prevent="navigateToResource(resource.key)"
|
||||||
>
|
>
|
||||||
<div class="flex gap-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
|
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
|
||||||
<!-- 排名标识 -->
|
<!-- 排名标识 -->
|
||||||
<div class="flex-shrink-0 flex items-center justify-center">
|
<div class="flex-shrink-0 flex items-center justify-center">
|
||||||
<div
|
<div
|
||||||
class="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold text-white"
|
class="w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium"
|
||||||
:class="index < 3
|
:class="index < 3
|
||||||
? 'bg-gradient-to-br from-red-500 to-orange-500 shadow-lg'
|
? 'bg-red-500 text-white'
|
||||||
: 'bg-gray-400'"
|
: 'bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300'"
|
||||||
>
|
>
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 封面图 -->
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<img
|
|
||||||
:src="getResourceImageUrl(resource)"
|
|
||||||
:alt="resource.title"
|
|
||||||
class="w-16 h-20 object-cover rounded-lg shadow-sm group-hover:shadow-md transition-shadow"
|
|
||||||
@error="handleResourceImageError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 资源信息 -->
|
<!-- 资源信息 -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-center justify-between">
|
||||||
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors flex-1">
|
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1 group-hover:text-red-600 dark:group-hover:text-red-400 transition-colors flex-1">
|
||||||
{{ resource.title }}
|
{{ resource.title }}
|
||||||
</h4>
|
</h4>
|
||||||
<div v-if="index < 3" class="flex-shrink-0">
|
<!-- 排名皇冠 -->
|
||||||
<i class="fas fa-crown text-yellow-500 text-xs"></i>
|
<div class="flex items-center gap-1 flex-shrink-0 ml-2">
|
||||||
|
<i v-if="index === 0" class="fas fa-crown text-yellow-500 text-xs" title="第一名"></i>
|
||||||
|
<i v-else-if="index === 1" class="fas fa-crown text-gray-400 text-xs" title="第二名"></i>
|
||||||
|
<i v-else-if="index === 2" class="fas fa-crown text-orange-600 text-xs" title="第三名"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-1">
|
||||||
{{ resource.description }}
|
{{ resource.description }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-2 mt-2">
|
|
||||||
<span class="text-xs text-gray-400">
|
|
||||||
<i class="fas fa-eye"></i> {{ resource.view_count || 0 }}
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-gray-400">
|
|
||||||
<i class="fas fa-heart text-red-400"></i> {{ resource.like_count || 0 }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">
|
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
@@ -774,24 +760,22 @@ const fetchHotResources = async () => {
|
|||||||
try {
|
try {
|
||||||
hotResourcesLoading.value = true
|
hotResourcesLoading.value = true
|
||||||
|
|
||||||
// 获取按浏览量排序的热门资源
|
// 使用专门的热门资源API,保持10个热门资源
|
||||||
const params = {
|
const params = {
|
||||||
limit: 8,
|
limit: 10
|
||||||
is_public: true,
|
|
||||||
order_by: 'view_count',
|
|
||||||
order_dir: 'desc'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await resourceApi.getResources(params) as any
|
const response = await resourceApi.getHotResources(params) as any
|
||||||
|
|
||||||
// 处理响应数据
|
// 处理响应数据
|
||||||
const resources = Array.isArray(response) ? response : (response?.items || [])
|
const resources = Array.isArray(response?.data) ? response.data : []
|
||||||
|
|
||||||
// 为每个资源添加模拟的点赞数(如果API没有提供)
|
hotResources.value = resources.slice(0, 10)
|
||||||
hotResources.value = resources.slice(0, 8).map((resource: any) => ({
|
|
||||||
...resource,
|
console.log('获取热门资源成功:', {
|
||||||
like_count: Math.floor(Math.random() * 1000) + 100 // 模拟点赞数
|
count: hotResources.value.length,
|
||||||
}))
|
params: params
|
||||||
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取热门资源失败:', error)
|
console.error('获取热门资源失败:', error)
|
||||||
|
|||||||
BIN
web/public/assets/images/og.webp
Normal file
BIN
web/public/assets/images/og.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Reference in New Issue
Block a user