message_service.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. package service
  2. import (
  3. "errors"
  4. "log"
  5. "github.com/2930134478/AI-CS/backend/models"
  6. "github.com/2930134478/AI-CS/backend/repository"
  7. "gorm.io/gorm"
  8. )
  9. // ErrConversationClosed indicates operations are attempted on a closed conversation.
  10. var (
  11. // ErrConversationClosed 表示会话已关闭,不能继续发送消息。
  12. ErrConversationClosed = errors.New("conversation is closed")
  13. // ErrConversationNotFound 表示未找到指定的会话记录。
  14. ErrConversationNotFound = gorm.ErrRecordNotFound
  15. )
  16. // MessageService 负责消息领域的业务处理。
  17. type MessageService struct {
  18. conversations *repository.ConversationRepository
  19. messages *repository.MessageRepository
  20. hub BroadcastHub
  21. aiService *AIService // AI 服务(用于 AI 自动回复)
  22. }
  23. // NewMessageService 创建 MessageService 实例。
  24. func NewMessageService(
  25. conversations *repository.ConversationRepository,
  26. messages *repository.MessageRepository,
  27. hub BroadcastHub,
  28. aiService *AIService,
  29. ) *MessageService {
  30. return &MessageService{
  31. conversations: conversations,
  32. messages: messages,
  33. hub: hub,
  34. aiService: aiService,
  35. }
  36. }
  37. // CreateMessage 创建消息并通过 WebSocket 广播。
  38. func (s *MessageService) CreateMessage(input CreateMessageInput) (*models.Message, error) {
  39. conv, err := s.conversations.GetByID(input.ConversationID)
  40. if err != nil {
  41. return nil, err
  42. }
  43. if conv.Status == "closed" {
  44. return nil, ErrConversationClosed
  45. }
  46. if input.SenderIsAgent && input.SenderID == 0 {
  47. return nil, errors.New("sender_id is required for agent messages")
  48. }
  49. message := &models.Message{
  50. ConversationID: input.ConversationID,
  51. SenderID: input.SenderID,
  52. SenderIsAgent: input.SenderIsAgent,
  53. Content: input.Content,
  54. MessageType: "user_message",
  55. ChatMode: conv.ChatMode, // 记录消息发送时的对话模式
  56. IsRead: false,
  57. // 文件相关字段(可选)
  58. FileURL: input.FileURL,
  59. FileType: input.FileType,
  60. FileName: input.FileName,
  61. FileSize: input.FileSize,
  62. MimeType: input.MimeType,
  63. }
  64. if err := s.messages.Create(message); err != nil {
  65. return nil, err
  66. }
  67. // 如果客服发送消息,且会话的 agent_id 为 0,则更新为当前客服的 ID
  68. updateFields := map[string]interface{}{
  69. "updated_at": message.CreatedAt,
  70. }
  71. if input.SenderIsAgent && input.SenderID > 0 && conv.AgentID == 0 {
  72. updateFields["agent_id"] = input.SenderID
  73. }
  74. if err := s.conversations.UpdateFields(conv.ID, updateFields); err != nil {
  75. return nil, err
  76. }
  77. if s.hub != nil {
  78. // 1. 先广播到该对话的所有客户端(访客和已连接该对话的客服)
  79. s.hub.BroadcastMessage(message.ConversationID, "new_message", message)
  80. // 2. 如果是访客发送的消息,且对话模式是人工客服,才广播到所有客服
  81. // 这样即使客服没有连接到这个对话,也能收到新消息的通知
  82. // 注意:AI 模式下的访客消息不广播给客服(避免干扰)
  83. if !input.SenderIsAgent && conv.ChatMode == "human" {
  84. s.hub.BroadcastToAllAgents("new_message", message)
  85. }
  86. } else {
  87. log.Printf("⚠️ WebSocket Hub 为空,无法广播消息: 消息ID=%d, 对话ID=%d", message.ID, message.ConversationID)
  88. }
  89. // 3. 触发 AI 回复的两种情况:
  90. // a) 访客对话 + AI 模式 + 访客发送的消息
  91. // b) 内部对话(知识库测试)+ 客服发送的消息
  92. needAIReply := s.aiService != nil && (
  93. (conv.ChatMode == "ai" && !input.SenderIsAgent) ||
  94. (conv.ConversationType == "internal" && input.SenderIsAgent))
  95. if needAIReply {
  96. go func() {
  97. // 用于查找 AI 配置的用户 ID:访客对话用 AgentID,内部对话用发送者(客服)ID
  98. userID := conv.AgentID
  99. if userID == 0 {
  100. userID = 1
  101. }
  102. if conv.ConversationType == "internal" && input.SenderID > 0 {
  103. userID = input.SenderID
  104. }
  105. aiResponse, err := s.aiService.GenerateAIResponse(message.ConversationID, input.Content, userID)
  106. if err != nil {
  107. log.Printf("❌ AI 生成回复失败: %v", err)
  108. // 使用友好的错误消息
  109. aiResponse = "AI客服好像出了点差错,请联系人工客服解决"
  110. }
  111. // 创建 AI 回复消息
  112. aiMessage := &models.Message{
  113. ConversationID: message.ConversationID,
  114. SenderID: 0, // AI 消息的 SenderID 为 0
  115. SenderIsAgent: true, // AI 回复视为客服消息
  116. Content: aiResponse,
  117. MessageType: "user_message",
  118. ChatMode: "ai", // AI 回复消息的模式为 "ai"
  119. IsRead: false,
  120. }
  121. if err := s.messages.Create(aiMessage); err != nil {
  122. log.Printf("❌ 创建 AI 回复消息失败: %v", err)
  123. return
  124. }
  125. // 更新对话的更新时间
  126. if err := s.conversations.UpdateFields(conv.ID, map[string]interface{}{
  127. "updated_at": aiMessage.CreatedAt,
  128. }); err != nil {
  129. log.Printf("⚠️ 更新对话时间失败: %v", err)
  130. }
  131. // 广播 AI 回复消息
  132. if s.hub != nil {
  133. // AI 回复只广播给访客,不广播给客服(避免干扰)
  134. // 客服可以在会话页面手动开启"显示 AI 消息"来查看
  135. s.hub.BroadcastMessage(aiMessage.ConversationID, "new_message", aiMessage)
  136. // 不再广播到所有客服
  137. // s.hub.BroadcastToAllAgents("new_message", aiMessage)
  138. }
  139. }()
  140. }
  141. return message, nil
  142. }
  143. // ListMessages 返回会话内的消息列表。
  144. // includeAIMessages: 是否包含 AI 消息(默认 false,不包含)
  145. // 如果 includeAIMessages == false,过滤掉所有 chat_mode == "ai" 的消息
  146. // 这样就能准确区分 AI 模式下的消息和人工模式下的消息,即使对话模式切换了也能正确过滤
  147. func (s *MessageService) ListMessages(conversationID uint, includeAIMessages bool) ([]models.Message, error) {
  148. messages, err := s.messages.ListByConversationID(conversationID)
  149. if err != nil {
  150. return nil, err
  151. }
  152. // 如果不包含 AI 消息,过滤掉所有 chat_mode == "ai" 的消息
  153. // 这样,无论对话当前是什么模式,都能准确过滤掉 AI 模式下的所有消息
  154. // 包括:访客在 AI 模式下发送的消息、AI 回复消息
  155. if !includeAIMessages {
  156. filtered := make([]models.Message, 0, len(messages))
  157. for _, msg := range messages {
  158. // 只显示 chat_mode != "ai" 的消息(人工模式下的消息)
  159. // 如果 chat_mode 为空(兼容历史数据),则根据 SenderID 和 SenderIsAgent 判断
  160. if msg.ChatMode != "" {
  161. // 有 chat_mode 字段,直接根据字段过滤
  162. if msg.ChatMode != "ai" {
  163. filtered = append(filtered, msg)
  164. }
  165. } else {
  166. // 兼容历史数据:chat_mode 为空时,使用旧逻辑
  167. // 过滤掉 AI 回复消息(SenderID == 0 && SenderIsAgent == true)
  168. if msg.SenderID != 0 || !msg.SenderIsAgent {
  169. filtered = append(filtered, msg)
  170. }
  171. }
  172. }
  173. return filtered, nil
  174. }
  175. return messages, nil
  176. }
  177. // MarkMessagesRead 将消息标记为已读并通知监听方。
  178. func (s *MessageService) MarkMessagesRead(conversationID uint, readerIsAgent bool) (*MarkMessagesReadResult, error) {
  179. messageIDs, unreadRemaining, readAt, err := s.messages.MarkMessagesRead(conversationID, !readerIsAgent)
  180. if err != nil {
  181. return nil, err
  182. }
  183. result := &MarkMessagesReadResult{
  184. ConversationID: conversationID,
  185. MessageIDs: messageIDs,
  186. UnreadCount: unreadRemaining,
  187. ReadAt: readAt,
  188. }
  189. if s.hub != nil && len(messageIDs) > 0 {
  190. s.hub.BroadcastMessage(conversationID, "messages_read", map[string]interface{}{
  191. "message_ids": messageIDs,
  192. "reader_is_agent": readerIsAgent,
  193. "read_at": readAt,
  194. "unread_count": unreadRemaining,
  195. "conversation_id": conversationID, // 确保 payload 中也包含 conversation_id
  196. })
  197. }
  198. return result, nil
  199. }