Files
urldb/handlers/ready_resource_handler.go

575 lines
16 KiB
Go
Raw Permalink Normal View History

2025-07-10 13:58:28 +08:00
package handlers
import (
"net/http"
"strconv"
"strings"
2025-07-18 09:42:07 +08:00
"github.com/ctwj/urldb/db/converter"
"github.com/ctwj/urldb/db/dto"
"github.com/ctwj/urldb/db/entity"
2025-07-10 13:58:28 +08:00
"github.com/gin-gonic/gin"
)
// GetReadyResources 获取待处理资源列表
func GetReadyResources(c *gin.Context) {
2025-07-11 00:49:41 +08:00
// 获取分页参数
pageStr := c.DefaultQuery("page", "1")
pageSizeStr := c.DefaultQuery("page_size", "100")
page, err := strconv.Atoi(pageStr)
if err != nil || page < 1 {
page = 1
}
pageSize, err := strconv.Atoi(pageSizeStr)
if err != nil || pageSize < 1 || pageSize > 1000 {
pageSize = 100
}
// 获取分页数据
resources, total, err := repoManager.ReadyResourceRepository.FindWithPagination(page, pageSize)
2025-07-10 13:58:28 +08:00
if err != nil {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
2025-07-10 13:58:28 +08:00
return
}
responses := converter.ToReadyResourceResponseList(resources)
2025-07-11 00:49:41 +08:00
// 使用标准化的分页响应格式
2025-07-11 17:45:16 +08:00
SuccessResponse(c, gin.H{
"data": responses,
"page": page,
"page_size": pageSize,
"total": total,
})
2025-07-10 13:58:28 +08:00
}
// BatchCreateReadyResources 批量创建待处理资源
func BatchCreateReadyResources(c *gin.Context) {
var req dto.BatchCreateReadyResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, err.Error(), http.StatusBadRequest)
2025-07-10 13:58:28 +08:00
return
}
// 1. 先收集所有待提交的URL去重
urlSet := make(map[string]struct{})
for _, reqResource := range req.Resources {
if len(reqResource.URL) == 0 {
continue
}
for _, u := range reqResource.URL {
if u != "" {
urlSet[u] = struct{}{}
}
}
}
uniqueUrls := make([]string, 0, len(urlSet))
for url := range urlSet {
uniqueUrls = append(uniqueUrls, url)
}
// 2. 批量查询待处理资源表中已存在的URL
existReadyUrls := make(map[string]struct{})
if len(uniqueUrls) > 0 {
readyList, _ := repoManager.ReadyResourceRepository.BatchFindByURLs(uniqueUrls)
for _, r := range readyList {
existReadyUrls[r.URL] = struct{}{}
}
}
// 3. 批量查询资源表中已存在的URL
existResourceUrls := make(map[string]struct{})
if len(uniqueUrls) > 0 {
resourceList, _ := repoManager.ResourceRepository.BatchFindByURLs(uniqueUrls)
for _, r := range resourceList {
existResourceUrls[r.URL] = struct{}{}
}
}
// 5. 过滤掉已存在的URL
2025-07-10 13:58:28 +08:00
var resources []entity.ReadyResource
for _, reqResource := range req.Resources {
if len(reqResource.URL) == 0 {
continue
}
key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey()
if err != nil {
ErrorResponse(c, "生成批量资源组标识失败: "+err.Error(), http.StatusInternalServerError)
return
}
for _, url := range reqResource.URL {
if url == "" {
continue
}
if _, ok := existReadyUrls[url]; ok {
continue
}
if _, ok := existResourceUrls[url]; ok {
continue
}
resource := entity.ReadyResource{
Title: reqResource.Title,
Description: reqResource.Description,
URL: url,
Category: reqResource.Category,
Tags: reqResource.Tags,
Img: reqResource.Img,
Source: reqResource.Source,
Extra: reqResource.Extra,
IP: reqResource.IP,
Key: key,
}
resources = append(resources, resource)
2025-07-10 13:58:28 +08:00
}
}
if len(resources) == 0 {
SuccessResponse(c, gin.H{
"count": 0,
"message": "无新增资源所有URL均已存在",
})
return
}
2025-07-10 13:58:28 +08:00
err := repoManager.ReadyResourceRepository.BatchCreate(resources)
if err != nil {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
2025-07-10 13:58:28 +08:00
return
}
2025-07-11 17:45:16 +08:00
SuccessResponse(c, gin.H{
"count": len(resources),
"message": "批量创建成功",
})
2025-07-10 13:58:28 +08:00
}
// CreateReadyResourcesFromText 从文本创建待处理资源
func CreateReadyResourcesFromText(c *gin.Context) {
text := c.PostForm("text")
if text == "" {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, "文本内容不能为空", http.StatusBadRequest)
2025-07-10 13:58:28 +08:00
return
}
lines := strings.Split(text, "\n")
var resources []entity.ReadyResource
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// 简单的URL提取逻辑
if strings.Contains(line, "http") {
resource := entity.ReadyResource{
URL: line,
}
resources = append(resources, resource)
}
}
if len(resources) == 0 {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, "未找到有效的URL", http.StatusBadRequest)
2025-07-10 13:58:28 +08:00
return
}
err := repoManager.ReadyResourceRepository.BatchCreate(resources)
if err != nil {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
2025-07-10 13:58:28 +08:00
return
}
2025-07-11 17:45:16 +08:00
SuccessResponse(c, gin.H{
"count": len(resources),
"message": "从文本创建成功",
})
2025-07-10 13:58:28 +08:00
}
// DeleteReadyResource 删除待处理资源
func DeleteReadyResource(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
2025-07-10 13:58:28 +08:00
return
}
err = repoManager.ReadyResourceRepository.Delete(uint(id))
if err != nil {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
2025-07-10 13:58:28 +08:00
return
}
2025-07-11 17:45:16 +08:00
SuccessResponse(c, gin.H{"message": "待处理资源删除成功"})
2025-07-10 13:58:28 +08:00
}
// ClearReadyResources 清空所有待处理资源
func ClearReadyResources(c *gin.Context) {
resources, err := repoManager.ReadyResourceRepository.FindAll()
if err != nil {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
2025-07-10 13:58:28 +08:00
return
}
for _, resource := range resources {
err = repoManager.ReadyResourceRepository.Delete(resource.ID)
if err != nil {
2025-07-11 17:45:16 +08:00
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
2025-07-10 13:58:28 +08:00
return
}
}
2025-07-11 17:45:16 +08:00
SuccessResponse(c, gin.H{
"deleted_count": len(resources),
"message": "所有待处理资源已清空",
})
2025-07-10 13:58:28 +08:00
}
// GetReadyResourcesByKey 根据key获取待处理资源
func GetReadyResourcesByKey(c *gin.Context) {
key := c.Param("key")
if key == "" {
ErrorResponse(c, "key参数不能为空", http.StatusBadRequest)
return
}
resources, err := repoManager.ReadyResourceRepository.FindByKey(key)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
responses := converter.ToReadyResourceResponseList(resources)
SuccessResponse(c, gin.H{
"data": responses,
"key": key,
"count": len(resources),
})
}
// DeleteReadyResourcesByKey 根据key删除待处理资源
func DeleteReadyResourcesByKey(c *gin.Context) {
key := c.Param("key")
if key == "" {
ErrorResponse(c, "key参数不能为空", http.StatusBadRequest)
return
}
// 先查询要删除的资源数量
resources, err := repoManager.ReadyResourceRepository.FindByKey(key)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
if len(resources) == 0 {
ErrorResponse(c, "未找到指定key的资源", http.StatusNotFound)
return
}
// 删除所有具有相同key的资源
err = repoManager.ReadyResourceRepository.DeleteByKey(key)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{
"deleted_count": len(resources),
"key": key,
"message": "资源组删除成功",
})
}
// getRetryableErrorCount 统计可重试的错误数量
func getRetryableErrorCount(resources []entity.ReadyResource) int {
count := 0
for _, resource := range resources {
if resource.ErrorMsg != "" {
errorMsg := strings.ToUpper(resource.ErrorMsg)
// 检查错误类型标记
if strings.Contains(resource.ErrorMsg, "[NO_ACCOUNT]") ||
strings.Contains(resource.ErrorMsg, "[NO_VALID_ACCOUNT]") ||
strings.Contains(resource.ErrorMsg, "[TRANSFER_FAILED]") ||
strings.Contains(resource.ErrorMsg, "[LINK_CHECK_FAILED]") {
count++
} else if strings.Contains(errorMsg, "没有可用的网盘账号") ||
strings.Contains(errorMsg, "没有有效的网盘账号") ||
strings.Contains(errorMsg, "网盘信息获取失败") ||
strings.Contains(errorMsg, "链接检查失败") {
count++
}
}
}
return count
}
// GetReadyResourcesWithErrors 获取有错误信息的待处理资源
func GetReadyResourcesWithErrors(c *gin.Context) {
// 获取分页参数
pageStr := c.DefaultQuery("page", "1")
pageSizeStr := c.DefaultQuery("page_size", "100")
errorFilter := c.Query("error_filter")
page, err := strconv.Atoi(pageStr)
if err != nil || page < 1 {
page = 1
}
pageSize, err := strconv.Atoi(pageSizeStr)
if err != nil || pageSize < 1 || pageSize > 1000 {
pageSize = 100
}
// 获取有错误的资源(分页,包括软删除的)
resources, total, err := repoManager.ReadyResourceRepository.FindWithErrorsPaginatedIncludingDeleted(page, pageSize, errorFilter)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
responses := converter.ToReadyResourceResponseList(resources)
// 统计错误类型
errorTypeStats := make(map[string]int)
for _, resource := range resources {
if resource.ErrorMsg != "" {
// 尝试从错误信息中提取错误类型
if len(resource.ErrorMsg) > 0 && resource.ErrorMsg[0] == '[' {
endIndex := strings.Index(resource.ErrorMsg, "]")
if endIndex > 0 {
errorType := resource.ErrorMsg[1:endIndex]
errorTypeStats[errorType]++
} else {
errorTypeStats["UNKNOWN"]++
}
} else {
// 如果没有错误类型标记,尝试从错误信息中推断
errorMsg := strings.ToUpper(resource.ErrorMsg)
if strings.Contains(errorMsg, "不支持的链接") {
errorTypeStats["UNSUPPORTED_LINK"]++
} else if strings.Contains(errorMsg, "链接无效") {
errorTypeStats["INVALID_LINK"]++
} else if strings.Contains(errorMsg, "没有可用的网盘账号") {
errorTypeStats["NO_ACCOUNT"]++
} else if strings.Contains(errorMsg, "没有有效的网盘账号") {
errorTypeStats["NO_VALID_ACCOUNT"]++
} else if strings.Contains(errorMsg, "网盘信息获取失败") {
errorTypeStats["TRANSFER_FAILED"]++
} else if strings.Contains(errorMsg, "创建网盘服务失败") {
errorTypeStats["SERVICE_CREATION_FAILED"]++
} else if strings.Contains(errorMsg, "处理标签失败") {
errorTypeStats["TAG_PROCESSING_FAILED"]++
} else if strings.Contains(errorMsg, "处理分类失败") {
errorTypeStats["CATEGORY_PROCESSING_FAILED"]++
} else if strings.Contains(errorMsg, "资源保存失败") {
errorTypeStats["RESOURCE_SAVE_FAILED"]++
} else if strings.Contains(errorMsg, "未找到对应的平台ID") {
errorTypeStats["PLATFORM_NOT_FOUND"]++
} else if strings.Contains(errorMsg, "链接检查失败") {
errorTypeStats["LINK_CHECK_FAILED"]++
} else {
errorTypeStats["UNKNOWN"]++
}
}
}
}
SuccessResponse(c, gin.H{
"data": responses,
"page": page,
"page_size": pageSize,
2025-08-01 15:50:04 +08:00
"total": total,
"count": len(resources),
"error_stats": errorTypeStats,
"retryable_count": getRetryableErrorCount(resources),
})
}
// ClearErrorMsg 清除指定资源的错误信息
func ClearErrorMsg(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
return
}
err = repoManager.ReadyResourceRepository.ClearErrorMsg(uint(id))
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{"message": "错误信息已清除"})
}
// RetryFailedResources 重试失败的资源
func RetryFailedResources(c *gin.Context) {
// 获取有错误的资源
resources, err := repoManager.ReadyResourceRepository.FindWithErrors()
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
if len(resources) == 0 {
SuccessResponse(c, gin.H{
"message": "没有需要重试的资源",
"count": 0,
})
return
}
// 只重试可重试的错误
clearedCount := 0
skippedCount := 0
for _, resource := range resources {
isRetryable := false
errorMsg := strings.ToUpper(resource.ErrorMsg)
// 检查错误类型标记
if strings.Contains(resource.ErrorMsg, "[NO_ACCOUNT]") ||
strings.Contains(resource.ErrorMsg, "[NO_VALID_ACCOUNT]") ||
strings.Contains(resource.ErrorMsg, "[TRANSFER_FAILED]") ||
strings.Contains(resource.ErrorMsg, "[LINK_CHECK_FAILED]") {
isRetryable = true
} else if strings.Contains(errorMsg, "没有可用的网盘账号") ||
strings.Contains(errorMsg, "没有有效的网盘账号") ||
strings.Contains(errorMsg, "网盘信息获取失败") ||
strings.Contains(errorMsg, "链接检查失败") {
isRetryable = true
}
if isRetryable {
if err := repoManager.ReadyResourceRepository.ClearErrorMsg(resource.ID); err == nil {
clearedCount++
}
} else {
skippedCount++
}
}
SuccessResponse(c, gin.H{
"message": "已清除可重试资源的错误信息,资源将在下次调度时重新处理",
"total_count": len(resources),
"cleared_count": clearedCount,
"skipped_count": skippedCount,
"retryable_count": getRetryableErrorCount(resources),
})
}
// BatchRestoreToReadyPool 批量将失败资源重新放入待处理池
func BatchRestoreToReadyPool(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
}
successCount := 0
failedCount := 0
for _, id := range req.IDs {
// 清除错误信息并恢复软删除的资源
err := repoManager.ReadyResourceRepository.ClearErrorMsgAndRestore(id)
if err != nil {
failedCount++
continue
}
successCount++
}
SuccessResponse(c, gin.H{
"message": "批量重新放入待处理池操作完成",
"total_count": len(req.IDs),
"success_count": successCount,
"failed_count": failedCount,
})
}
// BatchRestoreToReadyPoolByQuery 根据查询条件批量将失败资源重新放入待处理池
func BatchRestoreToReadyPoolByQuery(c *gin.Context) {
var req struct {
ErrorFilter string `json:"error_filter"`
}
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误: "+err.Error(), http.StatusBadRequest)
return
}
// 根据查询条件获取所有符合条件的资源
resources, err := repoManager.ReadyResourceRepository.FindWithErrorsByQuery(req.ErrorFilter)
if err != nil {
ErrorResponse(c, "查询资源失败: "+err.Error(), http.StatusInternalServerError)
return
}
if len(resources) == 0 {
SuccessResponse(c, gin.H{
"message": "没有找到符合条件的资源",
"total_count": 0,
"success_count": 0,
"failed_count": 0,
})
return
}
successCount := 0
failedCount := 0
for _, resource := range resources {
err := repoManager.ReadyResourceRepository.ClearErrorMsgAndRestore(resource.ID)
if err != nil {
failedCount++
continue
}
successCount++
}
SuccessResponse(c, gin.H{
"message": "批量重新放入待处理池操作完成",
"total_count": len(resources),
"success_count": successCount,
"failed_count": failedCount,
})
}
// ClearAllErrorsByQuery 根据查询条件批量清除错误信息并删除资源
func ClearAllErrorsByQuery(c *gin.Context) {
var req struct {
ErrorFilter string `json:"error_filter"`
}
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误: "+err.Error(), http.StatusBadRequest)
return
}
// 根据查询条件批量删除失败资源
affectedRows, err := repoManager.ReadyResourceRepository.ClearAllErrorsByQuery(req.ErrorFilter)
if err != nil {
ErrorResponse(c, "批量删除失败资源失败: "+err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{
"message": "批量删除失败资源操作完成",
"affected_rows": affectedRows,
})
}