diff --git a/handlers/resource_handler.go b/handlers/resource_handler.go index 8cba465..e676c59 100644 --- a/handlers/resource_handler.go +++ b/handlers/resource_handler.go @@ -8,6 +8,7 @@ import ( "time" pan "github.com/ctwj/urldb/common" + panutils "github.com/ctwj/urldb/common" commonutils "github.com/ctwj/urldb/common/utils" "github.com/ctwj/urldb/db/converter" "github.com/ctwj/urldb/db/dto" @@ -1212,6 +1213,282 @@ func GetRelatedResources(c *gin.Context) { SuccessResponse(c, responseData) } +// CheckResourceValidity 检查资源链接有效性 +func CheckResourceValidity(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + ErrorResponse(c, "无效的资源ID", http.StatusBadRequest) + return + } + + // 查询资源信息 + resource, err := repoManager.ResourceRepository.FindByID(uint(id)) + if err != nil { + ErrorResponse(c, "资源不存在", http.StatusNotFound) + return + } + + utils.Info("开始检测资源有效性 - ID: %d, URL: %s", resource.ID, resource.URL) + + // 检查缓存 + cacheKey := fmt.Sprintf("resource_validity_%d", resource.ID) + cacheManager := utils.GetResourceValidityCache() + ttl := 5 * time.Minute // 5分钟缓存 + + if cachedData, found := cacheManager.Get(cacheKey, ttl); found { + if result, ok := cachedData.(gin.H); ok { + utils.Info("使用资源有效性缓存 - ID: %d", resource.ID) + result["cached"] = true + SuccessResponse(c, result) + return + } + } + + // 执行检测:只使用深度检测实现 + isValid, detectionMethod, err := performAdvancedValidityCheck(resource) + + if err != nil { + utils.Error("深度检测资源链接失败 - ID: %d, Error: %v", resource.ID, err) + + // 深度检测失败,但不标记为无效(用户可自行验证) + result := gin.H{ + "resource_id": resource.ID, + "url": resource.URL, + "is_valid": resource.IsValid, // 保持原始状态 + "last_checked": time.Now().Format(time.RFC3339), + "error": err.Error(), + "detection_method": detectionMethod, + "cached": false, + "note": "当前网盘暂不支持自动检测,建议用户自行验证", + } + cacheManager.Set(cacheKey, result) + SuccessResponse(c, result) + return + } + + // 只有明确检测出无效的资源才更新数据库状态 + // 如果检测成功且结果与数据库状态不同,则更新 + if detectionMethod == "quark_deep" && isValid != resource.IsValid { + resource.IsValid = isValid + updateErr := repoManager.ResourceRepository.Update(resource) + if updateErr != nil { + utils.Error("更新资源有效性状态失败 - ID: %d, Error: %v", resource.ID, updateErr) + } else { + utils.Info("更新资源有效性状态 - ID: %d, Status: %v, Method: %s", resource.ID, isValid, detectionMethod) + } + } + + // 构建检测结果 + result := gin.H{ + "resource_id": resource.ID, + "url": resource.URL, + "is_valid": isValid, + "last_checked": time.Now().Format(time.RFC3339), + "detection_method": detectionMethod, + "cached": false, + } + + // 缓存检测结果 + cacheManager.Set(cacheKey, result) + + utils.Info("资源有效性检测完成 - ID: %d, Valid: %v, Method: %s", resource.ID, isValid, detectionMethod) + SuccessResponse(c, result) +} + +// performAdvancedValidityCheck 执行深度检测(只使用具体网盘服务) +func performAdvancedValidityCheck(resource *entity.Resource) (bool, string, error) { + // 提取分享ID和服务类型 + shareID, serviceType := panutils.ExtractShareId(resource.URL) + if serviceType == panutils.NotFound { + return false, "unsupported", fmt.Errorf("不支持的网盘服务: %s", resource.URL) + } + + utils.Info("开始深度检测 - Service: %s, ShareID: %s", serviceType.String(), shareID) + + // 根据服务类型选择检测策略 + switch serviceType { + case panutils.Quark: + return performQuarkValidityCheck(resource, shareID) + case panutils.Alipan: + return performAlipanValidityCheck(resource, shareID) + case panutils.BaiduPan, panutils.UC, panutils.Xunlei, panutils.Tianyi, panutils.Pan123, panutils.Pan115: + // 这些网盘暂未实现深度检测,返回不支持提示 + return false, "unsupported", fmt.Errorf("当前网盘类型 %s 暂不支持深度检测,请等待后续更新", serviceType.String()) + default: + return false, "unsupported", fmt.Errorf("未知的网盘服务类型: %s", serviceType.String()) + } +} + +// performQuarkValidityCheck 夸克网盘深度检测 +func performQuarkValidityCheck(resource *entity.Resource, shareID string) (bool, string, error) { + // 获取夸克网盘账号 + panID, err := getQuarkPanID() + if err != nil { + return false, "quark_failed", fmt.Errorf("获取夸克平台ID失败: %v", err) + } + + accounts, err := repoManager.CksRepository.FindByPanID(panID) + if err != nil { + return false, "quark_failed", fmt.Errorf("获取夸克网盘账号失败: %v", err) + } + + if len(accounts) == 0 { + return false, "quark_failed", fmt.Errorf("没有可用的夸克网盘账号") + } + + // 选择第一个有效账号 + var selectedAccount *entity.Cks + for _, account := range accounts { + if account.IsValid { + selectedAccount = &account + break + } + } + + if selectedAccount == nil { + return false, "quark_failed", fmt.Errorf("没有有效的夸克网盘账号") + } + + // 创建网盘服务配置 + config := &pan.PanConfig{ + URL: resource.URL, + Code: "", + IsType: 1, // 只获取基本信息,不转存 + ExpiredType: 1, + AdFid: "", + Stoken: "", + Cookie: selectedAccount.Ck, + } + + // 创建夸克网盘服务 + factory := pan.NewPanFactory() + panService, err := factory.CreatePanService(resource.URL, config) + if err != nil { + return false, "quark_failed", fmt.Errorf("创建夸克网盘服务失败: %v", err) + } + + // 执行深度检测(Transfer方法) + utils.Info("执行夸克网盘深度检测 - ShareID: %s", shareID) + result, err := panService.Transfer(shareID) + if err != nil { + return false, "quark_failed", fmt.Errorf("夸克网盘检测失败: %v", err) + } + + if !result.Success { + return false, "quark_failed", fmt.Errorf("夸克网盘链接无效: %s", result.Message) + } + + utils.Info("夸克网盘深度检测成功 - ShareID: %s", shareID) + return true, "quark_deep", nil +} + +// performAlipanValidityCheck 阿里云盘深度检测 +func performAlipanValidityCheck(resource *entity.Resource, shareID string) (bool, string, error) { + // 阿里云盘深度检测暂未实现 + utils.Info("阿里云盘暂不支持深度检测 - ShareID: %s", shareID) + return false, "unsupported", fmt.Errorf("阿里云盘暂不支持深度检测,请等待后续更新") +} + + +// BatchCheckResourceValidity 批量检查资源链接有效性 +func BatchCheckResourceValidity(c *gin.Context) { + var req struct { + IDs []uint `json:"ids" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + if len(req.IDs) == 0 { + ErrorResponse(c, "ID列表不能为空", http.StatusBadRequest) + return + } + + if len(req.IDs) > 20 { + ErrorResponse(c, "单次最多检测20个资源", http.StatusBadRequest) + return + } + + utils.Info("开始批量检测资源有效性 - Count: %d", len(req.IDs)) + + cacheManager := utils.GetResourceValidityCache() + ttl := 5 * time.Minute + results := make([]gin.H, 0, len(req.IDs)) + + for _, id := range req.IDs { + // 查询资源信息 + resource, err := repoManager.ResourceRepository.FindByID(id) + if err != nil { + results = append(results, gin.H{ + "resource_id": id, + "is_valid": false, + "error": "资源不存在", + "cached": false, + }) + continue + } + + // 检查缓存 + cacheKey := fmt.Sprintf("resource_validity_%d", id) + if cachedData, found := cacheManager.Get(cacheKey, ttl); found { + if result, ok := cachedData.(gin.H); ok { + result["cached"] = true + results = append(results, result) + continue + } + } + + // 执行深度检测 + isValid, detectionMethod, err := performAdvancedValidityCheck(resource) + + if err != nil { + // 深度检测失败,但不标记为无效(用户可自行验证) + result := gin.H{ + "resource_id": id, + "url": resource.URL, + "is_valid": resource.IsValid, // 保持原始状态 + "last_checked": time.Now().Format(time.RFC3339), + "error": err.Error(), + "detection_method": detectionMethod, + "cached": false, + "note": "当前网盘暂不支持自动检测,建议用户自行验证", + } + cacheManager.Set(cacheKey, result) + results = append(results, result) + continue + } + + // 只有明确检测出无效的资源才更新数据库状态 + if detectionMethod == "quark_deep" && isValid != resource.IsValid { + resource.IsValid = isValid + updateErr := repoManager.ResourceRepository.Update(resource) + if updateErr != nil { + utils.Error("更新资源有效性状态失败 - ID: %d, Error: %v", id, updateErr) + } + } + + result := gin.H{ + "resource_id": id, + "url": resource.URL, + "is_valid": isValid, + "last_checked": time.Now().Format(time.RFC3339), + "detection_method": detectionMethod, + "cached": false, + } + + cacheManager.Set(cacheKey, result) + results = append(results, result) + } + + utils.Info("批量检测资源有效性完成 - Count: %d", len(results)) + SuccessResponse(c, gin.H{ + "results": results, + "total": len(results), + }) +} + // getQuarkPanID 获取夸克网盘ID func getQuarkPanID() (uint, error) { // 通过FindAll方法查找所有平台,然后过滤出quark平台 diff --git a/main.go b/main.go index c2e555a..0a0cb45 100644 --- a/main.go +++ b/main.go @@ -247,6 +247,8 @@ func main() { api.GET("/resources/related", handlers.GetRelatedResources) api.POST("/resources/:id/view", handlers.IncrementResourceViewCount) api.GET("/resources/:id/link", handlers.GetResourceLink) + api.GET("/resources/:id/validity", handlers.CheckResourceValidity) + api.POST("/resources/validity/batch", handlers.BatchCheckResourceValidity) api.DELETE("/resources/batch", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchDeleteResources) // 分类管理 diff --git a/utils/cache.go b/utils/cache.go index 3249e91..9dab648 100644 --- a/utils/cache.go +++ b/utils/cache.go @@ -143,6 +143,9 @@ var ( // 标签缓存 TagsCache = NewCacheManager() + + // 资源有效性检测缓存 + ResourceValidityCache = NewCacheManager() ) // GetHotResourcesCache 获取热门资源缓存管理器 @@ -170,6 +173,11 @@ func GetTagsCache() *CacheManager { return TagsCache } +// GetResourceValidityCache 获取资源有效性检测缓存管理器 +func GetResourceValidityCache() *CacheManager { + return ResourceValidityCache +} + // ClearAllCaches 清空所有全局缓存 func ClearAllCaches() { HotResourcesCache.Clear() @@ -177,6 +185,7 @@ func ClearAllCaches() { SystemConfigCache.Clear() CategoriesCache.Clear() TagsCache.Clear() + ResourceValidityCache.Clear() } // CleanAllExpiredCaches 清理所有过期缓存 @@ -187,6 +196,7 @@ func CleanAllExpiredCaches(ttl time.Duration) { totalCleaned += SystemConfigCache.CleanExpired(ttl) totalCleaned += CategoriesCache.CleanExpired(ttl) totalCleaned += TagsCache.CleanExpired(ttl) + totalCleaned += ResourceValidityCache.CleanExpired(ttl) if totalCleaned > 0 { Info("清理过期缓存完成,共清理 %d 个缓存项", totalCleaned) diff --git a/web/composables/useApi.ts b/web/composables/useApi.ts index 7e21fcd..06dcf18 100644 --- a/web/composables/useApi.ts +++ b/web/composables/useApi.ts @@ -63,6 +63,10 @@ export const useResourceApi = () => { const getResourceLink = (id: number) => useApiFetch(`/resources/${id}/link`).then(parseApiResponse) // 新增:获取相关资源 const getRelatedResources = (params?: any) => useApiFetch('/resources/related', { params }).then(parseApiResponse) + // 新增:检查资源有效性 + const checkResourceValidity = (id: number) => useApiFetch(`/resources/${id}/validity`).then(parseApiResponse) + // 新增:批量检查资源有效性 + const batchCheckResourceValidity = (ids: number[]) => useApiFetch('/resources/validity/batch', { method: 'POST', body: { ids } }).then(parseApiResponse) // 新增:提交举报 const submitReport = (data: any) => useApiFetch('/reports', { method: 'POST', body: data }).then(parseApiResponse) // 新增:提交版权申述 @@ -82,7 +86,7 @@ export const useResourceApi = () => { const deleteCopyrightClaim = (id: number) => useApiFetch(`/copyright-claims/${id}`, { method: 'DELETE' }).then(parseApiResponse) return { - getResources, getHotResources, getResource, getResourcesByKey, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources, getResourceLink, getRelatedResources, + getResources, getHotResources, getResource, getResourcesByKey, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources, getResourceLink, getRelatedResources, checkResourceValidity, batchCheckResourceValidity, submitReport, submitCopyrightClaim, getReports, getReport, updateReport, deleteReport, getReportsRaw, getCopyrightClaims, getCopyrightClaim, updateCopyrightClaim, deleteCopyrightClaim diff --git a/web/pages/r/[key].vue b/web/pages/r/[key].vue index 16150ba..06aed5d 100644 --- a/web/pages/r/[key].vue +++ b/web/pages/r/[key].vue @@ -128,10 +128,19 @@
-

- - 网盘资源 ({{ resourcesData?.resources?.length || 0 }}) -

+
+

+ + 网盘资源 ({{ resourcesData?.resources?.length || 0 }}) +

+ + +
+ + {{ detectionStatus.label }} + ({{ detectionStatus.detectedCount }}已检测) +
+
- +
+ +
+ + + {{ getDetectionMethodLabel(detectionMethods[resource.id]) }} + + + + + + +
+ -
+
- 检测中 + 检测中... ({{ Object.keys(detectionResults).length }}/{{ resourcesData?.resources?.length }})