diff --git a/db/converter/system_config_converter.go b/db/converter/system_config_converter.go index 707d597..284f948 100644 --- a/db/converter/system_config_converter.go +++ b/db/converter/system_config_converter.go @@ -117,19 +117,11 @@ func RequestToSystemConfig(req *dto.SystemConfigRequest) []entity.SystemConfig { configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: strconv.FormatBool(req.AutoFetchHotDramaEnabled), Type: entity.ConfigTypeBool}) configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMaintenanceMode, Value: strconv.FormatBool(req.MaintenanceMode), Type: entity.ConfigTypeBool}) - // 整数字段 - 只添加非零值 - if req.AutoProcessInterval != 0 { - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoProcessInterval, Value: strconv.Itoa(req.AutoProcessInterval), Type: entity.ConfigTypeInt}) - } - if req.AutoTransferLimitDays != 0 { - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferLimitDays, Value: strconv.Itoa(req.AutoTransferLimitDays), Type: entity.ConfigTypeInt}) - } - if req.AutoTransferMinSpace != 0 { - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferMinSpace, Value: strconv.Itoa(req.AutoTransferMinSpace), Type: entity.ConfigTypeInt}) - } - if req.PageSize != 0 { - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyPageSize, Value: strconv.Itoa(req.PageSize), Type: entity.ConfigTypeInt}) - } + // 整数字段 - 添加所有提交的字段,包括0值 + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoProcessInterval, Value: strconv.Itoa(req.AutoProcessInterval), Type: entity.ConfigTypeInt}) + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferLimitDays, Value: strconv.Itoa(req.AutoTransferLimitDays), Type: entity.ConfigTypeInt}) + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferMinSpace, Value: strconv.Itoa(req.AutoTransferMinSpace), Type: entity.ConfigTypeInt}) + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyPageSize, Value: strconv.Itoa(req.PageSize), Type: entity.ConfigTypeInt}) return configs } diff --git a/db/entity/system_config.go b/db/entity/system_config.go index 4e24ab9..2b3e679 100644 --- a/db/entity/system_config.go +++ b/db/entity/system_config.go @@ -12,7 +12,7 @@ type SystemConfig struct { // 键值对配置 Key string `json:"key" gorm:"size:100;not null;unique;comment:配置键"` - Value string `json:"value" gorm:"size:1000"` + Value string `json:"value" gorm:"type:text"` Type string `json:"type" gorm:"size:20;default:'string'"` // string, int, bool, json } diff --git a/db/repo/resource_repository.go b/db/repo/resource_repository.go index 1045c62..b081260 100644 --- a/db/repo/resource_repository.go +++ b/db/repo/resource_repository.go @@ -32,7 +32,7 @@ type ResourceRepository interface { InvalidateCache() error FindExists(url string, excludeID ...uint) (bool, error) BatchFindByURLs(urls []string) ([]entity.Resource, error) - GetResourcesForTransfer(panID uint, sinceTime time.Time) ([]*entity.Resource, error) + GetResourcesForTransfer(panID uint, sinceTime time.Time, limit int) ([]*entity.Resource, error) CreateResourceTag(resourceID, tagID uint) error } @@ -398,12 +398,18 @@ func (r *ResourceRepositoryImpl) BatchFindByURLs(urls []string) ([]entity.Resour } // GetResourcesForTransfer 获取需要转存的资源 -func (r *ResourceRepositoryImpl) GetResourcesForTransfer(panID uint, sinceTime time.Time) ([]*entity.Resource, error) { +func (r *ResourceRepositoryImpl) GetResourcesForTransfer(panID uint, sinceTime time.Time, limit int) ([]*entity.Resource, error) { var resources []*entity.Resource query := r.db.Where("pan_id = ? AND (save_url = '' OR save_url IS NULL) AND (error_msg = '' OR error_msg IS NULL)", panID) if !sinceTime.IsZero() { query = query.Where("created_at >= ?", sinceTime) } + + // 添加数量限制 + if limit > 0 { + query = query.Limit(limit) + } + err := query.Order("created_at DESC").Find(&resources).Error if err != nil { return nil, err diff --git a/handlers/system_config_handler.go b/handlers/system_config_handler.go index 81a21db..036b664 100644 --- a/handlers/system_config_handler.go +++ b/handlers/system_config_handler.go @@ -51,23 +51,23 @@ func (h *SystemConfigHandler) UpdateConfig(c *gin.Context) { return } - if req.AutoProcessInterval != 0 && (req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440) { + if req.AutoProcessInterval > 0 && (req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440) { ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest) return } - if req.PageSize != 0 && (req.PageSize < 10 || req.PageSize > 500) { + if req.PageSize > 0 && (req.PageSize < 10 || req.PageSize > 500) { ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest) return } // 验证自动转存配置 - if req.AutoTransferLimitDays != 0 && (req.AutoTransferLimitDays < 0 || req.AutoTransferLimitDays > 365) { + if req.AutoTransferLimitDays > 0 && (req.AutoTransferLimitDays < 0 || req.AutoTransferLimitDays > 365) { ErrorResponse(c, "自动转存限制天数必须在0-365之间", http.StatusBadRequest) return } - if req.AutoTransferMinSpace != 0 && (req.AutoTransferMinSpace < 100 || req.AutoTransferMinSpace > 1024) { + if req.AutoTransferMinSpace > 0 && (req.AutoTransferMinSpace < 100 || req.AutoTransferMinSpace > 1024) { ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest) return } diff --git a/scheduler/auto_transfer.go b/scheduler/auto_transfer.go index 549b43a..cde4003 100644 --- a/scheduler/auto_transfer.go +++ b/scheduler/auto_transfer.go @@ -146,8 +146,24 @@ func (a *AutoTransferScheduler) processAutoTransfer() { utils.Info(fmt.Sprintf("找到 %d 个可用quark网盘账号,开始自动转存处理...", len(validAccounts))) - // 获取需要转存的资源 - resources, err := a.getResourcesForTransfer(quarkPanID) + // 计算处理数量限制 + // 假设每5秒转存一个资源,每分钟20个,5分钟100个 + // 根据时间间隔和账号数量计算大致的处理数量 + interval := 5 * time.Minute // 默认5分钟 + if autoProcessInterval, err := a.systemConfigRepo.GetConfigInt(entity.ConfigKeyAutoProcessInterval); err == nil && autoProcessInterval > 0 { + interval = time.Duration(autoProcessInterval) * time.Minute + } + + // 计算每分钟能处理的资源数量:账号数 * 12(每分钟12个,即每5秒一个) + resourcesPerMinute := len(validAccounts) * 12 + // 根据时间间隔计算总处理数量 + maxProcessCount := int(float64(resourcesPerMinute) * interval.Minutes()) + + utils.Info(fmt.Sprintf("时间间隔: %v, 账号数: %d, 每分钟处理能力: %d, 最大处理数量: %d", + interval, len(validAccounts), resourcesPerMinute, maxProcessCount)) + + // 获取需要转存的资源(限制数量) + resources, err := a.getResourcesForTransfer(quarkPanID, maxProcessCount) if err != nil { utils.Error(fmt.Sprintf("获取需要转存的资源失败: %v", err)) return @@ -167,33 +183,58 @@ func (a *AutoTransferScheduler) processAutoTransfer() { forbiddenWords = "" // 如果获取失败,使用空字符串 } - // 过滤包含违禁词的资源 + // 过滤包含违禁词的资源,并标记违禁词错误 var filteredResources []*entity.Resource + var forbiddenResources []*entity.Resource + if forbiddenWords != "" { words := strings.Split(forbiddenWords, ",") + // 清理违禁词数组,去除空格 + var cleanWords []string + for _, word := range words { + word = strings.TrimSpace(word) + if word != "" { + cleanWords = append(cleanWords, word) + } + } + for _, resource := range resources { shouldSkip := false + var matchedWords []string title := strings.ToLower(resource.Title) description := strings.ToLower(resource.Description) - for _, word := range words { - word = strings.TrimSpace(word) - if word != "" && (strings.Contains(title, strings.ToLower(word)) || strings.Contains(description, strings.ToLower(word))) { - utils.Info(fmt.Sprintf("跳过包含违禁词 '%s' 的资源: %s", word, resource.Title)) + for _, word := range cleanWords { + wordLower := strings.ToLower(word) + if strings.Contains(title, wordLower) || strings.Contains(description, wordLower) { + matchedWords = append(matchedWords, word) shouldSkip = true - break } } - if !shouldSkip { + if shouldSkip { + // 标记为违禁词错误 + resource.ErrorMsg = fmt.Sprintf("存在违禁词: %s", strings.Join(matchedWords, ", ")) + forbiddenResources = append(forbiddenResources, resource) + utils.Info(fmt.Sprintf("标记违禁词资源: %s, 违禁词: %s", resource.Title, strings.Join(matchedWords, ", "))) + } else { filteredResources = append(filteredResources, resource) } } - utils.Info(fmt.Sprintf("违禁词过滤后,剩余 %d 个资源需要转存", len(filteredResources))) + utils.Info(fmt.Sprintf("违禁词过滤后,剩余 %d 个资源需要转存,违禁词资源 %d 个", len(filteredResources), len(forbiddenResources))) } else { filteredResources = resources } + // 注意:资源数量已在数据库查询时限制,无需再次限制 + + // 保存违禁词资源的错误信息 + for _, resource := range forbiddenResources { + if err := a.resourceRepo.Update(resource); err != nil { + utils.Error(fmt.Sprintf("保存违禁词错误信息失败 (ID: %d): %v", resource.ID, err)) + } + } + // 并发自动转存 resourceCh := make(chan *entity.Resource, len(filteredResources)) for _, res := range filteredResources { @@ -220,7 +261,8 @@ func (a *AutoTransferScheduler) processAutoTransfer() { }(account) } wg.Wait() - utils.Info(fmt.Sprintf("自动转存处理完成,账号数: %d,资源数: %d", len(validAccounts), len(filteredResources))) + utils.Info(fmt.Sprintf("自动转存处理完成,账号数: %d,处理资源数: %d,违禁词资源数: %d", + len(validAccounts), len(filteredResources), len(forbiddenResources))) } // getQuarkPanID 获取夸克网盘ID @@ -241,7 +283,7 @@ func (a *AutoTransferScheduler) getQuarkPanID() (uint, error) { } // getResourcesForTransfer 获取需要转存的资源 -func (a *AutoTransferScheduler) getResourcesForTransfer(quarkPanID uint) ([]*entity.Resource, error) { +func (a *AutoTransferScheduler) getResourcesForTransfer(quarkPanID uint, limit int) ([]*entity.Resource, error) { // 获取最近24小时内的资源 sinceTime := time.Now().Add(-24 * time.Hour) @@ -251,7 +293,7 @@ func (a *AutoTransferScheduler) getResourcesForTransfer(quarkPanID uint) ([]*ent return nil, fmt.Errorf("资源仓库类型错误") } - return repoImpl.GetResourcesForTransfer(quarkPanID, sinceTime) + return repoImpl.GetResourcesForTransfer(quarkPanID, sinceTime, limit) } // transferResource 转存单个资源 diff --git a/scheduler/ready_resource.go b/scheduler/ready_resource.go index 2c598b3..fb771e7 100644 --- a/scheduler/ready_resource.go +++ b/scheduler/ready_resource.go @@ -186,6 +186,30 @@ func (r *ReadyResourceScheduler) convertReadyResourceToResource(readyResource en PanID: r.getPanIDByServiceType(serviceType), } + // 检查违禁词 + forbiddenWords, err := r.systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords) + if err == nil && forbiddenWords != "" { + words := strings.Split(forbiddenWords, ",") + var matchedWords []string + title := strings.ToLower(resource.Title) + description := strings.ToLower(resource.Description) + + for _, word := range words { + word = strings.TrimSpace(word) + if word != "" { + wordLower := strings.ToLower(word) + if strings.Contains(title, wordLower) || strings.Contains(description, wordLower) { + matchedWords = append(matchedWords, word) + } + } + } + + if len(matchedWords) > 0 { + utils.Warn(fmt.Sprintf("资源包含违禁词: %s, 违禁词: %s", resource.Title, strings.Join(matchedWords, ", "))) + return fmt.Errorf("存在违禁词: %s", strings.Join(matchedWords, ", ")) + } + } + // 不是夸克,直接保存 if serviceType != panutils.Quark { // 检测是否有效 diff --git a/web/components.d.ts b/web/components.d.ts index afa3022..9ee62d8 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -19,6 +19,7 @@ declare module 'vue' { NSwitch: typeof import('naive-ui')['NSwitch'] NTabPane: typeof import('naive-ui')['NTabPane'] NTabs: typeof import('naive-ui')['NTabs'] + NTag: typeof import('naive-ui')['NTag'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } diff --git a/web/components/AdminHeader.vue b/web/components/AdminHeader.vue index 4ab1437..42635b1 100644 --- a/web/components/AdminHeader.vue +++ b/web/components/AdminHeader.vue @@ -18,10 +18,6 @@ {{ currentPageTitle }} - -
@@ -37,7 +33,7 @@ diff --git a/web/pages/admin/system-config.vue b/web/pages/admin/system-config.vue index 94aa717..92a6a38 100644 --- a/web/pages/admin/system-config.vue +++ b/web/pages/admin/system-config.vue @@ -74,9 +74,20 @@
- +
+ +
+ + 开源违禁词 + +
+
生成 +

