Files
urldb/handlers/resource_handler.go

637 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"fmt"
"net/http"
"strconv"
pan "github.com/ctwj/urldb/common"
commonutils "github.com/ctwj/urldb/common/utils"
"github.com/ctwj/urldb/db/converter"
"github.com/ctwj/urldb/db/dto"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/utils"
"github.com/gin-gonic/gin"
)
// GetResources 获取资源列表
func GetResources(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
utils.Info("资源列表请求 - page: %d, pageSize: %d, User-Agent: %s", page, pageSize, c.GetHeader("User-Agent"))
// 添加缓存控制头,优化 SSR 性能
c.Header("Cache-Control", "public, max-age=30") // 30秒缓存平衡性能和实时性
c.Header("ETag", fmt.Sprintf("resources-%d-%d-%s-%s", page, pageSize, c.Query("search"), c.Query("pan_id")))
params := map[string]interface{}{
"page": page,
"page_size": pageSize,
}
if search := c.Query("search"); search != "" {
params["search"] = search
}
if panID := c.Query("pan_id"); panID != "" {
if id, err := strconv.ParseUint(panID, 10, 32); err == nil {
params["pan_id"] = uint(id)
}
}
if categoryID := c.Query("category_id"); categoryID != "" {
utils.Info("收到分类ID参数: %s", categoryID)
if id, err := strconv.ParseUint(categoryID, 10, 32); err == nil {
params["category_id"] = uint(id)
utils.Info("解析分类ID成功: %d", uint(id))
} else {
utils.Error("解析分类ID失败: %v", err)
}
}
if hasSaveURL := c.Query("has_save_url"); hasSaveURL != "" {
if hasSaveURL == "true" {
params["has_save_url"] = true
} else if hasSaveURL == "false" {
params["has_save_url"] = false
}
}
if noSaveURL := c.Query("no_save_url"); noSaveURL != "" {
if noSaveURL == "true" {
params["no_save_url"] = true
}
}
if panName := c.Query("pan_name"); panName != "" {
params["pan_name"] = panName
}
resources, total, err := repoManager.ResourceRepository.SearchWithFilters(params)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{
"data": converter.ToResourceResponseList(resources),
"total": total,
"page": page,
"page_size": pageSize,
})
}
// GetResourceByID 根据ID获取资源
func GetResourceByID(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
}
response := converter.ToResourceResponse(resource)
SuccessResponse(c, response)
}
// CheckResourceExists 检查资源是否存在测试FindExists函数
func CheckResourceExists(c *gin.Context) {
url := c.Query("url")
if url == "" {
ErrorResponse(c, "URL参数不能为空", http.StatusBadRequest)
return
}
excludeIDStr := c.Query("exclude_id")
var excludeID uint
if excludeIDStr != "" {
if id, err := strconv.ParseUint(excludeIDStr, 10, 32); err == nil {
excludeID = uint(id)
}
}
exists, err := repoManager.ResourceRepository.FindExists(url, excludeID)
if err != nil {
ErrorResponse(c, "检查失败: "+err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{
"url": url,
"exists": exists,
})
}
// CreateResource 创建资源
func CreateResource(c *gin.Context) {
var req dto.CreateResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, err.Error(), http.StatusBadRequest)
return
}
resource := &entity.Resource{
Title: req.Title,
Description: req.Description,
URL: req.URL,
PanID: req.PanID,
SaveURL: req.SaveURL,
FileSize: req.FileSize,
CategoryID: req.CategoryID,
IsValid: req.IsValid,
IsPublic: req.IsPublic,
Cover: req.Cover,
Author: req.Author,
ErrorMsg: req.ErrorMsg,
}
err := repoManager.ResourceRepository.Create(resource)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
// 处理标签关联
if len(req.TagIDs) > 0 {
err = repoManager.ResourceRepository.UpdateWithTags(resource, req.TagIDs)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
}
SuccessResponse(c, gin.H{
"message": "资源创建成功",
"resource": converter.ToResourceResponse(resource),
})
}
// UpdateResource 更新资源
func UpdateResource(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
return
}
var req dto.UpdateResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, err.Error(), http.StatusBadRequest)
return
}
resource, err := repoManager.ResourceRepository.FindByID(uint(id))
if err != nil {
ErrorResponse(c, "资源不存在", http.StatusNotFound)
return
}
// 更新资源信息
if req.Title != "" {
resource.Title = req.Title
}
if req.Description != "" {
resource.Description = req.Description
}
if req.URL != "" {
resource.URL = req.URL
}
if req.PanID != nil {
resource.PanID = req.PanID
}
if req.SaveURL != "" {
resource.SaveURL = req.SaveURL
}
if req.FileSize != "" {
resource.FileSize = req.FileSize
}
if req.CategoryID != nil {
resource.CategoryID = req.CategoryID
}
resource.IsValid = req.IsValid
resource.IsPublic = req.IsPublic
if req.Cover != "" {
resource.Cover = req.Cover
}
if req.Author != "" {
resource.Author = req.Author
}
if req.ErrorMsg != "" {
resource.ErrorMsg = req.ErrorMsg
}
// 处理标签关联
if len(req.TagIDs) > 0 {
err = repoManager.ResourceRepository.UpdateWithTags(resource, req.TagIDs)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
} else {
err = repoManager.ResourceRepository.Update(resource)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
}
SuccessResponse(c, gin.H{"message": "资源更新成功"})
}
// DeleteResource 删除资源
func DeleteResource(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.ResourceRepository.Delete(uint(id))
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{"message": "资源删除成功"})
}
// SearchResources 搜索资源
func SearchResources(c *gin.Context) {
query := c.Query("q")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var resources []entity.Resource
var total int64
var err error
if query == "" {
// 搜索关键词为空时,返回最新记录(分页)
resources, total, err = repoManager.ResourceRepository.FindWithRelationsPaginated(page, pageSize)
} else {
// 有搜索关键词时,执行搜索
resources, total, err = repoManager.ResourceRepository.Search(query, nil, page, pageSize)
}
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{
"resources": converter.ToResourceResponseList(resources),
"total": total,
"page": page,
"page_size": pageSize,
})
}
// 增加资源浏览次数
func IncrementResourceViewCount(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
ErrorResponse(c, "无效的资源ID", http.StatusBadRequest)
return
}
// 增加资源访问量
err = repoManager.ResourceRepository.IncrementViewCount(uint(id))
if err != nil {
ErrorResponse(c, "增加浏览次数失败", http.StatusInternalServerError)
return
}
// 记录访问记录
ipAddress := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
err = repoManager.ResourceViewRepository.RecordView(uint(id), ipAddress, userAgent)
if err != nil {
// 记录访问失败不影响主要功能,只记录日志
utils.Error("记录资源访问失败: %v", err)
}
SuccessResponse(c, gin.H{"message": "浏览次数+1"})
}
// BatchDeleteResources 批量删除资源
func BatchDeleteResources(c *gin.Context) {
var req struct {
IDs []uint `json:"ids"`
}
if err := c.ShouldBindJSON(&req); err != nil || len(req.IDs) == 0 {
ErrorResponse(c, "参数错误", 400)
return
}
count := 0
for _, id := range req.IDs {
if err := repoManager.ResourceRepository.Delete(id); err == nil {
count++
}
}
SuccessResponse(c, gin.H{"deleted": count, "message": "批量删除成功"})
}
// GetResourceLink 获取资源链接(智能转存)
func GetResourceLink(c *gin.Context) {
// 获取资源ID
resourceIDStr := c.Param("id")
resourceID, err := strconv.ParseUint(resourceIDStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的资源ID", http.StatusBadRequest)
return
}
utils.Info("获取资源链接请求 - resourceID: %d", resourceID)
// 查询资源信息
resource, err := repoManager.ResourceRepository.FindByID(uint(resourceID))
if err != nil {
utils.Error("查询资源失败: %v", err)
ErrorResponse(c, "资源不存在", http.StatusNotFound)
return
}
// 查询平台信息
var panInfo entity.Pan
if resource.PanID != nil {
panPtr, err := repoManager.PanRepository.FindByID(*resource.PanID)
if err != nil {
utils.Error("查询平台信息失败: %v", err)
} else if panPtr != nil {
panInfo = *panPtr
}
}
utils.Info("资源信息 - 平台: %s, 原始链接: %s, 转存链接: %s", panInfo.Name, resource.URL, resource.SaveURL)
// 统计访问次数
err = repoManager.ResourceRepository.IncrementViewCount(uint(resourceID))
if err != nil {
utils.Error("增加资源访问量失败: %v", err)
}
// 记录访问记录
ipAddress := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
err = repoManager.ResourceViewRepository.RecordView(uint(resourceID), ipAddress, userAgent)
if err != nil {
utils.Error("记录资源访问失败: %v", err)
}
// 如果不是夸克网盘,直接返回原链接
if panInfo.Name != "quark" && panInfo.Name != "xunlei" {
utils.Info("非夸克和迅雷资源,直接返回原链接")
SuccessResponse(c, gin.H{
"url": resource.URL,
"type": "original",
"platform": panInfo.Remark,
"resource_id": resource.ID,
})
return
}
// 如果已存在转存链接,直接返回
if resource.SaveURL != "" {
utils.Info("已存在转存链接,直接返回: %s", resource.SaveURL)
SuccessResponse(c, gin.H{
"url": resource.SaveURL,
"type": "transferred",
"platform": panInfo.Remark,
"resource_id": resource.ID,
})
return
}
// 检查是否开启自动转存
autoTransferEnabled, err := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoTransferEnabled)
if err != nil {
utils.Error("获取自动转存配置失败: %v", err)
// 配置获取失败,返回原链接
SuccessResponse(c, gin.H{
"url": resource.URL,
"type": "original",
"platform": panInfo.Remark,
"resource_id": resource.ID,
"message": "",
})
return
}
if !autoTransferEnabled {
utils.Info("自动转存功能未开启,返回原链接")
SuccessResponse(c, gin.H{
"url": resource.URL,
"type": "original",
"platform": panInfo.Remark,
"resource_id": resource.ID,
"message": "",
})
return
}
// 执行自动转存
utils.Info("开始执行自动转存")
transferResult := performAutoTransfer(resource)
if transferResult.Success {
utils.Info("自动转存成功,返回转存链接: %s", transferResult.SaveURL)
SuccessResponse(c, gin.H{
"url": transferResult.SaveURL,
"type": "transferred",
"platform": panInfo.Remark,
"resource_id": resource.ID,
"message": "资源易和谐,请及时用手机夸克扫码转存",
})
} else {
utils.Error("自动转存失败: %s", transferResult.ErrorMsg)
SuccessResponse(c, gin.H{
"url": resource.URL,
"type": "original",
"platform": panInfo.Remark,
"resource_id": resource.ID,
"message": "",
})
}
}
// TransferResult 转存结果
type TransferResult struct {
Success bool `json:"success"`
SaveURL string `json:"save_url"`
ErrorMsg string `json:"error_msg"`
}
// performAutoTransfer 执行自动转存
func performAutoTransfer(resource *entity.Resource) TransferResult {
utils.Info("开始执行资源转存 - ID: %d, URL: %s", resource.ID, resource.URL)
// 平台ID
panID := resource.PanID
// 获取可用的夸克账号
accounts, err := repoManager.CksRepository.FindByPanID(*panID)
if err != nil {
utils.Error("获取网盘账号失败: %v", err)
return TransferResult{
Success: false,
ErrorMsg: fmt.Sprintf("获取网盘账号失败: %v", err),
}
}
// 测试阶段,移除最小限制
// 获取最小存储空间配置
// autoTransferMinSpace, err := repoManager.SystemConfigRepository.GetConfigInt(entity.ConfigKeyAutoTransferMinSpace)
// if err != nil {
// utils.Error("获取最小存储空间配置失败: %v", err)
// autoTransferMinSpace = 5 // 默认5GB
// }
// // 过滤:只保留已激活、夸克平台、剩余空间足够的账号
// minSpaceBytes := int64(autoTransferMinSpace) * 1024 * 1024 * 1024
// var validAccounts []entity.Cks
// for _, acc := range accounts {
// if acc.IsValid && acc.PanID == *panID && acc.LeftSpace >= minSpaceBytes {
// validAccounts = append(validAccounts, acc)
// }
// }
// if len(validAccounts) == 0 {
// utils.Info("没有可用的网盘账号")
// return TransferResult{
// Success: false,
// ErrorMsg: "没有可用的网盘账号",
// }
// }
// utils.Info("找到 %d 个可用网盘账号,开始转存处理...", len(validAccounts))
// 使用第一个可用账号进行转存
// account := validAccounts[0]
account := accounts[0]
// 创建网盘服务工厂
factory := pan.NewPanFactory()
// 执行转存
result := transferSingleResource(resource, account, factory)
if result.Success {
// 更新资源的转存信息
resource.SaveURL = result.SaveURL
resource.ErrorMsg = ""
if err := repoManager.ResourceRepository.Update(resource); err != nil {
utils.Error("更新资源转存信息失败: %v", err)
}
} else {
// 更新错误信息
resource.ErrorMsg = result.ErrorMsg
if err := repoManager.ResourceRepository.Update(resource); err != nil {
utils.Error("更新资源错误信息失败: %v", err)
}
}
return result
}
// transferSingleResource 转存单个资源
func transferSingleResource(resource *entity.Resource, account entity.Cks, factory *pan.PanFactory) TransferResult {
utils.Info("开始转存资源 - 资源ID: %d, 账号: %s", resource.ID, account.Username)
service, err := factory.CreatePanService(resource.URL, &pan.PanConfig{
URL: resource.URL,
ExpiredType: 0,
IsType: 0,
Cookie: account.Ck,
})
if err != nil {
utils.Error("创建网盘服务失败: %v", err)
return TransferResult{
Success: false,
ErrorMsg: fmt.Sprintf("创建网盘服务失败: %v", err),
}
}
// 设置账号信息
service.SetCKSRepository(repoManager.CksRepository, account)
// 提取分享ID
shareID, _ := commonutils.ExtractShareIdString(resource.URL)
if shareID == "" {
return TransferResult{
Success: false,
ErrorMsg: "无效的分享链接",
}
}
// 执行转存
transferResult, err := service.Transfer(shareID) // 有些链接还需要其他信息从 url 中自行解析
if err != nil {
utils.Error("转存失败: %v", err)
return TransferResult{
Success: false,
ErrorMsg: fmt.Sprintf("转存失败: %v", err),
}
}
if transferResult == nil || !transferResult.Success {
errMsg := "转存失败"
if transferResult != nil && transferResult.Message != "" {
errMsg = transferResult.Message
}
utils.Error("转存失败: %s", errMsg)
return TransferResult{
Success: false,
ErrorMsg: errMsg,
}
}
// 提取转存链接
var saveURL string
if data, ok := transferResult.Data.(map[string]interface{}); ok {
if v, ok := data["shareUrl"]; ok {
saveURL, _ = v.(string)
}
}
if saveURL == "" {
saveURL = transferResult.ShareURL
}
if saveURL == "" {
return TransferResult{
Success: false,
ErrorMsg: "转存成功但未获取到分享链接",
}
}
utils.Info("转存成功 - 资源ID: %d, 转存链接: %s", resource.ID, saveURL)
return TransferResult{
Success: true,
SaveURL: saveURL,
}
}
// getQuarkPanID 获取夸克网盘ID
func getQuarkPanID() (uint, error) {
// 通过FindAll方法查找所有平台然后过滤出quark平台
pans, err := repoManager.PanRepository.FindAll()
if err != nil {
return 0, fmt.Errorf("查询平台信息失败: %v", err)
}
for _, p := range pans {
if p.Name == "quark" {
return p.ID, nil
}
}
return 0, fmt.Errorf("未找到quark平台")
}