mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
803 lines
22 KiB
Go
803 lines
22 KiB
Go
package utils
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
panutils "github.com/ctwj/urldb/common"
|
||
commonutils "github.com/ctwj/urldb/common/utils"
|
||
"github.com/ctwj/urldb/db/entity"
|
||
"github.com/ctwj/urldb/db/repo"
|
||
)
|
||
|
||
// Scheduler 定时任务管理器
|
||
type Scheduler struct {
|
||
doubanService *DoubanService
|
||
hotDramaRepo repo.HotDramaRepository
|
||
readyResourceRepo repo.ReadyResourceRepository
|
||
resourceRepo repo.ResourceRepository
|
||
systemConfigRepo repo.SystemConfigRepository
|
||
panRepo repo.PanRepository
|
||
cksRepo repo.CksRepository
|
||
stopChan chan bool
|
||
isRunning bool
|
||
readyResourceRunning bool
|
||
autoTransferRunning bool
|
||
processingMutex sync.Mutex // 防止ready_resource任务重叠执行
|
||
hotDramaMutex sync.Mutex // 防止热播剧任务重叠执行
|
||
autoTransferMutex sync.Mutex // 防止自动转存任务重叠执行
|
||
|
||
// 平台映射缓存
|
||
panCache map[string]*uint // serviceType -> panID
|
||
panCacheOnce sync.Once
|
||
}
|
||
|
||
// NewScheduler 创建新的定时任务管理器
|
||
func NewScheduler(hotDramaRepo repo.HotDramaRepository, readyResourceRepo repo.ReadyResourceRepository, resourceRepo repo.ResourceRepository, systemConfigRepo repo.SystemConfigRepository, panRepo repo.PanRepository, cksRepo repo.CksRepository) *Scheduler {
|
||
return &Scheduler{
|
||
doubanService: NewDoubanService(),
|
||
hotDramaRepo: hotDramaRepo,
|
||
readyResourceRepo: readyResourceRepo,
|
||
resourceRepo: resourceRepo,
|
||
systemConfigRepo: systemConfigRepo,
|
||
panRepo: panRepo,
|
||
cksRepo: cksRepo,
|
||
stopChan: make(chan bool),
|
||
isRunning: false,
|
||
readyResourceRunning: false,
|
||
autoTransferRunning: false,
|
||
processingMutex: sync.Mutex{},
|
||
hotDramaMutex: sync.Mutex{},
|
||
autoTransferMutex: sync.Mutex{},
|
||
panCache: make(map[string]*uint),
|
||
}
|
||
}
|
||
|
||
// StartHotDramaScheduler 启动热播剧定时任务
|
||
func (s *Scheduler) StartHotDramaScheduler() {
|
||
if s.isRunning {
|
||
Info("热播剧定时任务已在运行中")
|
||
return
|
||
}
|
||
|
||
s.isRunning = true
|
||
Info("启动热播剧定时任务")
|
||
|
||
go func() {
|
||
ticker := time.NewTicker(12 * time.Hour) // 每12小时执行一次
|
||
defer ticker.Stop()
|
||
|
||
// 立即执行一次
|
||
s.fetchHotDramaData()
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
// 使用TryLock防止任务重叠执行
|
||
if s.hotDramaMutex.TryLock() {
|
||
go func() {
|
||
defer s.hotDramaMutex.Unlock()
|
||
s.fetchHotDramaData()
|
||
}()
|
||
} else {
|
||
Info("上一次热播剧任务还在执行中,跳过本次执行")
|
||
}
|
||
case <-s.stopChan:
|
||
Info("停止热播剧定时任务")
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
// StopHotDramaScheduler 停止热播剧定时任务
|
||
func (s *Scheduler) StopHotDramaScheduler() {
|
||
if !s.isRunning {
|
||
Info("热播剧定时任务未在运行")
|
||
return
|
||
}
|
||
|
||
s.stopChan <- true
|
||
s.isRunning = false
|
||
Info("已发送停止信号给热播剧定时任务")
|
||
}
|
||
|
||
// fetchHotDramaData 获取热播剧数据
|
||
func (s *Scheduler) fetchHotDramaData() {
|
||
Info("开始获取热播剧数据...")
|
||
|
||
// 直接处理电影和电视剧数据,不再需要FetchHotDramaNames
|
||
s.processHotDramaNames([]string{})
|
||
}
|
||
|
||
// processHotDramaNames 处理热播剧名字
|
||
func (s *Scheduler) processHotDramaNames(dramaNames []string) {
|
||
Info("开始处理热播剧数据,共 %d 个", len(dramaNames))
|
||
|
||
// 收集所有数据
|
||
var allDramas []*entity.HotDrama
|
||
|
||
// 获取电影数据
|
||
movieDramas := s.processMovieData()
|
||
allDramas = append(allDramas, movieDramas...)
|
||
|
||
// 获取电视剧数据
|
||
tvDramas := s.processTvData()
|
||
allDramas = append(allDramas, tvDramas...)
|
||
|
||
// 清空数据库
|
||
Info("准备清空数据库,当前共有 %d 条数据", len(allDramas))
|
||
if err := s.hotDramaRepo.DeleteAll(); err != nil {
|
||
Error("清空数据库失败: %v", err)
|
||
return
|
||
}
|
||
Info("数据库清空完成")
|
||
|
||
// 批量插入所有数据
|
||
if len(allDramas) > 0 {
|
||
Info("开始批量插入 %d 条数据", len(allDramas))
|
||
if err := s.hotDramaRepo.BatchCreate(allDramas); err != nil {
|
||
Error("批量插入数据失败: %v", err)
|
||
} else {
|
||
Info("成功批量插入 %d 条数据", len(allDramas))
|
||
}
|
||
} else {
|
||
Info("没有数据需要插入")
|
||
}
|
||
|
||
Info("热播剧数据处理完成")
|
||
}
|
||
|
||
// processMovieData 处理电影数据
|
||
func (s *Scheduler) processMovieData() []*entity.HotDrama {
|
||
Info("开始处理电影数据...")
|
||
|
||
var movieDramas []*entity.HotDrama
|
||
|
||
// 使用GetTypePage方法获取电影数据
|
||
movieResult, err := s.doubanService.GetTypePage("热门", "全部")
|
||
if err != nil {
|
||
Error("获取电影榜单失败: %v", err)
|
||
return movieDramas
|
||
}
|
||
|
||
if movieResult.Success && movieResult.Data != nil {
|
||
Info("电影获取到 %d 个数据", len(movieResult.Data.Items))
|
||
|
||
for _, item := range movieResult.Data.Items {
|
||
drama := &entity.HotDrama{
|
||
Title: item.Title,
|
||
CardSubtitle: item.CardSubtitle,
|
||
EpisodesInfo: item.EpisodesInfo,
|
||
IsNew: item.IsNew,
|
||
Rating: item.Rating.Value,
|
||
RatingCount: item.Rating.Count,
|
||
Year: item.Year,
|
||
Region: item.Region,
|
||
Genres: strings.Join(item.Genres, ", "),
|
||
Directors: strings.Join(item.Directors, ", "),
|
||
Actors: strings.Join(item.Actors, ", "),
|
||
PosterURL: item.Pic.Normal,
|
||
Category: "电影",
|
||
SubType: "热门",
|
||
Source: "douban",
|
||
DoubanID: item.ID,
|
||
DoubanURI: item.URI,
|
||
}
|
||
|
||
movieDramas = append(movieDramas, drama)
|
||
Info("收集电影: %s (评分: %.1f, 年份: %s, 地区: %s)",
|
||
item.Title, item.Rating.Value, item.Year, item.Region)
|
||
}
|
||
} else {
|
||
Warn("电影获取数据失败或为空")
|
||
}
|
||
|
||
Info("电影数据处理完成,共收集 %d 条数据", len(movieDramas))
|
||
return movieDramas
|
||
}
|
||
|
||
// processTvData 处理电视剧数据
|
||
func (s *Scheduler) processTvData() []*entity.HotDrama {
|
||
Info("开始处理电视剧数据...")
|
||
|
||
var tvDramas []*entity.HotDrama
|
||
|
||
// 获取所有tv类型
|
||
tvTypes := s.doubanService.GetAllTvTypes()
|
||
Info("获取到 %d 个tv类型: %v", len(tvTypes), tvTypes)
|
||
|
||
// 遍历每个type,分别请求数据
|
||
for _, tvType := range tvTypes {
|
||
Info("正在处理tv类型: %s", tvType)
|
||
|
||
// 使用GetTypePage方法请求数据
|
||
tvResult, err := s.doubanService.GetTypePage("tv", tvType)
|
||
if err != nil {
|
||
Error("获取tv类型 %s 数据失败: %v", tvType, err)
|
||
continue
|
||
}
|
||
|
||
if tvResult.Success && tvResult.Data != nil {
|
||
Info("tv类型 %s 获取到 %d 个数据", tvType, len(tvResult.Data.Items))
|
||
|
||
for _, item := range tvResult.Data.Items {
|
||
drama := &entity.HotDrama{
|
||
Title: item.Title,
|
||
CardSubtitle: item.CardSubtitle,
|
||
EpisodesInfo: item.EpisodesInfo,
|
||
IsNew: item.IsNew,
|
||
Rating: item.Rating.Value,
|
||
RatingCount: item.Rating.Count,
|
||
Year: item.Year,
|
||
Region: item.Region,
|
||
Genres: strings.Join(item.Genres, ", "),
|
||
Directors: strings.Join(item.Directors, ", "),
|
||
Actors: strings.Join(item.Actors, ", "),
|
||
PosterURL: item.Pic.Normal,
|
||
Category: "电视剧",
|
||
SubType: tvType, // 使用具体的tv类型
|
||
Source: "douban",
|
||
DoubanID: item.ID,
|
||
DoubanURI: item.URI,
|
||
}
|
||
|
||
tvDramas = append(tvDramas, drama)
|
||
Info("收集tv类型 %s: %s (评分: %.1f, 年份: %s, 地区: %s)",
|
||
tvType, item.Title, item.Rating.Value, item.Year, item.Region)
|
||
}
|
||
} else {
|
||
Warn("tv类型 %s 获取数据失败或为空", tvType)
|
||
}
|
||
|
||
// 每个type请求间隔1秒,避免请求过于频繁
|
||
time.Sleep(1 * time.Second)
|
||
}
|
||
|
||
Info("电视剧数据处理完成,共收集 %d 条数据", len(tvDramas))
|
||
return tvDramas
|
||
}
|
||
|
||
// IsRunning 检查定时任务是否在运行
|
||
func (s *Scheduler) IsRunning() bool {
|
||
return s.isRunning
|
||
}
|
||
|
||
// GetHotDramaNames 手动获取热播剧名字(用于测试或手动调用)
|
||
func (s *Scheduler) GetHotDramaNames() ([]string, error) {
|
||
// 由于删除了FetchHotDramaNames方法,返回空数组
|
||
return []string{}, nil
|
||
}
|
||
|
||
// StartReadyResourceScheduler 启动待处理资源自动处理任务
|
||
func (s *Scheduler) StartReadyResourceScheduler() {
|
||
if s.readyResourceRunning {
|
||
Info("待处理资源自动处理任务已在运行中")
|
||
return
|
||
}
|
||
|
||
s.readyResourceRunning = true
|
||
Info("启动待处理资源自动处理任务")
|
||
|
||
go func() {
|
||
// 获取系统配置中的间隔时间
|
||
config, err := s.systemConfigRepo.GetOrCreateDefault()
|
||
interval := 5 * time.Minute // 默认5分钟
|
||
if err == nil && config.AutoProcessInterval > 0 {
|
||
interval = time.Duration(config.AutoProcessInterval) * time.Minute
|
||
}
|
||
|
||
ticker := time.NewTicker(interval)
|
||
defer ticker.Stop()
|
||
|
||
Info("待处理资源自动处理任务已启动,间隔时间: %v", interval)
|
||
|
||
// 立即执行一次
|
||
s.processReadyResources()
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
// 使用TryLock防止任务重叠执行
|
||
if s.processingMutex.TryLock() {
|
||
go func() {
|
||
defer s.processingMutex.Unlock()
|
||
s.processReadyResources()
|
||
}()
|
||
} else {
|
||
Info("上一次待处理资源任务还在执行中,跳过本次执行")
|
||
}
|
||
case <-s.stopChan:
|
||
Info("停止待处理资源自动处理任务")
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
// StopReadyResourceScheduler 停止待处理资源自动处理任务
|
||
func (s *Scheduler) StopReadyResourceScheduler() {
|
||
if !s.readyResourceRunning {
|
||
Info("待处理资源自动处理任务未在运行")
|
||
return
|
||
}
|
||
|
||
s.stopChan <- true
|
||
s.readyResourceRunning = false
|
||
Info("已发送停止信号给待处理资源自动处理任务")
|
||
}
|
||
|
||
// processReadyResources 处理待处理资源
|
||
func (s *Scheduler) processReadyResources() {
|
||
Info("开始处理待处理资源...")
|
||
|
||
// 检查系统配置,确认是否启用自动处理
|
||
config, err := s.systemConfigRepo.GetOrCreateDefault()
|
||
if err != nil {
|
||
Error("获取系统配置失败: %v", err)
|
||
return
|
||
}
|
||
|
||
if !config.AutoProcessReadyResources {
|
||
Info("自动处理待处理资源功能已禁用")
|
||
return
|
||
}
|
||
|
||
// 获取所有待处理资源
|
||
readyResources, err := s.readyResourceRepo.FindAll()
|
||
if err != nil {
|
||
Error("获取待处理资源失败: %v", err)
|
||
return
|
||
}
|
||
|
||
if len(readyResources) == 0 {
|
||
Info("没有待处理的资源")
|
||
return
|
||
}
|
||
|
||
Info("找到 %d 个待处理资源,开始处理...", len(readyResources))
|
||
|
||
processedCount := 0
|
||
factory := panutils.GetInstance() // 使用单例模式
|
||
for _, readyResource := range readyResources {
|
||
|
||
//readyResource.URL 是 查重
|
||
exits, err := s.resourceRepo.FindExists(readyResource.URL)
|
||
if err != nil {
|
||
Error("查重失败: %v", err)
|
||
continue
|
||
}
|
||
if exits {
|
||
Info("资源已存在: %s", readyResource.URL)
|
||
s.readyResourceRepo.Delete(readyResource.ID)
|
||
continue
|
||
}
|
||
|
||
if err := s.convertReadyResourceToResource(readyResource, factory); err != nil {
|
||
Error("处理资源失败 (ID: %d): %v", readyResource.ID, err)
|
||
}
|
||
s.readyResourceRepo.Delete(readyResource.ID)
|
||
processedCount++
|
||
Info("成功处理资源: %s", readyResource.URL)
|
||
}
|
||
|
||
Info("待处理资源处理完成,共处理 %d 个资源", processedCount)
|
||
}
|
||
|
||
// convertReadyResourceToResource 将待处理资源转换为正式资源
|
||
func (s *Scheduler) convertReadyResourceToResource(readyResource entity.ReadyResource, factory *panutils.PanFactory) error {
|
||
Debug("开始处理资源: %s", readyResource.URL)
|
||
|
||
// 提取分享ID和服务类型
|
||
shareID, serviceType := panutils.ExtractShareId(readyResource.URL)
|
||
if serviceType == panutils.NotFound {
|
||
Warn("不支持的链接地址: %s", readyResource.URL)
|
||
return nil
|
||
}
|
||
|
||
Debug("检测到服务类型: %s, 分享ID: %s", serviceType.String(), shareID)
|
||
|
||
// 不是夸克,直接保存,
|
||
if serviceType != panutils.Quark {
|
||
// 检测是否有效
|
||
checkResult, _ := commonutils.CheckURL(readyResource.URL)
|
||
if !checkResult.Status {
|
||
Warn("链接无效: %s", readyResource.URL)
|
||
return nil
|
||
}
|
||
|
||
// 入库
|
||
}
|
||
|
||
// 准备配置
|
||
config := &panutils.PanConfig{
|
||
URL: readyResource.URL,
|
||
Code: "", // 可以从readyResource中获取
|
||
IsType: 1, // 转存并分享后的资源信息 0 转存后分享, 1 只获取基本信息
|
||
ExpiredType: 1, // 永久分享
|
||
AdFid: "",
|
||
Stoken: "",
|
||
}
|
||
|
||
// 通过工厂获取对应的网盘服务单例
|
||
panService, err := factory.CreatePanService(readyResource.URL, config)
|
||
if err != nil {
|
||
Error("获取网盘服务失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
// 阿里云盘特殊处理:检查URL有效性
|
||
// if serviceType == panutils.Alipan {
|
||
// checkResult, _ := CheckURL(readyResource.URL)
|
||
// if !checkResult.Status {
|
||
// log.Printf("阿里云盘链接无效: %s", readyResource.URL)
|
||
// return nil
|
||
// }
|
||
|
||
// // 如果有标题,直接创建资源
|
||
// if readyResource.Title != nil && *readyResource.Title != "" {
|
||
// resource := &entity.Resource{
|
||
// Title: *readyResource.Title,
|
||
// Description: readyResource.Description,
|
||
// URL: readyResource.URL,
|
||
// PanID: s.determinePanID(readyResource.URL),
|
||
// IsValid: true,
|
||
// IsPublic: true,
|
||
// }
|
||
|
||
// // 如果有分类信息,尝试查找或创建分类
|
||
// if readyResource.Category != "" {
|
||
// categoryID, err := s.getOrCreateCategory(readyResource.Category)
|
||
// if err == nil {
|
||
// resource.CategoryID = &categoryID
|
||
// }
|
||
// }
|
||
|
||
// return s.resourceRepo.Create(resource)
|
||
// }
|
||
// }
|
||
|
||
// 统一处理:尝试转存获取标题
|
||
result, err := panService.Transfer(shareID)
|
||
if err != nil {
|
||
Error("网盘信息获取失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
if !result.Success {
|
||
Error("网盘信息获取失败: %s", result.Message)
|
||
return nil
|
||
}
|
||
|
||
// 提取转存结果
|
||
if resultData, ok := result.Data.(map[string]interface{}); ok {
|
||
title := resultData["title"].(string)
|
||
shareURL := resultData["shareUrl"].(string)
|
||
// fid := resultData["fid"].(string) // 暂时未使用
|
||
|
||
// 创建资源记录
|
||
resource := &entity.Resource{
|
||
Title: title,
|
||
Description: readyResource.Description,
|
||
URL: shareURL,
|
||
PanID: s.getPanIDByServiceType(serviceType),
|
||
IsValid: true,
|
||
IsPublic: true,
|
||
}
|
||
|
||
// 如果有分类信息,尝试查找或创建分类
|
||
if readyResource.Category != "" {
|
||
categoryID, err := s.getOrCreateCategory(readyResource.Category)
|
||
if err == nil {
|
||
resource.CategoryID = &categoryID
|
||
}
|
||
}
|
||
|
||
return s.resourceRepo.Create(resource)
|
||
}
|
||
|
||
Error("转存结果格式异常")
|
||
return nil
|
||
}
|
||
|
||
// getOrCreateCategory 获取或创建分类
|
||
func (s *Scheduler) getOrCreateCategory(categoryName string) (uint, error) {
|
||
// 这里需要实现分类的查找和创建逻辑
|
||
// 由于没有CategoryRepository的注入,这里先返回0
|
||
// 你可以根据需要添加CategoryRepository的依赖
|
||
return 0, nil
|
||
}
|
||
|
||
// initPanCache 初始化平台映射缓存
|
||
func (s *Scheduler) initPanCache() {
|
||
s.panCacheOnce.Do(func() {
|
||
// 获取所有平台数据
|
||
pans, err := s.panRepo.FindAll()
|
||
if err != nil {
|
||
Error("初始化平台缓存失败: %v", err)
|
||
return
|
||
}
|
||
|
||
// 建立 ServiceType 到 PanID 的映射
|
||
serviceTypeToPanName := map[string]string{
|
||
"quark": "quark",
|
||
"alipan": "aliyun", // 阿里云盘在数据库中的名称是 aliyun
|
||
"baidu": "baidu",
|
||
"uc": "uc",
|
||
"unknown": "other",
|
||
}
|
||
|
||
// 创建平台名称到ID的映射
|
||
panNameToID := make(map[string]*uint)
|
||
for _, pan := range pans {
|
||
panID := pan.ID
|
||
panNameToID[pan.Name] = &panID
|
||
}
|
||
|
||
// 建立 ServiceType 到 PanID 的映射
|
||
for serviceType, panName := range serviceTypeToPanName {
|
||
if panID, exists := panNameToID[panName]; exists {
|
||
s.panCache[serviceType] = panID
|
||
Debug("平台映射缓存: %s -> %s (ID: %d)", serviceType, panName, *panID)
|
||
} else {
|
||
Warn("警告: 未找到平台 %s 对应的数据库记录", panName)
|
||
}
|
||
}
|
||
|
||
// 确保有默认的 other 平台
|
||
if otherID, exists := panNameToID["other"]; exists {
|
||
s.panCache["unknown"] = otherID
|
||
}
|
||
|
||
Info("平台映射缓存初始化完成,共 %d 个映射", len(s.panCache))
|
||
})
|
||
}
|
||
|
||
// getPanIDByServiceType 根据服务类型获取平台ID
|
||
func (s *Scheduler) getPanIDByServiceType(serviceType panutils.ServiceType) *uint {
|
||
s.initPanCache()
|
||
|
||
serviceTypeStr := serviceType.String()
|
||
if panID, exists := s.panCache[serviceTypeStr]; exists {
|
||
return panID
|
||
}
|
||
|
||
// 如果找不到,返回 other 平台的ID
|
||
if otherID, exists := s.panCache["other"]; exists {
|
||
Warn("未找到服务类型 %s 的映射,使用默认平台 other", serviceTypeStr)
|
||
return otherID
|
||
}
|
||
|
||
Warn("未找到服务类型 %s 的映射,且没有默认平台,返回nil", serviceTypeStr)
|
||
return nil
|
||
}
|
||
|
||
// IsReadyResourceRunning 检查待处理资源自动处理任务是否在运行
|
||
func (s *Scheduler) IsReadyResourceRunning() bool {
|
||
return s.readyResourceRunning
|
||
}
|
||
|
||
// StartAutoTransferScheduler 启动自动转存定时任务
|
||
func (s *Scheduler) StartAutoTransferScheduler() {
|
||
if s.autoTransferRunning {
|
||
Info("自动转存定时任务已在运行中")
|
||
return
|
||
}
|
||
|
||
s.autoTransferRunning = true
|
||
Info("启动自动转存定时任务")
|
||
|
||
go func() {
|
||
// 获取系统配置中的间隔时间
|
||
config, err := s.systemConfigRepo.GetOrCreateDefault()
|
||
interval := 5 * time.Minute // 默认5分钟
|
||
if err == nil && config.AutoProcessInterval > 0 {
|
||
interval = time.Duration(config.AutoProcessInterval) * time.Minute
|
||
}
|
||
|
||
ticker := time.NewTicker(interval)
|
||
defer ticker.Stop()
|
||
|
||
Info("自动转存定时任务已启动,间隔时间: %v", interval)
|
||
|
||
// 立即执行一次
|
||
s.processAutoTransfer()
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
// 使用TryLock防止任务重叠执行
|
||
if s.autoTransferMutex.TryLock() {
|
||
go func() {
|
||
defer s.autoTransferMutex.Unlock()
|
||
s.processAutoTransfer()
|
||
}()
|
||
} else {
|
||
Info("上一次自动转存任务还在执行中,跳过本次执行")
|
||
}
|
||
case <-s.stopChan:
|
||
Info("停止自动转存定时任务")
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
// StopAutoTransferScheduler 停止自动转存定时任务
|
||
func (s *Scheduler) StopAutoTransferScheduler() {
|
||
if !s.autoTransferRunning {
|
||
Info("自动转存定时任务未在运行")
|
||
return
|
||
}
|
||
|
||
s.stopChan <- true
|
||
s.autoTransferRunning = false
|
||
Info("已发送停止信号给自动转存定时任务")
|
||
}
|
||
|
||
// IsAutoTransferRunning 检查自动转存定时任务是否在运行
|
||
func (s *Scheduler) IsAutoTransferRunning() bool {
|
||
return s.autoTransferRunning
|
||
}
|
||
|
||
// processAutoTransfer 处理自动转存
|
||
func (s *Scheduler) processAutoTransfer() {
|
||
Info("开始处理自动转存...")
|
||
|
||
// 检查系统配置,确认是否启用自动转存
|
||
config, err := s.systemConfigRepo.GetOrCreateDefault()
|
||
if err != nil {
|
||
Error("获取系统配置失败: %v", err)
|
||
return
|
||
}
|
||
|
||
if !config.AutoTransferEnabled {
|
||
Info("自动转存功能已禁用")
|
||
return
|
||
}
|
||
|
||
// 获取所有有效的网盘账号
|
||
accounts, err := s.cksRepo.FindAll()
|
||
if err != nil {
|
||
Error("获取网盘账号失败: %v", err)
|
||
return
|
||
}
|
||
|
||
if len(accounts) == 0 {
|
||
Info("没有可用的网盘账号")
|
||
return
|
||
}
|
||
|
||
Info("找到 %d 个网盘账号,开始自动转存处理...", len(accounts))
|
||
|
||
// 获取需要转存的资源
|
||
resources, err := s.getResourcesForTransfer(config)
|
||
if err != nil {
|
||
Error("获取需要转存的资源失败: %v", err)
|
||
return
|
||
}
|
||
|
||
if len(resources) == 0 {
|
||
Info("没有需要转存的资源")
|
||
return
|
||
}
|
||
|
||
Info("找到 %d 个需要转存的资源", len(resources))
|
||
|
||
// 执行自动转存
|
||
transferCount := 0
|
||
for _, resource := range resources {
|
||
if err := s.transferResource(resource, accounts, config); err != nil {
|
||
Error("转存资源失败 (ID: %d): %v", resource.ID, err)
|
||
} else {
|
||
transferCount++
|
||
Info("成功转存资源: %s", resource.Title)
|
||
}
|
||
}
|
||
|
||
Info("自动转存处理完成,共转存 %d 个资源", transferCount)
|
||
}
|
||
|
||
// getResourcesForTransfer 获取需要转存的资源
|
||
func (s *Scheduler) getResourcesForTransfer(config *entity.SystemConfig) ([]*entity.Resource, error) {
|
||
// TODO: 实现获取需要转存的资源逻辑
|
||
// 1. 获取所有有效的资源
|
||
// 2. 根据配置的转存限制天数过滤资源
|
||
// 3. 排除已经转存过的资源
|
||
// 4. 按优先级排序(可以根据浏览次数、创建时间等)
|
||
|
||
Info("获取需要转存的资源 - 限制天数: %d", config.AutoTransferLimitDays)
|
||
|
||
// 临时返回空数组,等待具体实现
|
||
return []*entity.Resource{}, nil
|
||
}
|
||
|
||
// transferResource 转存单个资源
|
||
func (s *Scheduler) transferResource(resource *entity.Resource, accounts []entity.Cks, config *entity.SystemConfig) error {
|
||
// TODO: 实现单个资源的转存逻辑
|
||
// 1. 选择合适的网盘账号(根据剩余空间、VIP状态等)
|
||
// 2. 检查账号剩余空间是否满足最小空间要求
|
||
// 3. 调用网盘API进行转存
|
||
// 4. 更新资源状态和转存记录
|
||
// 5. 更新账号使用空间
|
||
|
||
Info("开始转存资源: %s (ID: %d)", resource.Title, resource.ID)
|
||
|
||
// 选择最佳账号
|
||
selectedAccount := s.selectBestAccount(accounts, config)
|
||
if selectedAccount == nil {
|
||
return fmt.Errorf("没有合适的网盘账号")
|
||
}
|
||
|
||
Info("选择账号: %s (剩余空间: %d GB)", selectedAccount.Username, selectedAccount.LeftSpace/1024/1024/1024)
|
||
|
||
// TODO: 执行实际的转存操作
|
||
// 这里需要调用网盘API进行转存
|
||
|
||
return nil
|
||
}
|
||
|
||
// selectBestAccount 选择最佳网盘账号
|
||
func (s *Scheduler) selectBestAccount(accounts []entity.Cks, config *entity.SystemConfig) *entity.Cks {
|
||
// TODO: 实现账号选择逻辑
|
||
// 1. 过滤出有效的账号
|
||
// 2. 检查剩余空间是否满足最小要求
|
||
// 3. 优先选择VIP账号
|
||
// 4. 优先选择剩余空间大的账号
|
||
// 5. 考虑账号的使用频率(避免单个账号过度使用)
|
||
|
||
minSpaceBytes := int64(config.AutoTransferMinSpace) * 1024 * 1024 * 1024 // 转换为字节
|
||
|
||
var bestAccount *entity.Cks
|
||
var maxScore int64 = -1
|
||
|
||
for _, account := range accounts {
|
||
if !account.IsValid {
|
||
continue
|
||
}
|
||
|
||
// 检查剩余空间
|
||
if account.LeftSpace < minSpaceBytes {
|
||
continue
|
||
}
|
||
|
||
// 计算账号评分
|
||
score := s.calculateAccountScore(&account)
|
||
if score > maxScore {
|
||
maxScore = score
|
||
bestAccount = &account
|
||
}
|
||
}
|
||
|
||
return bestAccount
|
||
}
|
||
|
||
// calculateAccountScore 计算账号评分
|
||
func (s *Scheduler) calculateAccountScore(account *entity.Cks) int64 {
|
||
// TODO: 实现账号评分算法
|
||
// 1. VIP账号加分
|
||
// 2. 剩余空间大的账号加分
|
||
// 3. 使用率低的账号加分
|
||
// 4. 可以根据历史使用情况调整评分
|
||
|
||
score := int64(0)
|
||
|
||
// VIP账号加分
|
||
if account.VipStatus {
|
||
score += 1000
|
||
}
|
||
|
||
// 剩余空间加分(每GB加1分)
|
||
score += account.LeftSpace / (1024 * 1024 * 1024)
|
||
|
||
// 使用率加分(使用率越低分数越高)
|
||
if account.Space > 0 {
|
||
usageRate := float64(account.UsedSpace) / float64(account.Space)
|
||
score += int64((1 - usageRate) * 500) // 使用率越低,加分越多
|
||
}
|
||
|
||
return score
|
||
}
|