package services import ( "fmt" "strconv" "strings" "github.com/ctwj/urldb/db/entity" "github.com/ctwj/urldb/utils" "github.com/silenceper/wechat/v2/cache" "github.com/silenceper/wechat/v2/officialaccount" "github.com/silenceper/wechat/v2/officialaccount/config" "github.com/silenceper/wechat/v2/officialaccount/message" ) // loadConfig 加载微信配置 func (s *WechatBotServiceImpl) loadConfig() error { configs, err := s.systemConfigRepo.GetOrCreateDefault() if err != nil { return fmt.Errorf("加载配置失败: %v", err) } utils.Info("[WECHAT] 从数据库加载到 %d 个配置项", len(configs)) // 初始化默认值 s.config.Enabled = false s.config.AppID = "" s.config.AppSecret = "" s.config.Token = "" s.config.EncodingAesKey = "" s.config.WelcomeMessage = "欢迎关注老九网盘资源库!发送关键词即可搜索资源。" s.config.AutoReplyEnabled = true s.config.SearchLimit = 5 for _, config := range configs { switch config.Key { case entity.ConfigKeyWechatBotEnabled: s.config.Enabled = config.Value == "true" utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s (Enabled: %v)", config.Key, config.Value, s.config.Enabled) case entity.ConfigKeyWechatAppId: s.config.AppID = config.Value utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key) case entity.ConfigKeyWechatAppSecret: s.config.AppSecret = config.Value utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key) case entity.ConfigKeyWechatToken: s.config.Token = config.Value utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key) case entity.ConfigKeyWechatEncodingAesKey: s.config.EncodingAesKey = config.Value utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key) case entity.ConfigKeyWechatWelcomeMessage: if config.Value != "" { s.config.WelcomeMessage = config.Value } utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s", config.Key, config.Value) case entity.ConfigKeyWechatAutoReplyEnabled: s.config.AutoReplyEnabled = config.Value == "true" utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s (AutoReplyEnabled: %v)", config.Key, config.Value, s.config.AutoReplyEnabled) case entity.ConfigKeyWechatSearchLimit: if config.Value != "" { limit, err := strconv.Atoi(config.Value) if err == nil && limit > 0 { s.config.SearchLimit = limit } } utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s (SearchLimit: %d)", config.Key, config.Value, s.config.SearchLimit) } } utils.Info("[WECHAT:SERVICE] 微信公众号机器人配置加载完成: Enabled=%v, AutoReplyEnabled=%v", s.config.Enabled, s.config.AutoReplyEnabled) return nil } // Start 启动微信公众号机器人服务 func (s *WechatBotServiceImpl) Start() error { if s.isRunning { utils.Info("[WECHAT:SERVICE] 微信公众号机器人服务已经在运行中") return nil } // 加载配置 if err := s.loadConfig(); err != nil { return fmt.Errorf("加载配置失败: %v", err) } if !s.config.Enabled || s.config.AppID == "" || s.config.AppSecret == "" { utils.Info("[WECHAT:SERVICE] 微信公众号机器人未启用或配置不完整") return nil } // 创建微信客户端 cfg := &config.Config{ AppID: s.config.AppID, AppSecret: s.config.AppSecret, Token: s.config.Token, EncodingAESKey: s.config.EncodingAesKey, Cache: cache.NewMemory(), } s.wechatClient = officialaccount.NewOfficialAccount(cfg) s.isRunning = true utils.Info("[WECHAT:SERVICE] 微信公众号机器人服务已启动") return nil } // Stop 停止微信公众号机器人服务 func (s *WechatBotServiceImpl) Stop() error { if !s.isRunning { return nil } s.isRunning = false utils.Info("[WECHAT:SERVICE] 微信公众号机器人服务已停止") return nil } // IsRunning 检查微信公众号机器人服务是否正在运行 func (s *WechatBotServiceImpl) IsRunning() bool { return s.isRunning } // ReloadConfig 重新加载微信公众号机器人配置 func (s *WechatBotServiceImpl) ReloadConfig() error { utils.Info("[WECHAT:SERVICE] 开始重新加载配置...") // 重新加载配置 if err := s.loadConfig(); err != nil { utils.Error("[WECHAT:SERVICE] 重新加载配置失败: %v", err) return fmt.Errorf("重新加载配置失败: %v", err) } utils.Info("[WECHAT:SERVICE] 配置重新加载完成: Enabled=%v, AutoReplyEnabled=%v", s.config.Enabled, s.config.AutoReplyEnabled) return nil } // GetRuntimeStatus 获取微信公众号机器人运行时状态 func (s *WechatBotServiceImpl) GetRuntimeStatus() map[string]interface{} { status := map[string]interface{}{ "is_running": s.IsRunning(), "config_loaded": s.config != nil, "app_id": s.config.AppID, } return status } // GetConfig 获取当前配置 func (s *WechatBotServiceImpl) GetConfig() *WechatBotConfig { return s.config } // HandleMessage 处理微信消息 func (s *WechatBotServiceImpl) HandleMessage(msg *message.MixMessage) (interface{}, error) { utils.Info("[WECHAT:MESSAGE] 收到消息: FromUserName=%s, MsgType=%s, Event=%s, Content=%s", msg.FromUserName, msg.MsgType, msg.Event, msg.Content) switch msg.MsgType { case message.MsgTypeText: return s.handleTextMessage(msg) case message.MsgTypeEvent: return s.handleEventMessage(msg) default: return nil, nil // 不处理其他类型消息 } } // handleTextMessage 处理文本消息 func (s *WechatBotServiceImpl) handleTextMessage(msg *message.MixMessage) (interface{}, error) { utils.Debug("[WECHAT:MESSAGE] 处理文本消息 - AutoReplyEnabled: %v", s.config.AutoReplyEnabled) if !s.config.AutoReplyEnabled { utils.Info("[WECHAT:MESSAGE] 自动回复未启用") return nil, nil } keyword := strings.TrimSpace(msg.Content) utils.Info("[WECHAT:MESSAGE] 搜索关键词: '%s'", keyword) // 检查是否是分页命令 if keyword == "上一页" || keyword == "prev" { return s.handlePrevPage(string(msg.FromUserName)) } if keyword == "下一页" || keyword == "next" { return s.handleNextPage(string(msg.FromUserName)) } // 检查是否是获取命令(例如:获取 1, 获取2等) if strings.HasPrefix(keyword, "获取") || strings.HasPrefix(keyword, "get") { return s.handleGetResource(string(msg.FromUserName), keyword) } if keyword == "" { utils.Info("[WECHAT:MESSAGE] 关键词为空,返回提示消息") 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) if err != nil { utils.Error("[WECHAT:SEARCH] 搜索失败: %v", err) return message.NewText("搜索服务暂时不可用,请稍后重试"), nil } utils.Info("[WECHAT:MESSAGE] 搜索完成,找到 %d 个资源", len(resources)) if len(resources) == 0 { utils.Info("[WECHAT:MESSAGE] 未找到相关资源,返回提示消息") return message.NewText(fmt.Sprintf("未找到关键词\"%s\"相关的资源,请尝试其他关键词", keyword)), nil } // 创建搜索会话并保存第一页结果 s.searchSessionManager.CreateSession(string(msg.FromUserName), keyword, resources, 4) pageResources := s.searchSessionManager.GetCurrentPageResources(string(msg.FromUserName)) // 格式化第一页搜索结果 resultText := s.formatSearchResultsWithPagination(keyword, pageResources, string(msg.FromUserName)) utils.Info("[WECHAT:MESSAGE] 格式化搜索结果,返回文本长度: %d", len(resultText)) return message.NewText(resultText), nil } // handlePrevPage 处理上一页命令 func (s *WechatBotServiceImpl) handlePrevPage(userID string) (interface{}, error) { session := s.searchSessionManager.GetSession(userID) if session == nil { return message.NewText("没有找到搜索记录,请先进行搜索"), nil } if !s.searchSessionManager.HasPrevPage(userID) { return message.NewText("已经是第一页了"), nil } prevResources := s.searchSessionManager.PrevPage(userID) if prevResources == nil { return message.NewText("获取上一页失败"), nil } currentPage, totalPages, _, _ := s.searchSessionManager.GetPageInfo(userID) resultText := s.formatPageResources(session.Keyword, prevResources, currentPage, totalPages, userID) return message.NewText(resultText), nil } // handleNextPage 处理下一页命令 func (s *WechatBotServiceImpl) handleNextPage(userID string) (interface{}, error) { session := s.searchSessionManager.GetSession(userID) if session == nil { return message.NewText("没有找到搜索记录,请先进行搜索"), nil } if !s.searchSessionManager.HasNextPage(userID) { return message.NewText("已经是最后一页了"), nil } nextResources := s.searchSessionManager.NextPage(userID) if nextResources == nil { return message.NewText("获取下一页失败"), nil } currentPage, totalPages, _, _ := s.searchSessionManager.GetPageInfo(userID) resultText := s.formatPageResources(session.Keyword, nextResources, currentPage, totalPages, userID) return message.NewText(resultText), nil } // handleGetResource 处理获取资源命令 func (s *WechatBotServiceImpl) handleGetResource(userID, command string) (interface{}, error) { session := s.searchSessionManager.GetSession(userID) if session == nil { 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) if err != nil { _, err = fmt.Sscanf(command, "获取 %d", &index) if err != 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("❌ 资源编号超出范围\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)) // 描述 if resource.Description != "" { result.WriteString(fmt.Sprintf("\n📝 描述:\n %s\n", resource.Description)) } // 文件大小 if resource.FileSize != "" { result.WriteString(fmt.Sprintf("\n📊 大小: %s\n", resource.FileSize)) } // 作者 if 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📥 转存链接:\n %s", resource.SaveURL)) } else if 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 } // formatSearchResultsWithPagination 格式化带分页的搜索结果 func (s *WechatBotServiceImpl) formatSearchResultsWithPagination(keyword string, resources []entity.Resource, userID string) string { currentPage, totalPages, _, _ := s.searchSessionManager.GetPageInfo(userID) return s.formatPageResources(keyword, resources, currentPage, totalPages, userID) } // formatPageResources 格式化页面资源 // 根据用户需求,搜索结果中不显示资源链接,只显示标题和描述 func (s *WechatBotServiceImpl) formatPageResources(keyword string, resources []entity.Resource, currentPage, totalPages int, userID string) string { var result strings.Builder 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)*4 + i + 1 resourceText.WriteString(fmt.Sprintf("%d. 📌 %s\n", globalIndex, resource.Title)) if resource.Description != "" { // 限制描述长度以避免消息过长(正确处理中文字符) desc := resource.Description // 将字符串转换为 rune 切片以正确处理中文字符 runes := []rune(desc) if len(runes) > 50 { desc = string(runes[:50]) + "..." } resourceText.WriteString(fmt.Sprintf(" 📝 %s\n", desc)) } // 添加标签显示(格式:🏷️标签,空格,再接别的标签) 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()) } // 添加分页提示 var pageTips []string if currentPage > 1 { pageTips = append(pageTips, "上一页") } if currentPage < totalPages { pageTips = append(pageTips, "下一页") } if len(pageTips) > 0 { result.WriteString(fmt.Sprintf("💡 提示:回复\"%s\"翻页\n", strings.Join(pageTips, "\"或\""))) } // 确保消息不超过微信限制(正确处理中文字符) message := result.String() // 将字符串转换为 rune 切片以正确处理中文字符 runes := []rune(message) if len(runes) > 600 { // 如果还是超过限制,截断消息(微信建议不超过600个字符) message = string(runes[:597]) + "..." } return message } // handleEventMessage 处理事件消息 func (s *WechatBotServiceImpl) handleEventMessage(msg *message.MixMessage) (interface{}, error) { if msg.Event == message.EventSubscribe { // 新用户关注 return message.NewText(s.config.WelcomeMessage), nil } return nil, nil } // SearchResources 搜索资源 func (s *WechatBotServiceImpl) SearchResources(keyword string) ([]entity.Resource, error) { // 使用统一搜索函数(包含Meilisearch优先搜索和违禁词处理) return UnifiedSearchResources(keyword, s.config.SearchLimit, s.systemConfigRepo, s.resourceRepo) } // formatSearchResults 格式化搜索结果 func (s *WechatBotServiceImpl) formatSearchResults(keyword string, resources []entity.Resource) string { var result strings.Builder result.WriteString(fmt.Sprintf("🔍 搜索\"%s\"的结果(共%d条):\n\n", keyword, len(resources))) for i, resource := range resources { result.WriteString(fmt.Sprintf("%d. %s\n", i+1, resource.Title)) if resource.Cover != "" { result.WriteString(fmt.Sprintf(" ![封面](%s)\n", resource.Cover)) } if resource.Description != "" { desc := resource.Description if len(desc) > 50 { desc = desc[:50] + "..." } result.WriteString(fmt.Sprintf(" %s\n", desc)) } if resource.SaveURL != "" { result.WriteString(fmt.Sprintf(" 转存链接:%s\n", resource.SaveURL)) } else if resource.URL != "" { result.WriteString(fmt.Sprintf(" 资源链接:%s\n", resource.URL)) } result.WriteString("\n") } result.WriteString("💡 提示:回复资源编号可获取详细信息") return result.String() } // SendWelcomeMessage 发送欢迎消息(预留接口,实际通过事件处理) func (s *WechatBotServiceImpl) SendWelcomeMessage(openID string) error { // 实际上欢迎消息是通过关注事件自动发送的 // 这里提供一个手动发送的接口 if !s.isRunning || s.wechatClient == nil { return fmt.Errorf("微信客户端未初始化") } // 注意:Customer API 需要额外的权限,这里仅作示例 // 实际应用中可能需要使用模板消息或其他方式 return nil }