用于公开API的访问认证,建议使用随机字符串 @@ -309,6 +336,7 @@ const notification = useNotification() // 响应式数据 const loading = ref(false) +const loadingForbiddenWords = ref(false) const config = ref({ // SEO 配置 siteTitle: '老九网盘资源数据库', @@ -332,24 +360,29 @@ const config = ref({ }) // 系统配置状态(用于SEO) -const systemConfig = ref(null) +const systemConfig = ref({ + site_title: '老九网盘资源数据库', + site_description: '系统配置管理页面', + keywords: '系统配置,管理', + author: '系统管理员' +}) const originalConfig = ref(null) // 页面元数据 - 移到变量声明之后 useHead({ - title: () => systemConfig.value?.site_title ? `${systemConfig.value.site_title} - 系统配置` : '系统配置 - 老九网盘资源数据库', + title: () => `${systemConfig.value.site_title} - 系统配置`, meta: [ { name: 'description', - content: () => systemConfig.value?.site_description || '系统配置管理页面' + content: () => systemConfig.value.site_description }, { name: 'keywords', - content: () => systemConfig.value?.keywords || '系统配置,管理' + content: () => systemConfig.value.keywords }, { name: 'author', - content: () => systemConfig.value?.author || '系统管理员' + content: () => systemConfig.value.author } ] }) @@ -370,13 +403,13 @@ const loadConfig = async () => { author: response.author || '系统管理员', copyright: response.copyright || '© 2024 老九网盘资源数据库', autoProcessReadyResources: response.auto_process_ready_resources || false, - autoProcessInterval: response.auto_process_interval || 30, + autoProcessInterval: String(response.auto_process_interval || 30), autoTransferEnabled: response.auto_transfer_enabled || false, // 新增 - autoTransferLimitDays: response.auto_transfer_limit_days || 30, // 新增:自动转存限制天数 - autoTransferMinSpace: response.auto_transfer_min_space || 500, // 新增:最小存储空间(GB) + autoTransferLimitDays: String(response.auto_transfer_limit_days || 30), // 新增:自动转存限制天数 + autoTransferMinSpace: String(response.auto_transfer_min_space || 500), // 新增:最小存储空间(GB) autoFetchHotDramaEnabled: response.auto_fetch_hot_drama_enabled || false, // 新增 - forbiddenWords: response.forbidden_words || '', - pageSize: response.page_size || 100, + forbiddenWords: formatForbiddenWordsForDisplay(response.forbidden_words || ''), + pageSize: String(response.page_size || 100), maintenanceMode: response.maintenance_mode || false, apiToken: response.api_token || '' // 加载API Token } @@ -421,25 +454,25 @@ const saveConfig = async () => { changes.auto_process_ready_resources = currentConfig.autoProcessReadyResources } if (currentConfig.autoProcessInterval !== original.autoProcessInterval) { - changes.auto_process_interval = currentConfig.autoProcessInterval + changes.auto_process_interval = parseInt(currentConfig.autoProcessInterval) || 0 } if (currentConfig.autoTransferEnabled !== original.autoTransferEnabled) { changes.auto_transfer_enabled = currentConfig.autoTransferEnabled } if (currentConfig.autoTransferLimitDays !== original.autoTransferLimitDays) { - changes.auto_transfer_limit_days = currentConfig.autoTransferLimitDays + changes.auto_transfer_limit_days = parseInt(currentConfig.autoTransferLimitDays) || 0 } if (currentConfig.autoTransferMinSpace !== original.autoTransferMinSpace) { - changes.auto_transfer_min_space = currentConfig.autoTransferMinSpace + changes.auto_transfer_min_space = parseInt(currentConfig.autoTransferMinSpace) || 0 } if (currentConfig.autoFetchHotDramaEnabled !== original.autoFetchHotDramaEnabled) { changes.auto_fetch_hot_drama_enabled = currentConfig.autoFetchHotDramaEnabled } if (currentConfig.forbiddenWords !== original.forbiddenWords) { - changes.forbidden_words = currentConfig.forbiddenWords + changes.forbidden_words = formatForbiddenWordsForSave(currentConfig.forbiddenWords) } if (currentConfig.pageSize !== original.pageSize) { - changes.page_size = currentConfig.pageSize + changes.page_size = parseInt(currentConfig.pageSize) || 0 } if (currentConfig.maintenanceMode !== original.maintenanceMode) { changes.maintenance_mode = currentConfig.maintenanceMode @@ -504,6 +537,65 @@ const generateApiToken = () => { }) }; +// 复制API Token +const copyApiToken = async () => { + try { + await navigator.clipboard.writeText(config.value.apiToken); + notification.success({ + content: 'API Token已复制到剪贴板', + duration: 3000 + }); + } catch (err) { + // 降级方案:使用传统的复制方法 + const textArea = document.createElement('textarea'); + textArea.value = config.value.apiToken; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand('copy'); + notification.success({ + content: 'API Token已复制到剪贴板', + duration: 3000 + }); + } catch (fallbackErr) { + notification.error({ + content: '复制失败,请手动复制', + duration: 3000 + }); + } + document.body.removeChild(textArea); + } +}; + + +// 打开违禁词源文件 +const openForbiddenWordsSource = () => { + const url = 'https://raw.githubusercontent.com/ctwj/urldb/refs/heads/main/db/forbidden.txt' + window.open(url, '_blank', 'noopener,noreferrer') +} + +// 格式化违禁词用于显示(逗号分隔转为多行) +const formatForbiddenWordsForDisplay = (forbiddenWords) => { + if (!forbiddenWords) return '' + + // 按逗号分割,过滤空字符串,然后按行显示 + return forbiddenWords.split(',') + .map(word => word.trim()) + .filter(word => word.length > 0) + .join('\n') +} + +// 格式化违禁词用于保存(多行转为逗号分隔) +const formatForbiddenWordsForSave = (forbiddenWords) => { + if (!forbiddenWords) return '' + + // 按行分割,过滤空行,然后用逗号连接 + return forbiddenWords.split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0) + .join(',') +} + // 页面加载时获取配置 onMounted(() => { loadConfig()