update: 优化获取链接的方式

This commit is contained in:
ctwj
2025-08-09 09:51:55 +08:00
parent 32e7240287
commit 81eb99691d
6 changed files with 418 additions and 28 deletions

View File

@@ -1,9 +1,12 @@
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"
@@ -16,7 +19,7 @@ import (
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", page, pageSize)
params := map[string]interface{}{
@@ -292,14 +295,14 @@ func IncrementResourceViewCount(c *gin.Context) {
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")
@@ -308,7 +311,7 @@ func IncrementResourceViewCount(c *gin.Context) {
// 记录访问失败不影响主要功能,只记录日志
utils.Error("记录资源访问失败: %v", err)
}
SuccessResponse(c, gin.H{"message": "浏览次数+1"})
}
@@ -329,3 +332,305 @@ func BatchDeleteResources(c *gin.Context) {
}
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" {
utils.Info("非夸克资源,直接返回原链接")
SuccessResponse(c, gin.H{
"url": resource.URL,
"type": "original",
"platform": panInfo.Remark,
"resource_id": resource.ID,
})
return
}
// 夸克资源处理逻辑
utils.Info("夸克资源处理开始")
// 如果已存在转存链接,直接返回
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
quarkPanID, err := getQuarkPanID()
if err != nil {
utils.Error("获取夸克平台ID失败: %v", err)
return TransferResult{
Success: false,
ErrorMsg: fmt.Sprintf("获取夸克平台ID失败: %v", err),
}
}
// 获取可用的夸克账号
accounts, err := repoManager.CksRepository.FindAll()
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 == quarkPanID && 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]
// 创建网盘服务工厂
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),
}
}
// 提取分享ID
shareID, _ := commonutils.ExtractShareIdString(resource.URL)
if shareID == "" {
return TransferResult{
Success: false,
ErrorMsg: "无效的分享链接",
}
}
// 执行转存
transferResult, err := service.Transfer(shareID)
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平台")
}

View File

