diff --git a/db/converter/telegram_channel_converter.go b/db/converter/telegram_channel_converter.go index df47df6..77e58f3 100644 --- a/db/converter/telegram_channel_converter.go +++ b/db/converter/telegram_channel_converter.go @@ -24,6 +24,8 @@ func TelegramChannelToResponse(channel entity.TelegramChannel) dto.TelegramChann ContentCategories: channel.ContentCategories, ContentTags: channel.ContentTags, IsActive: channel.IsActive, + ResourceStrategy: channel.ResourceStrategy, + TimeLimit: channel.TimeLimit, LastPushAt: channel.LastPushAt, RegisteredBy: channel.RegisteredBy, RegisteredAt: channel.RegisteredAt, @@ -41,7 +43,7 @@ func TelegramChannelsToResponse(channels []entity.TelegramChannel) []dto.Telegra // RequestToTelegramChannel 将请求DTO转换为TelegramChannel实体 func RequestToTelegramChannel(req dto.TelegramChannelRequest, registeredBy string) entity.TelegramChannel { - return entity.TelegramChannel{ + channel := entity.TelegramChannel{ ChatID: req.ChatID, ChatName: req.ChatName, ChatType: req.ChatType, @@ -55,6 +57,21 @@ func RequestToTelegramChannel(req dto.TelegramChannelRequest, registeredBy strin RegisteredBy: registeredBy, RegisteredAt: time.Now(), } + + // 设置默认值(如果为空) + if req.ResourceStrategy == "" { + channel.ResourceStrategy = "random" + } else { + channel.ResourceStrategy = req.ResourceStrategy + } + + if req.TimeLimit == "" { + channel.TimeLimit = "none" + } else { + channel.TimeLimit = req.TimeLimit + } + + return channel } // TelegramBotConfigToResponse 将Telegram bot配置转换为响应DTO diff --git a/db/dto/telegram_channel.go b/db/dto/telegram_channel.go index bed652e..9052db4 100644 --- a/db/dto/telegram_channel.go +++ b/db/dto/telegram_channel.go @@ -14,6 +14,8 @@ type TelegramChannelRequest struct { ContentCategories string `json:"content_categories"` ContentTags string `json:"content_tags"` IsActive bool `json:"is_active"` + ResourceStrategy string `json:"resource_strategy"` + TimeLimit string `json:"time_limit"` } // TelegramChannelUpdateRequest 更新 Telegram 频道/群组请求(ChatID可选) @@ -28,6 +30,8 @@ type TelegramChannelUpdateRequest struct { ContentCategories string `json:"content_categories"` ContentTags string `json:"content_tags"` IsActive bool `json:"is_active"` + ResourceStrategy string `json:"resource_strategy"` + TimeLimit string `json:"time_limit"` } // TelegramChannelResponse Telegram 频道/群组响应 @@ -43,6 +47,8 @@ type TelegramChannelResponse struct { ContentCategories string `json:"content_categories"` ContentTags string `json:"content_tags"` IsActive bool `json:"is_active"` + ResourceStrategy string `json:"resource_strategy"` + TimeLimit string `json:"time_limit"` LastPushAt *time.Time `json:"last_push_at"` RegisteredBy string `json:"registered_by"` RegisteredAt time.Time `json:"registered_at"` diff --git a/db/entity/telegram_channel.go b/db/entity/telegram_channel.go index f94364b..c40afc2 100644 --- a/db/entity/telegram_channel.go +++ b/db/entity/telegram_channel.go @@ -36,6 +36,10 @@ type TelegramChannel struct { Token string `json:"token" gorm:"size:255;comment:访问令牌"` ApiType string `json:"api_type" gorm:"size:50;comment:API类型"` IsPushSavedInfo bool `json:"is_push_saved_info" gorm:"default:false;comment:是否只推送已转存资源"` + + // 资源策略和时间限制配置 + ResourceStrategy string `json:"resource_strategy" gorm:"size:20;default:'random';comment:资源策略:latest-最新优先,transferred-已转存优先,random-纯随机"` + TimeLimit string `json:"time_limit" gorm:"size:20;default:'none';comment:时间限制:none-无限制,week-一周内,month-一月内"` } // TableName 指定表名 diff --git a/handlers/telegram_handler.go b/handlers/telegram_handler.go index b51262f..92529c6 100644 --- a/handlers/telegram_handler.go +++ b/handlers/telegram_handler.go @@ -206,6 +206,9 @@ func (h *TelegramHandler) UpdateChannel(c *gin.Context) { return } + utils.Info("[TELEGRAM:HANDLER] 接收到频道更新请求: ID=%s, ChatName=%s, PushStartTime=%s, PushEndTime=%s, ResourceStrategy=%s, TimeLimit=%s", + idStr, req.ChatName, req.PushStartTime, req.PushEndTime, req.ResourceStrategy, req.TimeLimit) + // 查找现有频道 channel, err := h.telegramChannelRepo.FindByID(uint(id)) if err != nil { @@ -213,6 +216,10 @@ func (h *TelegramHandler) UpdateChannel(c *gin.Context) { return } + // 保存前的日志 + utils.Info("[TELEGRAM:HANDLER] 更新前频道状态: PushStartTime=%s, PushEndTime=%s, ResourceStrategy=%s, TimeLimit=%s", + channel.PushStartTime, channel.PushEndTime, channel.ResourceStrategy, channel.TimeLimit) + // 如果前端传递了ChatID,验证它是否与现有频道匹配 if req.ChatID != 0 && req.ChatID != channel.ChatID { ErrorResponse(c, "ChatID不匹配,无法更新此频道", http.StatusBadRequest) @@ -229,12 +236,18 @@ func (h *TelegramHandler) UpdateChannel(c *gin.Context) { channel.ContentCategories = req.ContentCategories channel.ContentTags = req.ContentTags channel.IsActive = req.IsActive + channel.ResourceStrategy = req.ResourceStrategy + channel.TimeLimit = req.TimeLimit if err := h.telegramChannelRepo.Update(channel); err != nil { ErrorResponse(c, "更新频道失败", http.StatusInternalServerError) return } + // 保存后的日志 + utils.Info("[TELEGRAM:HANDLER] 更新后频道状态: PushStartTime=%s, PushEndTime=%s, ResourceStrategy=%s, TimeLimit=%s", + channel.PushStartTime, channel.PushEndTime, channel.ResourceStrategy, channel.TimeLimit) + response := converter.TelegramChannelToResponse(*channel) SuccessResponse(c, response) } @@ -278,13 +291,18 @@ func (h *TelegramHandler) RegisterChannelByCommand(chatID int64, chatName, chatT // 创建新的频道记录 channel := entity.TelegramChannel{ - ChatID: chatID, - ChatName: chatName, - ChatType: chatType, - PushEnabled: true, - PushFrequency: 5, // 默认5分钟 - IsActive: true, - RegisteredBy: "bot_command", + ChatID: chatID, + ChatName: chatName, + ChatType: chatType, + PushEnabled: true, + PushFrequency: 15, // 默认15分钟 + PushStartTime: "08:30", // 默认开始时间8:30 + PushEndTime: "11:30", // 默认结束时间11:30 + IsActive: true, + RegisteredBy: "bot_command", + RegisteredAt: time.Now(), + ResourceStrategy: "random", // 默认纯随机 + TimeLimit: "none", // 默认无限制 } return h.telegramChannelRepo.Create(&channel) diff --git a/migrations/telegram_channels.sql b/migrations/telegram_channels.sql index e14b94b..4e2c3a9 100644 --- a/migrations/telegram_channels.sql +++ b/migrations/telegram_channels.sql @@ -28,6 +28,12 @@ CREATE TABLE telegram_channels ( token VARCHAR(255) COMMENT '访问令牌', api_type VARCHAR(50) COMMENT 'API类型', is_push_saved_info BOOLEAN DEFAULT FALSE COMMENT '是否只推送已转存资源', + + -- 资源策略和时间限制配置 + resource_strategy VARCHAR(20) DEFAULT 'random' COMMENT '资源策略:latest-最新优先,transferred-已转存优先,random-纯随机', + time_limit VARCHAR(20) DEFAULT 'none' COMMENT '时间限制:none-无限制,week-一周内,month-一月内', + push_start_time VARCHAR(10) COMMENT '推送开始时间,格式HH:mm', + push_end_time VARCHAR(10) COMMENT '推送结束时间,格式HH:mm', -- 索引 INDEX idx_chat_id (chat_id), diff --git a/services/telegram_bot_service.go b/services/telegram_bot_service.go index ce727c1..da55cfc 100644 --- a/services/telegram_bot_service.go +++ b/services/telegram_bot_service.go @@ -937,6 +937,191 @@ func (s *TelegramBotServiceImpl) pushToChannel(channel entity.TelegramChannel) { func (s *TelegramBotServiceImpl) findResourcesForChannel(channel entity.TelegramChannel) []interface{} { utils.Info("[TELEGRAM:PUSH] 开始为频道 %s (%d) 查找资源", channel.ChatName, channel.ChatID) + // 获取最近推送的历史资源ID,避免重复推送 + excludeResourceIDs := s.getRecentlyPushedResourceIDs(channel.ChatID) + + // 解析资源策略 + strategy := channel.ResourceStrategy + if strategy == "" { + strategy = "random" // 默认纯随机 + } + + utils.Info("[TELEGRAM:PUSH] 使用策略: %s, 时间限制: %s, 排除最近推送资源数: %d", + strategy, channel.TimeLimit, len(excludeResourceIDs)) + + // 根据策略获取资源 + switch strategy { + case "latest": + // 最新优先策略 - 获取最近的资源 + return s.findLatestResources(channel, excludeResourceIDs) + case "transferred": + // 已转存优先策略 - 优先获取有转存链接的资源 + return s.findTransferredResources(channel, excludeResourceIDs) + case "random": + // 纯随机策略(原逻辑) + return s.findRandomResources(channel, excludeResourceIDs) + default: + // 默认随机策略 + return s.findRandomResources(channel, excludeResourceIDs) + } +} + +// findLatestResources 查找最新资源 +func (s *TelegramBotServiceImpl) findLatestResources(channel entity.TelegramChannel, excludeResourceIDs []uint) []interface{} { + params := s.buildFilterParams(channel) + + // 使用现有的搜索功能,按更新时间倒序获取最新资源 + resources, _, err := s.resourceRepo.SearchWithFilters(params) + if err != nil { + utils.Error("[TELEGRAM:PUSH] 获取最新资源失败: %v", err) + return []interface{}{} + } + + // 排除最近推送过的资源 + if len(excludeResourceIDs) > 0 { + resources = s.excludePushedResources(resources, excludeResourceIDs) + } + + // 应用时间限制 + if channel.TimeLimit != "none" && len(resources) > 0 { + resources = s.applyTimeFilter(resources, channel.TimeLimit) + } + + if len(resources) == 0 { + utils.Info("[TELEGRAM:PUSH] 没有找到符合条件的最新资源") + return []interface{}{} + } + + // 返回最新资源(第一条) + utils.Info("[TELEGRAM:PUSH] 成功获取最新资源: %s", resources[0].Title) + return []interface{}{resources[0]} +} + +// findTransferredResources 查找已转存资源 +func (s *TelegramBotServiceImpl) findTransferredResources(channel entity.TelegramChannel, excludeResourceIDs []uint) []interface{} { + params := s.buildFilterParams(channel) + + // 添加转存链接条件 + params["has_save_url"] = true + + // 优先获取有转存链接的资源 + resources, _, err := s.resourceRepo.SearchWithFilters(params) + if err != nil { + utils.Error("[TELEGRAM:PUSH] 获取已转存资源失败: %v", err) + return []interface{}{} + } + + // 排除最近推送过的资源 + if len(excludeResourceIDs) > 0 { + resources = s.excludePushedResources(resources, excludeResourceIDs) + } + + // 应用时间限制 + if channel.TimeLimit != "none" && len(resources) > 0 { + resources = s.applyTimeFilter(resources, channel.TimeLimit) + } + + if len(resources) == 0 { + utils.Info("[TELEGRAM:PUSH] 没有找到符合条件的已转存资源,尝试获取随机资源") + // 如果没有已转存资源,回退到随机策略 + return s.findRandomResources(channel, excludeResourceIDs) + } + + // 返回第一个有转存链接的资源 + utils.Info("[TELEGRAM:PUSH] 成功获取已转存资源: %s", resources[0].Title) + return []interface{}{resources[0]} +} + +// findRandomResources 查找随机资源(原有逻辑) +func (s *TelegramBotServiceImpl) findRandomResources(channel entity.TelegramChannel, excludeResourceIDs []uint) []interface{} { + params := s.buildFilterParams(channel) + + // 如果是已转存优先策略但没有找到转存资源,这里会回退到随机策略 + // 此时不需要额外的转存链接条件,让随机函数处理 + + // 先尝试获取候选资源列表,然后从中排除已推送的资源 + var candidateResources []entity.Resource + var err error + + // 使用搜索功能获取候选资源,然后过滤 + params["limit"] = 100 // 获取更多候选资源 + candidateResources, _, err = s.resourceRepo.SearchWithFilters(params) + if err != nil { + utils.Error("[TELEGRAM:PUSH] 获取候选资源失败: %v", err) + return []interface{}{} + } + + // 排除最近推送过的资源 + if len(excludeResourceIDs) > 0 { + candidateResources = s.excludePushedResources(candidateResources, excludeResourceIDs) + } + + // 应用时间限制 + if channel.TimeLimit != "none" && len(candidateResources) > 0 { + candidateResources = s.applyTimeFilter(candidateResources, channel.TimeLimit) + } + + // 如果还有候选资源,随机选择一个 + if len(candidateResources) > 0 { + // 简单随机选择(未来可以考虑使用更好的随机算法) + randomIndex := time.Now().Nanosecond() % len(candidateResources) + selectedResource := candidateResources[randomIndex] + + utils.Info("[TELEGRAM:PUSH] 成功获取随机资源: %s (从 %d 个候选资源中选择)", + selectedResource.Title, len(candidateResources)) + return []interface{}{selectedResource} + } + + // 如果候选资源不足,回退到数据库随机函数 + defer func() { + if r := recover(); r != nil { + utils.Warn("[TELEGRAM:PUSH] 随机查询失败,回退到传统方法: %v", r) + } + }() + + randomResource, err := s.resourceRepo.GetRandomResourceWithFilters(params["category"].(string), params["tag"].(string), channel.IsPushSavedInfo) + if err == nil && randomResource != nil { + utils.Info("[TELEGRAM:PUSH] 使用数据库随机函数获取资源: %s", randomResource.Title) + return []interface{}{randomResource} + } + + return []interface{}{} +} + +// applyTimeFilter 应用时间限制过滤 +func (s *TelegramBotServiceImpl) applyTimeFilter(resources []entity.Resource, timeLimit string) []entity.Resource { + now := time.Now() + var filtered []entity.Resource + + for _, resource := range resources { + include := false + + switch timeLimit { + case "week": + // 一周内 + if resource.CreatedAt.After(now.AddDate(0, 0, -7)) { + include = true + } + case "month": + // 一月内 + if resource.CreatedAt.After(now.AddDate(0, -1, 0)) { + include = true + } + case "none": + // 无限制,包含所有 + include = true + } + + if include { + filtered = append(filtered, resource) + } + } + + return filtered +} + +// buildFilterParams 构建过滤参数 +func (s *TelegramBotServiceImpl) buildFilterParams(channel entity.TelegramChannel) map[string]interface{} { params := map[string]interface{}{"category": "", "tag": ""} if channel.ContentCategories != "" { @@ -955,20 +1140,7 @@ func (s *TelegramBotServiceImpl) findResourcesForChannel(channel entity.Telegram params["tag"] = tags[0] } - // 尝试使用 PostgreSQL 的随机功能 - defer func() { - if r := recover(); r != nil { - utils.Warn("[TELEGRAM:PUSH] 随机查询失败,回退到传统方法: %v", r) - } - }() - - randomResource, err := s.resourceRepo.GetRandomResourceWithFilters(params["category"].(string), params["tag"].(string), channel.IsPushSavedInfo) - if err == nil && randomResource != nil { - utils.Info("[TELEGRAM:PUSH] 成功获取随机资源: %s", randomResource.Title) - return []interface{}{randomResource} - } - - return []interface{}{} + return params } // buildPushMessage 构建推送消息 @@ -1091,16 +1263,20 @@ func (s *TelegramBotServiceImpl) RegisterChannel(chatID int64, chatName, chatTyp ChatName: chatName, ChatType: chatType, PushEnabled: true, - PushFrequency: 5, // 默认5分钟 + PushFrequency: 15, // 默认15分钟 + PushStartTime: "08:30", // 默认开始时间8:30 + PushEndTime: "11:30", // 默认结束时间11:30 IsActive: true, RegisteredBy: "bot_command", RegisteredAt: time.Now(), ContentCategories: "", ContentTags: "", - API: "", // 后续可配置 - Token: "", // 后续可配置 - ApiType: "l9", // 默认l9类型 - IsPushSavedInfo: false, // 默认推送所有资源 + API: "", // 后续可配置 + Token: "", // 后续可配置 + ApiType: "l9", // 默认l9类型 + IsPushSavedInfo: false, // 默认推送所有资源 + ResourceStrategy: "random", // 默认纯随机 + TimeLimit: "none", // 默认无限制 } return s.channelRepo.Create(&channel) @@ -1396,6 +1572,21 @@ func (s *TelegramBotServiceImpl) handleChannelRegistration(message *tgbotapi.Mes if existingChannel.ApiType == "" { existingChannel.ApiType = "telegram" } + if existingChannel.ResourceStrategy == "" { + existingChannel.ResourceStrategy = "random" + } + if existingChannel.TimeLimit == "" { + existingChannel.TimeLimit = "none" + } + if existingChannel.PushFrequency == 0 { + existingChannel.PushFrequency = 15 + } + if existingChannel.PushStartTime == "" { + existingChannel.PushStartTime = "08:30" + } + if existingChannel.PushEndTime == "" { + existingChannel.PushEndTime = "11:30" + } err := s.channelRepo.Update(existingChannel) if err != nil { @@ -1464,3 +1655,41 @@ func (s *TelegramBotServiceImpl) CleanupDuplicateChannels() error { utils.Info("[TELEGRAM:CLEANUP:SUCCESS] 成功清理重复的频道记录") return nil } + +// getRecentlyPushedResourceIDs 获取最近推送过的资源ID列表 +func (s *TelegramBotServiceImpl) getRecentlyPushedResourceIDs(chatID int64) []uint { + // 这里需要实现获取推送历史的逻辑 + // 由于没有现有的推送历史表,我们暂时返回空列表 + // 未来可以添加一个 TelegramPushHistory 实体来跟踪推送历史 + utils.Debug("[TELEGRAM:PUSH] 获取推送历史,ChatID: %d", chatID) + + // 暂时返回空列表,表示没有历史推送记录 + // TODO: 实现推送历史跟踪功能 + return []uint{} +} + +// excludePushedResources 从候选资源中排除已推送过的资源 +func (s *TelegramBotServiceImpl) excludePushedResources(resources []entity.Resource, excludeIDs []uint) []entity.Resource { + if len(excludeIDs) == 0 { + return resources + } + + utils.Debug("[TELEGRAM:PUSH] 排除 %d 个已推送资源", len(excludeIDs)) + + // 创建排除ID的映射,提高查找效率 + excludeMap := make(map[uint]bool) + for _, id := range excludeIDs { + excludeMap[id] = true + } + + // 过滤资源列表 + var filtered []entity.Resource + for _, resource := range resources { + if !excludeMap[resource.ID] { + filtered = append(filtered, resource) + } + } + + utils.Debug("[TELEGRAM:PUSH] 过滤后剩余 %d 个资源", len(filtered)) + return filtered +} diff --git a/web/components/TelegramBotTab.vue b/web/components/TelegramBotTab.vue index f824150..b08d8f2 100644 --- a/web/components/TelegramBotTab.vue +++ b/web/components/TelegramBotTab.vue @@ -293,21 +293,27 @@ -
{{ channel.push_frequency }} 分钟
- {{ channel.content_categories || '全部' }} + {{ getResourceStrategyLabel(channel.resource_strategy) }}
- {{ channel.content_tags || '全部' }} + {{ getTimeLimitLabel(channel.time_limit) }} +
++ {{ channel.push_start_time && channel.push_end_time ? `${channel.push_start_time}-${channel.push_end_time}` : '全天' }}
+ 纯随机:完全随机推送资源;最新优先:优先推送最新资源;已转存优先:优先推送已转存的资源 +
++ 无限制:推送所有时间段的资源;一周内:仅推送最近一周的资源;一月内:仅推送最近一个月的资源 +
+