diff --git a/main.go b/main.go index dcad5c7..45285d6 100644 --- a/main.go +++ b/main.go @@ -152,9 +152,15 @@ func main() { // 将Repository管理器注入到handlers中 handlers.SetRepositoryManager(repoManager) + // 将Repository管理器注入到services中 + services.SetRepositoryManager(repoManager) + // 设置Meilisearch管理器到handlers中 handlers.SetMeilisearchManager(meilisearchManager) + // 设置Meilisearch管理器到services中 + services.SetMeilisearchManager(meilisearchManager) + // 设置全局调度器的Meilisearch管理器 scheduler.SetGlobalMeilisearchManager(meilisearchManager) diff --git a/services/base.go b/services/base.go new file mode 100644 index 0000000..23139bf --- /dev/null +++ b/services/base.go @@ -0,0 +1,102 @@ +package services + +import ( + "github.com/ctwj/urldb/db/entity" + "github.com/ctwj/urldb/db/repo" + "github.com/ctwj/urldb/utils" +) + +var repoManager *repo.RepositoryManager +var meilisearchManager *MeilisearchManager + +// SetRepositoryManager 设置Repository管理器 +func SetRepositoryManager(manager *repo.RepositoryManager) { + repoManager = manager +} + +// SetMeilisearchManager 设置Meilisearch管理器 +func SetMeilisearchManager(manager *MeilisearchManager) { + meilisearchManager = manager +} + +// UnifiedSearchResources 执行统一搜索(优先使用Meilisearch,否则使用数据库搜索)并处理违禁词 +func UnifiedSearchResources(keyword string, limit int, systemConfigRepo repo.SystemConfigRepository, resourceRepo repo.ResourceRepository) ([]entity.Resource, error) { + var resources []entity.Resource + var total int64 + var err error + + // 如果启用了Meilisearch,优先使用Meilisearch搜索 + if meilisearchManager != nil && meilisearchManager.IsEnabled() { + // 构建MeiliSearch过滤器 + filters := make(map[string]interface{}) + + // 使用Meilisearch搜索 + service := meilisearchManager.GetService() + if service != nil { + docs, docTotal, err := service.Search(keyword, filters, 1, limit) + if err == nil { + // 将Meilisearch文档转换为Resource实体 + for _, doc := range docs { + resource := entity.Resource{ + ID: doc.ID, + Title: doc.Title, + Description: doc.Description, + URL: doc.URL, + SaveURL: doc.SaveURL, + FileSize: doc.FileSize, + Key: doc.Key, + PanID: doc.PanID, + CreatedAt: doc.CreatedAt, + UpdatedAt: doc.UpdatedAt, + } + resources = append(resources, resource) + } + total = docTotal + + // 获取违禁词配置并处理违禁词 + cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) { + return systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords) + }) + if err != nil { + utils.Error("获取违禁词配置失败: %v", err) + cleanWords = []string{} // 如果获取失败,使用空列表 + } + + // 处理违禁词替换 + if len(cleanWords) > 0 { + resources = utils.ProcessResourcesForbiddenWords(resources, cleanWords) + } + + return resources, nil + } else { + utils.Error("MeiliSearch搜索失败,回退到数据库搜索: %v", err) + } + } + } + + // 如果MeiliSearch未启用、搜索失败或没有搜索关键词,使用数据库搜索 + resources, total, err = resourceRepo.Search(keyword, nil, 1, limit) + if err != nil { + return nil, err + } + + if total == 0 { + return []entity.Resource{}, nil + } + + // 获取违禁词配置并处理违禁词 + cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) { + return systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords) + }) + if err != nil { + utils.Error("获取违禁词配置失败: %v", err) + cleanWords = []string{} // 如果获取失败,使用空列表 + } + + // 处理违禁词替换 + if len(cleanWords) > 0 { + resources = utils.ProcessResourcesForbiddenWords(resources, cleanWords) + } + + return resources, nil +} diff --git a/services/wechat_bot_service_impl.go b/services/wechat_bot_service_impl.go index 2483404..bae71e0 100644 --- a/services/wechat_bot_service_impl.go +++ b/services/wechat_bot_service_impl.go @@ -197,6 +197,24 @@ func (s *WechatBotServiceImpl) handleTextMessage(msg *message.MixMessage) (inter return message.NewText("请输入搜索关键词"), nil } + // 检查搜索关键词是否包含违禁词 + cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) { + return s.systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords) + }) + if err != nil { + utils.Error("获取违禁词配置失败: %v", err) + cleanWords = []string{} // 如果获取失败,使用空列表 + } + + // 检查关键词是否包含违禁词 + if len(cleanWords) > 0 { + containsForbidden, matchedWords := utils.CheckContainsForbiddenWords(keyword, cleanWords) + if containsForbidden { + utils.Info("[WECHAT:MESSAGE] 搜索关键词包含违禁词: %v", matchedWords) + return message.NewText("您的搜索关键词包含违禁内容,不予处理"), nil + } + } + // 搜索资源 utils.Debug("[WECHAT:MESSAGE] 开始搜索资源,限制数量: %d", s.config.SearchLimit) resources, err := s.SearchResources(keyword) @@ -212,7 +230,7 @@ func (s *WechatBotServiceImpl) handleTextMessage(msg *message.MixMessage) (inter } // 创建搜索会话并保存第一页结果 - s.searchSessionManager.CreateSession(string(msg.FromUserName), keyword, resources, 5) + s.searchSessionManager.CreateSession(string(msg.FromUserName), keyword, resources, 4) pageResources := s.searchSessionManager.GetCurrentPageResources(string(msg.FromUserName)) // 格式化第一页搜索结果 @@ -270,42 +288,82 @@ func (s *WechatBotServiceImpl) handleGetResource(userID, command string) (interf return message.NewText("没有找到搜索记录,请先进行搜索"), nil } + // 检查是否只输入了"获取"或"get",没有指定编号 + if command == "获取" || command == "get" { + return message.NewText("📌 请输入要获取的资源编号\n\n💡 提示:回复\"获取 1\"或\"get 1\"获取第一个资源的详细信息"), nil + } + // 解析命令,例如:"获取 1" 或 "get 2" + // 支持"获取4"这种没有空格的格式 var index int - _, err := fmt.Sscanf(command, "获取 %d", &index) + _, err := fmt.Sscanf(command, "获取%d", &index) if err != nil { - _, err = fmt.Sscanf(command, "get %d", &index) + _, err = fmt.Sscanf(command, "获取 %d", &index) if err != nil { - return message.NewText("命令格式错误,请使用:获取 1 或 get 1"), nil + _, err = fmt.Sscanf(command, "get%d", &index) + if err != nil { + _, err = fmt.Sscanf(command, "get %d", &index) + if err != nil { + return message.NewText("❌ 命令格式错误\n\n📌 正确格式:\n • 获取 1\n • get 1\n • 获取1\n • get1"), nil + } + } } } if index < 1 || index > len(session.Resources) { - return message.NewText(fmt.Sprintf("资源编号超出范围,请输入 1-%d 之间的数字", len(session.Resources))), nil + return message.NewText(fmt.Sprintf("❌ 资源编号超出范围\n\n📌 请输入 1-%d 之间的数字\n💡 提示:回复\"获取 %d\"获取第%d个资源", len(session.Resources), index, index)), nil } // 获取指定资源 resource := session.Resources[index-1] - // 格式化资源详细信息 + // 格式化资源详细信息(美化输出) var result strings.Builder - result.WriteString(fmt.Sprintf("📋 资源详情\n\n")) - result.WriteString(fmt.Sprintf("标题: %s\n", resource.Title)) + // result.WriteString(fmt.Sprintf("📌 资源详情\n\n")) + + // 标题 + result.WriteString(fmt.Sprintf("📌 标题: %s\n", resource.Title)) + + // 描述 if resource.Description != "" { - result.WriteString(fmt.Sprintf("描述: %s\n", resource.Description)) + result.WriteString(fmt.Sprintf("\n📝 描述:\n %s\n", resource.Description)) } + + // 文件大小 if resource.FileSize != "" { - result.WriteString(fmt.Sprintf("大小: %s\n", resource.FileSize)) + result.WriteString(fmt.Sprintf("\n📊 大小: %s\n", resource.FileSize)) } + + // 作者 if resource.Author != "" { - result.WriteString(fmt.Sprintf("作者: %s\n", resource.Author)) + result.WriteString(fmt.Sprintf("\n👤 作者: %s\n", resource.Author)) } + + // 分类 + if resource.Category.Name != "" { + result.WriteString(fmt.Sprintf("\n📂 分类: %s\n", resource.Category.Name)) + } + + // 标签 + if len(resource.Tags) > 0 { + result.WriteString("\n🏷️ 标签: ") + var tags []string + for _, tag := range resource.Tags { + tags = append(tags, tag.Name) + } + result.WriteString(fmt.Sprintf("%s\n", strings.Join(tags, " "))) + } + + // 链接(美化) if resource.SaveURL != "" { - result.WriteString(fmt.Sprintf("\n📥 转存链接: %s", resource.SaveURL)) + result.WriteString(fmt.Sprintf("\n📥 转存链接:\n %s", resource.SaveURL)) } else if resource.URL != "" { - result.WriteString(fmt.Sprintf("\n🔗 资源链接: %s", resource.URL)) + result.WriteString(fmt.Sprintf("\n🔗 资源链接:\n %s", resource.URL)) } + // 添加操作提示 + result.WriteString(fmt.Sprintf("\n\n💡 提示:回复\"获取 %d\"可再次查看此资源", index)) + return message.NewText(result.String()), nil } @@ -322,18 +380,65 @@ func (s *WechatBotServiceImpl) formatPageResources(keyword string, resources []e result.WriteString(fmt.Sprintf("🔍 搜索\"%s\"的结果(第%d/%d页):\n\n", keyword, currentPage, totalPages)) for i, resource := range resources { + // 构建当前资源的文本表示 + var resourceText strings.Builder + // 计算全局索引(当前页的第i个资源在整个结果中的位置) - globalIndex := (currentPage-1)*5 + i + 1 - result.WriteString(fmt.Sprintf("%d. %s\n", globalIndex, resource.Title)) + globalIndex := (currentPage-1)*4 + i + 1 + resourceText.WriteString(fmt.Sprintf("%d. 📌 %s\n", globalIndex, resource.Title)) + if resource.Description != "" { + // 限制描述长度以避免消息过长(正确处理中文字符) desc := resource.Description - if len(desc) > 50 { - desc = desc[:50] + "..." + // 将字符串转换为 rune 切片以正确处理中文字符 + runes := []rune(desc) + if len(runes) > 50 { + desc = string(runes[:50]) + "..." } - result.WriteString(fmt.Sprintf(" %s\n", desc)) + resourceText.WriteString(fmt.Sprintf(" 📝 %s\n", desc)) } - result.WriteString(fmt.Sprintf(" 回复\"获取 %d\"查看详细信息\n", globalIndex)) - result.WriteString("\n") + + // 添加标签显示(格式:🏷️标签,空格,再接别的标签) + if len(resource.Tags) > 0 { + var tags []string + for _, tag := range resource.Tags { + tags = append(tags, "🏷️"+tag.Name) + } + // 限制标签数量以避免消息过长 + if len(tags) > 5 { + tags = tags[:5] + } + resourceText.WriteString(fmt.Sprintf(" %s\n", strings.Join(tags, " "))) + } + + resourceText.WriteString(fmt.Sprintf(" 👉 回复\"获取 %d\"查看详细信息\n", globalIndex)) + resourceText.WriteString("\n") + + // 预计算添加当前资源后的消息长度 + tempMessage := result.String() + resourceText.String() + + // 添加分页提示和预留空间 + if currentPage > 1 || currentPage < totalPages { + tempMessage += "💡 提示:回复\"" + if currentPage > 1 && currentPage < totalPages { + tempMessage += "上一页\"或\"下一页" + } else if currentPage > 1 { + tempMessage += "上一页" + } else { + tempMessage += "下一页" + } + tempMessage += "\"翻页\n" + } + + // 检查添加当前资源后是否会超过微信限制 + tempRunes := []rune(tempMessage) + if len(tempRunes) > 550 { + result.WriteString("💡 内容较多,请翻页查看更多\n") + break + } + + // 如果不会超过限制,则添加当前资源到结果中 + result.WriteString(resourceText.String()) } // 添加分页提示 @@ -349,7 +454,16 @@ func (s *WechatBotServiceImpl) formatPageResources(keyword string, resources []e result.WriteString(fmt.Sprintf("💡 提示:回复\"%s\"翻页\n", strings.Join(pageTips, "\"或\""))) } - return result.String() + // 确保消息不超过微信限制(正确处理中文字符) + message := result.String() + // 将字符串转换为 rune 切片以正确处理中文字符 + runes := []rune(message) + if len(runes) > 600 { + // 如果还是超过限制,截断消息(微信建议不超过600个字符) + message = string(runes[:597]) + "..." + } + + return message } // handleEventMessage 处理事件消息 @@ -363,17 +477,8 @@ func (s *WechatBotServiceImpl) handleEventMessage(msg *message.MixMessage) (inte // SearchResources 搜索资源 func (s *WechatBotServiceImpl) SearchResources(keyword string) ([]entity.Resource, error) { - // 使用现有的资源搜索功能 - resources, total, err := s.resourceRepo.Search(keyword, nil, 1, s.config.SearchLimit) - if err != nil { - return nil, err - } - - if total == 0 { - return []entity.Resource{}, nil - } - - return resources, nil + // 使用统一搜索函数(包含Meilisearch优先搜索和违禁词处理) + return UnifiedSearchResources(keyword, s.config.SearchLimit, s.systemConfigRepo, s.resourceRepo) } // formatSearchResults 格式化搜索结果 @@ -416,4 +521,4 @@ func (s *WechatBotServiceImpl) SendWelcomeMessage(openID string) error { // 注意:Customer API 需要额外的权限,这里仅作示例 // 实际应用中可能需要使用模板消息或其他方式 return nil -} \ No newline at end of file +} diff --git a/utils/forbidden_words.go b/utils/forbidden_words.go index b158169..7c89713 100644 --- a/utils/forbidden_words.go +++ b/utils/forbidden_words.go @@ -130,7 +130,7 @@ func (p *ForbiddenWordsProcessor) ProcessForbiddenWords(text string, forbiddenWo // ParseForbiddenWordsConfig 解析违禁词配置字符串 // 参数: -// - config: 违禁词配置字符串,多个词用逗号分隔 +// - config: 违禁词配置字符串,多个词用逗号或换行符分隔 // // 返回: // - []string: 处理后的违禁词列表 @@ -139,16 +139,21 @@ func (p *ForbiddenWordsProcessor) ParseForbiddenWordsConfig(config string) []str return nil } - words := strings.Split(config, ",") - var cleanWords []string - for _, word := range words { - word = strings.TrimSpace(word) - if word != "" { - cleanWords = append(cleanWords, word) + var words []string + // 首先尝试用换行符分割 + lines := strings.Split(config, "\n") + for _, line := range lines { + // 对每一行再用逗号分割(兼容两种格式) + parts := strings.Split(line, ",") + for _, part := range parts { + word := strings.TrimSpace(part) + if word != "" { + words = append(words, word) + } } } - return cleanWords + return words } // 全局实例,方便直接调用