@@ -168,6 +168,7 @@ func main() {
api.GET("/resources/:id", handlers.GetResourceByID)
api.GET("/resources/check-exists", handlers.CheckResourceExists)
api.POST("/resources/:id/view", handlers.IncrementResourceViewCount)
api.GET("/resources/:id/link", handlers.GetResourceLink)
api.DELETE("/resources/batch", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchDeleteResources)
// 分类管理

View File

@@ -14,8 +14,22 @@
</div>
<div class="text-center">
<!-- 移动端所有链接都显示链接文本和操作按钮 -->
<div v-if="isMobile" class="space-y-4">
<!-- 加载状态 -->
<div v-if="loading" class="space-y-4">
<div class="flex flex-col items-center justify-center py-8">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4"></div>
<p class="text-sm text-gray-600 dark:text-gray-400">正在获取链接...</p>
</div>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="space-y-4">
<div class="bg-red-50 dark:bg-red-900/20 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-triangle text-red-500 mr-2"></i>
<p class="text-sm text-red-700 dark:text-red-300">{{ error }}</p>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<p class="text-sm text-gray-700 dark:text-gray-300 break-all">{{ url }}</p>
</div>
@@ -35,8 +49,47 @@
</div>
</div>
<!-- 正常显示 -->
<div v-else>
<!-- 移动端所有链接都显示链接文本和操作按钮 -->
<div v-if="isMobile" class="space-y-4">
<!-- 显示链接状态信息 -->
<div v-if="message" class="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-info-circle text-blue-500 mr-2"></i>
<p class="text-sm text-blue-700 dark:text-blue-300">{{ message }}</p>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<p class="text-sm text-gray-700 dark:text-gray-300 break-all">{{ url }}</p>
</div>
<div class="flex gap-2">
<button
@click="openLink"
class="flex-1 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors text-sm flex items-center justify-center gap-2"
>
<i class="fas fa-external-link-alt"></i> 跳转
</button>
<button
@click="copyUrl"
class="flex-1 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors text-sm flex items-center justify-center gap-2"
>
<i class="fas fa-copy"></i> 复制
</button>
</div>
</div>
<!-- PC端根据链接类型显示不同内容 -->
<div v-else class="space-y-4">
<!-- 显示链接状态信息 -->
<div v-if="message" class="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-info-circle text-blue-500 mr-2"></i>
<p class="text-sm text-blue-700 dark:text-blue-300">{{ message }}</p>
</div>
</div>
<!-- 夸克链接只显示二维码 -->
<div v-if="isQuarkLink" class="space-y-4">
<div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg">
@@ -86,6 +139,7 @@
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -98,6 +152,11 @@ interface Props {
visible: boolean
save_url?: string
url?: string
loading?: boolean
linkType?: string
platform?: string
message?: string
error?: string
}
interface Emits {

View File

@@ -66,11 +66,13 @@ export const useResourceApi = () => {
const deleteResource = (id: number) => useApiFetch(`/resources/${id}`, { method: 'DELETE' }).then(parseApiResponse)
const searchResources = (params: any) => useApiFetch('/search', { params }).then(parseApiResponse)
const getResourcesByPan = (panId: number, params?: any) => useApiFetch('/resources', { params: { ...params, pan_id: panId } }).then(parseApiResponse)
// 新增:统一的资源访问次数上报
// 新增:统一的资源访问次数上报注意getResourceLink 已包含访问统计,通常不需要单独调用此方法)
const incrementViewCount = (id: number) => useApiFetch(`/resources/${id}/view`, { method: 'POST' })
// 新增:批量删除资源
const batchDeleteResources = (ids: number[]) => useApiFetch('/resources/batch', { method: 'DELETE', body: { ids } }).then(parseApiResponse)
return { getResources, getResource, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources }
// 新增:获取资源链接(智能转存)
const getResourceLink = (id: number) => useApiFetch(`/resources/${id}/link`).then(parseApiResponse)
return { getResources, getResource, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources, getResourceLink }
}
export const useAuthApi = () => {

View File

@@ -169,27 +169,30 @@ const activeTab = ref('resource')
// 配置表单数据
const configForm = ref({
auto_process_enabled: false,
auto_process_interval: 30,
auto_process_interval: '30',
auto_transfer_enabled: false,
auto_transfer_min_space: 500,
auto_transfer_min_space: '500',
ad_keywords: '',
auto_insert_ad: '',
hot_drama_auto_fetch: false
})
// 表单验证规则
const rules = {} as any
// 获取系统配置
const fetchConfig = async () => {
try {
const { useSystemConfigApi } = await import('~/composables/useApi')
const systemConfigApi = useSystemConfigApi()
const response = await systemConfigApi.getSystemConfig()
const response = await systemConfigApi.getSystemConfig() as any
if (response) {
configForm.value = {
auto_process_enabled: response.auto_process_ready_resources || false,
auto_process_interval: response.auto_process_interval || 30,
auto_process_interval: String(response.auto_process_interval || 30),
auto_transfer_enabled: response.auto_transfer_enabled || false,
auto_transfer_min_space: response.auto_transfer_min_space || 500,
auto_transfer_min_space: String(response.auto_transfer_min_space || 500),
ad_keywords: response.ad_keywords || '',
auto_insert_ad: response.auto_insert_ad || '',
hot_drama_auto_fetch: response.auto_fetch_hot_drama_enabled || false
@@ -214,9 +217,9 @@ const saveConfig = async () => {
await systemConfigApi.updateSystemConfig({
auto_process_ready_resources: configForm.value.auto_process_enabled,
auto_process_interval: configForm.value.auto_process_interval,
auto_process_interval: parseInt(configForm.value.auto_process_interval) || 30,
auto_transfer_enabled: configForm.value.auto_transfer_enabled,
auto_transfer_min_space: configForm.value.auto_transfer_min_space,
auto_transfer_min_space: parseInt(configForm.value.auto_transfer_min_space) || 500,
ad_keywords: configForm.value.ad_keywords,
auto_insert_ad: configForm.value.auto_insert_ad,
auto_fetch_hot_drama_enabled: configForm.value.hot_drama_auto_fetch

View File

@@ -182,6 +182,11 @@
:visible="showLinkModal"
:url="selectedResource?.url"
:save_url="selectedResource?.save_url"
:loading="selectedResource?.loading"
:linkType="selectedResource?.linkType"
:platform="selectedResource?.platform"
:message="selectedResource?.message"
:error="selectedResource?.error"
@close="showLinkModal = false"
/>
@@ -328,23 +333,38 @@ const getPlatformIcon = (panId: string) => {
return platform?.icon || '未知平台'
}
// 跳转到链接
const openLink = async (url: string, resourceId: number) => {
try {
await fetch(`/api/resources/${resourceId}/view`, { method: 'POST' })
} catch (e) {}
if (process.client) {
window.open(url, '_blank')
}
}
// 注意:链接访问统计已整合到 getResourceLink API 中
// 切换链接显示
const toggleLink = async (resource: any) => {
try {
await resourceApi.incrementViewCount(resource.id)
} catch (e) {}
selectedResource.value = resource
// 显示加载状态
selectedResource.value = { ...resource, loading: true }
showLinkModal.value = true
try {
// 调用新的获取链接API同时统计访问次数
const linkData = await resourceApi.getResourceLink(resource.id) as any
console.log('获取到的链接数据:', linkData)
// 更新资源信息,包含新的链接信息
selectedResource.value = {
...resource,
url: linkData.url,
save_url: linkData.type === 'transferred' ? linkData.url : resource.save_url,
loading: false,
linkType: linkData.type,
platform: linkData.platform,
message: linkData.message
}
} catch (error) {
console.error('获取资源链接失败:', error)
// 出错时使用原始资源信息
selectedResource.value = {
...resource,
loading: false,
error: '获取链接失败,显示原始链接'
}
}
}
// 复制到剪贴板